SOQL For Loop的效率问题

一天,有人和我抱怨说系统越跑越慢。
说当年系统刚开发出来的时候,页面唰的一下就打开了。
而现在同样的页面却要转圈转个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改成先查询后循环的方式就结案了呢。
不,我们最后把这个表格改成了异步加载。。。。

《SOQL For Loop的效率问题》有8个想法

  1. 最后一句完美反转,以为懂了这期主题,看到最后又懵了。“表格改成了异步加载”是什么操作呢?

  2. Good share, 有什么方法能改变SOQL For Loop每次取得记录的数量吗?

    1. 由于是SFDC内建的机制,目前没什么好办法,根据文档,只能选择是1还是200。

  3. Good Share,有什么办法可以变更SOQL For Loop每次取得的数量吗?

  4. 精彩,文风诙谐,调优分析步骤严谨,再加以实验数据论证,最后给出root cause 及解决方案。教科书般的演示

回复 Balabala 取消回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据