本文回复笔者知识星球里一个朋友的提问:
当前程序外层 loop 数量大约100万,在 loop 里单条 select 从 a018,konp 表取价格,程序运行时间过长,开发优化了程序,将读取 a018,konp 放到 loop 外边,然后再 loop 中用 read 从内表读取价格(二分法)。 结果性能对比测试,基本没提升。 数据情况,loop 数量大约100万,a018/konp分别为5万左右,内表用得是标准。 是否用排序内表或者hash内表更好?
本文咱们就来聊聊这个性能问题。
该问题其实很有代表性:外层循环大约 100 万,循环里单条 SELECT 去 A018 和 KONP 取价格,开发人员把数据库读取挪到循环外,再在循环里对内表做 READ,而且还说用了二分法,结果前后性能差异不明显。
遇到这种情况,我的第一判断不是 ABAP 没道理,而是这次优化虽然换了位置,却没有真正换掉访问模型。
ABAP 标准内表默认不是优化键访问,标准内表走的是线性查找,排序内表走二分查找,哈希内表走哈希访问,二级键访问才会稳定进入优化路径。
目前已经做的性能优化其实只做了一个表层动作,把 SELECT 从循环里挪到了循环外。
可如果循环里的查找依旧没有用上真正合适的键,或者取数粒度还是不对,或者真正耗时压根不在这两张表访问上,那结果自然就会变成,代码看起来更讲道理了,跑起来却没多大变化。
很多性能问题并不是 SQL 和内表的简单对抗,而是数据库访问、应用层查找、键设计、数据去重、宽行搬运、业务判断,全都掺在一起。
标准内表这四个字,恰恰可能是问题的核心
问题描述里特别提到了一句,内表类型用的是标准内表。
这个信息很关键。因为不少项目里大家口头上说用了二分法,实际代码里只是把数据放进了STANDARD TABLE,再写了个READ TABLE ... WITH KEY,甚至连BINARY SEARCH都没加,或者排序顺序和读取键顺序根本不一致。
这样一来,所谓优化很可能只是心理安慰。
标准内表按键访问默认是线性搜索。只有在满足条件时,BINARY SEARCH才会把标准内表访问变成二分查找。
这里面有个特别容易踩坑的点,二分查找不是你写了BINARY SEARCH就万事大吉,它对排序顺序有很严格的要求。
内表必须按搜索键字段的升序排序,而且排序字段的优先顺序必须和搜索键顺序严格匹配。
举个例子,你如果先按lifnr matnr ekorg排,再按lifnr ekorg matnr去读,代码不会替你兜底。这已经不是快不快的问题,连读取结果都或许不可靠。
要是项目里只是口头说用了二分法,但没有把排序键和读取键做成完全一致,那性能没起来,甚至逻辑有隐藏风险,都是很正常的结果。(Eduardo Copat)
还有一个经常被忽略的细节,standard key往往包含过多字段,键访问本身就可能产生性能问题。也就是说,很多人以为自己定义了标准内表,图的是灵活,实际却把访问路径搞得很模糊。
对问题里提到的这种100 万次高频查价来说,表类型和表键设计不是辅助信息,而是主角。
如果没有把键设计清楚,哪怕把SELECT全搬到循环外,最终也可能只是把数据库层面的读数据消耗,换成了应用服务器里的比较和拷贝。
SAP ABAP 帮助文档对此有着清晰的提醒:
https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/ABENITAB_STANDARD_KEY.html
50,000 行看着不大,100 万次查找累起来就完全是另一回事
不少人看到A018和KONP大约各5 万行,会有一种直觉:内表才这么大,读起来能有多慢?
问题就在于,单次不慢,不代表累计不慢。
真正的二分查找,5 万行大约是16次左右的比较。
外层100 万次循环,就相当于做了接近1,600 万次键比较。
这还没算业务字段赋值、结构复制、条件判断、命中后再次读取KONP、日期有效期判断、金额单位处理这些动作。
放在一台很忙的应用服务器上,这笔账绝对不算小。
它通常会比100 万次数据库往返便宜很多,但它未必已经便宜到可以忽略。
这可以从理论上解释优化后观察到的现象:数据库访问减少了,但性能并没有提升多少。
这个时候再往下深挖,核心就变成一句话:这次优化到底是把100 万次数据库访问,变成了100 万次真正优化过的内存查找,还是只是把性能开销从数据库搬到了ABAP层?
这个问题,靠猜永远猜不出来。用 ABAP 提供的各种性能评测事务码,一测便知。
就拿采购定价类程序来说,外层100 万行业务数据,真正不同的取价键往往远小于100 万。很多时候只是同一批供应商、采购组织、物料、日期组合在反复出现。
也就是说,性价比最高的优化动作,往往不是把SELECT机械地挪出循环,而是先把外层数据按取价键去重,把100 万行压成几万条真正不同的查价请求,再一次性联表取回结果。
具体实践过程中避免嵌套选择,优先使用JOIN、VIEW、SUBQUERY,需要时再结合FOR ALL ENTRIES,但要注意驱动内表不能为空,还要配合跟踪工具看真实效果。
把 A018 和 KONP 拉到循环外,不等于就已经拿到了最优路径
当前做法里还有另一层隐患。
假设开发人员在循环外把A018、KONP分开取出来,循环里先读A018,再根据命中的条件记录去读KONP,表面上像是省掉了数据库开销,实质上只是把数据库里的联接动作拆到了ABAP层。
多表读取时,出于性能考虑,更高优先级的方案通常是数据库侧的JOIN、VIEW或SUBQUERY。
尤其在SAP HANA场景里,数据库就是为这种集合式处理设计的。
如果把本该让数据库一次性做完的事情拆成应用层的多次内表定位,很多时候不但没有赚到,反而会因为应用层要反复构造键、比较字段、复制结构、处理重复命中而把时间又赔回去。
再往深一层看,采购价格这类逻辑天生就经常带有效期、条件类型、组织维度、物料维度、甚至数量等级这样的约束。
只要你的读取不是按最终唯一业务键做精确命中,而是按一个前缀键命中一批候选记录,再在ABAP里逐条判断谁才是有效价格,那二分查找就只能帮你迅速找到候选区间,帮不了你选出最终那条。
很多团队误以为自己用了二分法,成本就接近零了。
其实不然,二分法只是把5 万行全表扫描,换成了从某个位置开始局部扫描。要是候选区间不小,外层又有100 万次,这部分累计起来的性能开销同样会非常可观。
还有一类情况,数据库访问其实已经不算贵了,真正的慢在别的地方
现在看到性能优化前后差异很小,还有一种很现实的解释,原来的那条SELECT虽然写在循环里,但它对数据库来说已经是高选择性、命中良好、代价不高的访问。
与此同时,外层循环本身、字段转换、条件判断、内表拷贝、价格回写、后续汇总、排序、输出,才是真正的大头。
此时可以用SAT 事务码做运行时分析,用ST05或SQLM看SQL的执行次数和总耗时,再结合SWLT把静态检查和运行时数据合在一起看。
使用工具的意义,在于它能把感觉型优化变成证据型优化。
只要跑一次新旧版本的SAT,再配合一次ST05或SQLM,马上就能判断,原来的热点到底在SQL,还是已经转移到内表处理和其他逻辑上了。
如果SAT里显示,READ TABLE、数据搬运、价格判断这些语句已经排在最前面,而ST05里A018、KONP的总SQL时间占比并不高,那就说明你们的SELECT并不是决定性矛盾。
相反,如果ST05里还能看到海量单条SELECT,或者SQLM里这两张表的语句执行次数仍然是天文数字,那就说明现在的优化根本没有落地到位,可能只是把一部分逻辑搬了出来,但循环里的数据库访问仍在暗处继续发生。
SQLM会统计每条SQL的执行次数和聚合信息,SWLT则能把这些运行时数据和静态性能检查按源码位置合并起来,非常适合本文这种已经做过一轮优化、但收益不符合预期的场景。
下一步该怎么做?
我觉得下一步的优化方向不是继续凭感觉改,而是沿着四条线同时推进。
第一条线是量化热点。
你要做的是同一批测试数据、同一台系统、同一套变式,分别跑旧版本和新版本,抓SAT、ST05、SQLM。
SAT看ABAP语句和调用层级,ST05看具体数据库访问和执行计划,SQLM看一段业务流程里的SQL聚合。只要这三份证据一摆出来,你们内部关于到底慢在哪里的争论,基本就能停了。
第二条线是把100 万次查价压成尽量少的唯一键查价。
本文这个场景,最该做的不是一股脑把A018、KONP全读到内表里,而是先从外层数据里抽出真正决定价格的字段组合,做一张唯一键驱动表。
采购价格这类场景里,很多输入行只是业务单据不同,取价键却是一样的。
如果把100 万行压到2 万或3 万个唯一键,再去数据库一次性取价格,收益通常远远大于单纯的内表替换。
这也是为什么我一直强调,性能优化不是SELECT和READ的对换,而是访问粒度的重构。
第三条线是把内表结构改对。
对高频精确等值查找,首选通常是HASHED TABLE,因为它的键访问时间基本不随数据量增长而变化。
哈希键访问响应时间是常量级。
要是你的取价结果能够整理成唯一业务键对应唯一结果,那就不要再用标准内表加手工二分了,直接上哈希表。
要是你的查价逻辑天然带范围,比如按前导字段定位后,还要在有效期区间里找一条最合适的记录,那更适合SORTED TABLE或者给标准内表加一个真正命中读取路径的排序二级键。
不过也要注意,二级键会增加维护时间和内存,所以别贪多,只给真正高频读路径建一个有用的键。
本文这个场景属于先一次性装载、后高频只读,正是二级键比较合适的场景。
笔者还有一句经验判断,也可以拿来快速筛查。
真正有效的内表优化,通常会让ST05里的SQL执行次数和总时间明显下降。
如果看到ST05的数据库时间已经掉下去了,但总运行时间几乎没变,那八成就是ABAP层成了新热点。
反过来,如果ST05里A018、KONP的访问次数还是高得离谱,那就说明你们所谓的SELECT循环外置,并没有真正切断循环里的数据库访问链路。
一段话总结全文。
现在的优化之所以没有明显收益,大概率不是因为ABAP内表查找不如数据库,而是因为这次优化只做了SELECT位置迁移,没有同时完成三件更重要的事。
其一,没有确认循环里的读取是否真的命中了优化键。
其二,没有把100 万次业务查价压缩成尽量少的唯一取价键。
其三,没有把多表组合和结果缩减尽量交给数据库一次性完成。
只要这三件事里有一件没做好,SELECT挪出去这件事带来的收益就会被抵消掉,甚至被新的应用层开销吞掉。
所以下一步别再问开发是不是把 SELECT 再改一改就行,而是先用SAT和ST05把热点坐实,再按唯一取价键去重,再把结果集用JOIN缩小,再按最终查找特征把结果放进HASHED TABLE或SORTED TABLE,而不是继续拿STANDARD TABLE当万金油。
只要这套路径走通了,这类报表的性能改善通常才会真正出现,而且改善会更稳定,不会停留在测试里偶尔快一点、上线后又说不清楚的状态。
也欢迎大家加入笔者的知识星球,一起讨论 SAP 技术问题。