一天,有人和我抱怨说系统越跑越慢。
说当年系统刚开发出来的时候,页面唰的一下就打开了。
而现在同样的页面却要转圈转个800年。
客户天天向他抱怨。
听到这个现象我瞬间来了兴趣,随即放下了手中的工作。
这位兄台,借一步说话。
千里迢迢赶到了案发现场。兄台亲手做了演示。
确实,那个号称唰一下就打开的页面,现在要转个许久才行。
从上到下的仔细观察了一下页面(是个VF)的内容,发现有个带翻页的表格。
除了这个表格之外,其他的内容都只是简简单单的字段内容显示。
总体来讲,一眼看上去也没有什么非常不对劲的东西。
Talk is cheap, Show me the code.
兄台麻利儿的打开了VF和Controller的代码。
大概扫了一遍,格式工整,命名规范,结构清晰,注释齐备,没有任何奇葩写法。
那就奇怪了,如果是代码写法问题,那么应该从一开始就慢才对。
如果说有什么东西是随着时间不断积累的,不一定是经验和阅历,也有可能是数据量。
所以我又扫了一遍Controller里的SOQL。
发现只有在初始化表格的SOQL中使用了LIMIT 10000。
而其他的SOQL大都直接指定了ID作为检索条件。
我指了指那条SOQL,与那位兄台确认了一下眼神,不过兄台一脸迷茫。
“咳。。。我觉得是这里的问题,系统刚开始用的时候没有数据,所以这里初始化做的很快。但系统用久了之后,数据越来越多,这里要处理的数据也越来越多,所以就慢了。”
“哦。。。但是,才10000条数据,应该不至于啊。。。”
我竟一时语塞。
盯着这段代码半晌。
.................................... for(tom__c t: [SELECT Id, Name FROM tom__c WHERE RecordTypeId = 'XXXXXXXXXXXXXX' LIMIT 10000]) { tableRowList.add(new tableRow(t)); } .................................
求锤得锤,用实锤说话。
换测试环境,开Debug Log!
为了看到底是哪里拖慢了页面速度,我在每块处理前后都加了system.debug();
虽然做了万全之准备,结果却仍让人始料未及。
1. Debug Log的每一步的执行时间居然丢失了毫秒细节!
令人不得不吐槽的是,在Developer Console里面打开同样的一条Debug Log,执行时间的毫秒细节居然是有的!
2.由于Log内容太多,输出内容被无情截断。
所以只好换一个打法。
不再尝试执行完整transaction,
而是将每一块代码都改造成独立的代码块,
前后加上system.debug(datetime.now() + ‘ —- ‘ + datetime.now().millisecond());
逐一在匿名块中执行。
经过如同人生般漫长而又枯燥的实验后。
终于发现,耗时最长的确实是LIMIT 10000的那块代码。
那我就奇怪了,就是循环个10000条数据,也不至于那么慢啊。
难道是因为SOQL For Loop。
于是,立即动手做了下面的实验。
1. 准备一个干净的DE
2. 准备2000条Account(再多放不下)
3. 分别执行下面的匿名块
system.debug(datetime.now() + ' ---- ' + datetime.now().millisecond()); for(Account acc : [Select Id, Name from Account]){ String a = acc.Name; } system.debug(datetime.now() + ' ---- ' + datetime.now().millisecond()); // --------------WoShiFenGeXian---------------- system.debug(datetime.now() + ' ---- ' + datetime.now().millisecond()); List<Account> accList = [Select Id, Name from Account]; for(Account acc : accList){ String a = acc.Name; } system.debug(datetime.now() + ' ---- ' + datetime.now().millisecond());
4.调换顺序多次执行,以确保结果公平。因为最先跑的没有缓存,索引等数据库优化,会吃亏。所以为了严谨要排除前几轮之后取平均值。
结果如下。
SOQL For Loop写法,2061条数据循环,427-324=103毫秒。
先查询后循环写法,2061条数据循环,995-924=71毫秒。
速度提升 (103-71)/103*100%约等于31%
先查询后循环写法大比分胜出。
由于我的实验的循环内容几乎是没有任何负载,所以循环处理的消耗可以忽略不计。
但是,如果每次循环都很耗时(很不幸,这位兄台的系统就是如此),
这个百分比还会扩大。
那么为什么SOQL For Loop的写法会慢呢。
SOQL For Loop的官方文档介绍
原来SOQL For Loop的写法不仅仅是节省代码行数,为了美观。
而是改变了底层原理。
SOQL For Loop不再是将检索结果一起从数据库里取出来之后,使用迭代器一条一条处理。
而是改用SOAP API,使用Query与QueryMore从数据库里拿一批处理一批。
所以SOQL For Loop因此具有了额外的特性——————可以选择200条循环一次。
到最后就变成了一个经典的困境。
在生活中,你只能用时间去换钱,或者用钱换时间。
在算法里,必须用空间换时间,或者用时间换空间。
先查询后循环的方式,由于要先把整个结果集放到内存里,所以要使用很大的一块空间,对于viewState紧张的页面,或者已经占用了大量内存的代码,可能会成为压死骆驼的最后一根稻草。
而SOQL For Loop拿到一批,处理一批,释放一批的做法,不会占用大量的内存空间。但代价是要承担这部分性能消耗所造成的额外时间开销。
如果开启更详细的Debug log,则可以看到SOQL For Loop在循环的时候在疯狂的进行内存操作。
那么瓶颈找到了。
我们是不是把SOQL For Loop改成先查询后循环的方式就结案了呢。
不,我们最后把这个表格改成了异步加载。。。。
啥时候更新啊?
很不错。
写得不错。
最后一句完美反转,以为懂了这期主题,看到最后又懵了。“表格改成了异步加载”是什么操作呢?
Good share, 有什么方法能改变SOQL For Loop每次取得记录的数量吗?
由于是SFDC内建的机制,目前没什么好办法,根据文档,只能选择是1还是200。
Good Share,有什么办法可以变更SOQL For Loop每次取得的数量吗?
精彩,文风诙谐,调优分析步骤严谨,再加以实验数据论证,最后给出root cause 及解决方案。教科书般的演示