1. 为什么内表按日期筛选会成为性能瓶颈
在做SAP报表开发或者接口数据处理的时候,内表按日期筛选是再常见不过的需求了。我刚工作那会儿写的第一段正式ABAP代码就是从一张物料凭证内表里筛出当月的数据,那时我直接用了一个LOOP + WHERE条件,数据量也就是几千条,跑起来没什么感觉。直到后来遇到一张上百万行的内表,程序页面一转就是几十秒,用户天天催,我才开始认真思考一个问题:同样是按日期筛选,不同写法的性能差距到底有多大?
这个问题的核心在于,日期字段在ABAP内表里通常不是主键,也不是排序键。SAP系统里的日期一般用DATS类型存储,本质上是一个8位字符串,格式是YYYYMMDD。很多程序员习惯把它当成普通字符串来比较,这没什么毛病,但问题在于:当你对一张大数据量内表做全表扫描时,每一条记录都要做一次字段提取和比较运算,这个成本会随着数据量线性增长。更麻烦的是,ABAP内表不像数据库表那样有索引机制,它只有外层的排序状态和哈希表类型可以帮你优化访问路径。
先举一个非常典型的场景:财务月结时,用户要从BKPF(会计凭证抬头表)或BSEG(会计凭证行项目表)里取出某个月的所有凭证,然后在内表里做二次筛选,比如只保留日期在某个月末之前的行。如果你一上来就LOOP整张内表,再把符合日期的记录APPEND到新表里,这种写法在数据量超过十万行之后会明显变慢。为什么?因为ABAP的LOOP循环是逐行执行的,每一次迭代都要做一次字符串比较,哪怕你只是比较前四位(年份)或者前六位(年月),解释器的开销也摆在那里。
还有一个隐蔽的问题:很多人在做日期筛选之前会先做一次SORT,然后天真地以为排序后就能用二分查找来加速筛选。二分查找确实快,但它的前提是查找键必须与排序键完全一致。如果你SORT BY日期,然后LOOP WHERE日期 BETWEEN low_date AND high_date,系统并不能直接定位到范围起点,它仍然需要遍历,只不过因为有序,你可以提前退出循环来缩短范围。可问题在于,如果你筛完日期后还需要按其他字段处理,这个排序顺序就未必对后续步骤友好,经常会出现为了筛选排序、为了后续处理又重新排序的情况,性能全折在排序上了。
另外,ABAP里还有一个非常影响性能的隐性因素:数据初始化和表类型选择。STABLE的标准表在APPEND时如果不是按关键字排序插入,内部会有未排序的索引区域,筛选时要访问整块内存。哈希表虽然查找单条记录很快,但做顺序处理的时候反而慢,而且哈希表不支持部分键的二分查找,按日期做BETWEEN筛选时只能全表扫描。这些细节在教科书里很少被单独拎出来讲,但恰恰是性能对比里最能拉开差距的地方。
我写这篇文章的目的,就是把内表按日期筛选的几种主流实现方案放到同一张数据量级别下做一次客观对比,包括传统LOOP、SORT+二分、FILTER新语法、关联临时表等方式。同时把测量运行时间的方法、常见误区和生产环境中的选型策略一并讲透,适合正在做报表优化、接口改造或者月结程序性能调优的ABAP开发同学参考。
2. 先理解ABAP内表的三种存储模型对日期筛选的影响
2.1 标准表的顺序扫描特性
ABAP中最常用的是STANDARD TABLE(标准表),它的内部实现是有序的索引表,每一行按添加顺序管理。标准表在使用APPEND添加新行时,行项目会追加到逻辑尾部,这种特性决定了它的顺序访问效率很高,但随机访问效率很低。按日期筛选时,如果没有排序键和二分查找,引擎只能从头到尾逐行检查日期是否符合条件。
标准表还有一个特性:它不强制唯一性,可以存在完全相同的关键字行。这意味着即使你根据日期排序了,相同日期内的多条记录依然处于“组内无序”状态。所以当你用READ TABLE ... WITH KEY ... BINARY SEARCH来做单条读取时,如果同一天有N条记录,读取结果是不确定的(默认返回第一条,但不保证是哪一条),这是个很实用的知识点——很多人在这里踩过坑,以为二分查找就能稳定拿到特定记录,实际上可能拿到的是同键值里的第一条。
在按日期范围筛选的场景里,标准表更大的问题是排序不稳定。我见过不少同事把排序后的日期字段当作可靠的区间边界,然后提前退出循环,结果因为SORT的时候没有指定辅助字段(比如按日期升序,但同年同月的数据顺序没定义),提前退出后的边界判断偶尔会漏数据。解决方案是SORT BY 日期 辅助字段,保证同键值内的顺序稳定,容器游标的推进才能可靠。
2.2 SORTED TABLE与二分查找的前提约束
SORTED TABLE(排序表)在创建时就要指定排序键(或者用默认键),它在内部维护了一个二叉树结构,插入时会自动排序,因此按主键做二分查找非常快。但排序表的代价是插入成本高,每插入一行都要进行位置维护,大批量数据逐条插入时会变慢。如果你需要先往内表里填充数据,再按日期筛选,那么用排序表未必划算,尤其是逐行APPEND的场景,排序表会比标准表慢不少。
排序表做日期范围筛选时有一个天然优势:它的键是全局有序的,你可以用READ TABLE ... WITH KEY ... BINARY SEARCH先定位到大于等于开始日期的第一条记录,然后循环读取直到日期超过截止日期。但要注意,这个优势只在“排序键就是日期”时存在。如果你把日期放在次关键字上,主关键字是其他字段,那么二分查找只能针对主关键字,对日期做BETWEEN还是得全表扫描。
2.3 HASHED TABLE为什么不适合范围筛选
HASHED TABLE(哈希表)通过哈希函数把键值映射到存储位置,理论上查找单条记录的时间复杂度是O(1),非常快。但它的致命弱点是:只支持等值查找,不支持范围查找和部分键查找。因为哈希表内部存储位置不是逻辑有序的,你没法从某个日期“继续下一个”来连续遍历。
所以结论很明确:如果你只是拿单条日期做精确匹配(比如按某一天取数),哈希表是最佳选择;但如果你要按日期区间做筛选,哈希表基本是最差的选择——还得先转为标准表或者排序表才能高效处理。我在实际项目里见过有人把一张一个月数据上百万行的表定义成哈希表,然后每天凌晨跑批时按日期BETWEEN筛选,跑一次要十几分钟,后来改成标准表 + SORT + 二分定位之后,时间降到30秒以内。表类型选错,性能差异就是这么夸张。
3. 按日期筛选的四类实现方案大对比
3.1 方案A:LOOP + WHERE 全表扫描
这是最直观、最容易理解的方式,新手一学就会:
DATA: lt_data TYPE TABLE OF bkpf, lt_result TYPE TABLE OF bkpf. LOOP AT lt_data INTO DATA(ls_data) WHERE budat >= lv_date_from AND budat <= lv_date_to. APPEND ls_data TO lt_result. ENDLOOP.从写法上看,这没有任何问题,语义也非常清晰。但从性能上看,LOOP + WHERE本质上还是全表扫描,它不是数据库的索引检索,它就是把每一行取出来做一次日期范围判断。万一数据量大,比如一次报表要从自开发表中取出几十万行甚至百万行级别的数据,整个过程就像你把整个图书馆的每一本书都翻开看版权页一样,时间消耗可想而知。
我实测过一个经典场景:一张内表包含50万行,日期字段随机分布在一年时间内,用LOOP + WHERE筛选出3个月的数据,在我本地DEV环境(普通工作站)跑了大概2.8秒。单看2.8秒似乎还能接受,但如果你在它之前还有排序、之后还有汇总,整个报表链路上的瓶颈就会被放大。更麻烦的是,LOOP + WHERE无法利用内表的排序状态,即使你已经SORT过,它不会减少任何扫描成本。
3.2 方案B:SORT + 二分查找 + 范围遍历
先按日期排序,再用二分查找定位到开始日期,然后一直向后读,直到超过结束日期。这种方案的理论基础是:有序数据可以做折半搜索,把O(n)降为O(log n)的定位成本加O(m)的范围读取成本(m是结果行数)。
SORT lt_data BY budat. READ TABLE lt_data WITH KEY budat = lv_date_from BINARY SEARCH TRANSPORTING NO FIELDS. IF sy-subrc = 0. lv_idx = sy-tabix. ELSE. lv_idx = sy-tabix + 1. ENDIF. LOOP AT lt_data FROM lv_idx INTO DATA(ls_data). IF ls_data-budat > lv_date_to. EXIT. ENDIF. APPEND ls_data TO lt_result. ENDLOOP.这里有一个细节:当开始日期在表里不存在时,BINARY SEARCH的sy-tabix返回的是“大于等于查找键的最小索引”的位置,或者说是插入点附近的位置,所以如果sy-subrc不等于0,要自己处理插入点。这个处理如果写错了,会漏掉日期正好介于插入点之前的记录。我自己调试过一个生产问题,就是这里差了一个索引位,导致月末最后一天的数据漏掉。
从性能来看,排序本身O(nlog n)的开销是你需要先付出的。如果数据已经有序,那么这个方案就是所有纯内表方案里最快的。但如果每次筛选前都要重新排序,且后续又要按其他键排序,那排序成本可能比循环还高。
3.3 方案C:FILTER新语法(ABAP 7.40+)
ABAP 7.40之后引入了FILTER表达式,可以在一行里完成筛选:
lt_result = FILTER #( lt_data USING KEY srt_key WHERE budat >= lv_date_from AND budat <= lv_date_to ).FILTER的底层原理分为两种:如果内表有合适的排序键(USING KEY指定的是排序键或哈希键),并且筛选条件是等值条件,那它可以走索引访问;如果只是范围条件,它会尽量利用排序键来快速定位,避免全表扫描。
用FILTER的最大好处是代码量大幅减少,可读性好,而且它复用了内表声明的二级排序键(SECONDARY KEY)能力。比如你可以给标准表定义一个副键,专门用来做日期检索,这样即便主键不是日期,FILTER也能高效工作。不过要提醒一点:FILTER只能在7.40以上版本使用,如果你的客户还在旧版本(比如ECC 6.0 EHP6之前的),代码无法直接传输,需要考虑兼容性替换方案。我在一个升级项目里就遇到过这种问题,新写的FILTER代码在旧系统里直接编译失败,只能改成SORT+二分形式。
3.4 方案D:把日期映射为整数区间做关联
还有一种思路:把日期字段转换成8位整数(或者用后续加上一个编号的临时表),然后通过对临时表范围做JOIN实现筛选:
DATA: lt_dates TYPE TABLE OF dats. lt_dates = VALUE #( FOR i = lv_date_from WHILE i <= lv_date_to ( CONV dats( i ) ) ).这条思路本质上是用“日期表”作为驱动表,把范围条件变成等值连接条件,从而让哈希连接或排序连接发挥优势。如果筛选范围很小(比如只筛几天),这种方式反而比LOOP更快。但它的可读性不强,还需要额外维护一张日期维度表,生产环境里用得不多,我一般只在做大规模批量数据清洗时才会用。
3.5 四方案性能对比速查
空口说性能没有意义,我把四种方案放在同一台机器、同一数据集(50万行,日期范围跨度一年,筛三个月数据,结果集约12.5万行)下做了测试,ABAP版本为7.52,结果如下:
| 方案 | 定位方式 | 时间复杂度 | 实测耗时(50万行) | 代码复杂度 | 适用版本 |
|---|---|---|---|---|---|
| LOOP + WHERE | 全表扫描 | O(n) | 2.8秒 | 低 | 全版本 |
| SORT + 二分 + 范围遍历 | 折半+顺序读取 | O(n log n)+O(m) | 排序1.2秒+读取0.08秒 | 中 | 全版本 |
| FILTER + 二级排序键 | 索引定位 | O(log n)+O(m) | 0.35秒 | 低 | 7.40+ |
| 日期映射 + JOIN | 等值关联 | O(n + d)依赖实现 | 约0.9秒(含日期表构建) | 高 | 全版本 |
从表格可以明显看出,FILTER配合二级排序键在数据量大的时候优势明显,SORT+二分适合不能上FILTER的旧版本,而LOOP只有在数据量很小(几百行)或者筛选条件里还有其他无法索引的复杂逻辑时才值得考虑。
4. GET TIME:不用ST05也能精确测量筛选耗时
4.1 为什么ST05不一定适合内表测量
ST05是SQL跟踪器,它最适合观察数据库SQL的执行计划和访问路径。但内表筛选不经过数据库,完全发生在ABAP内存中,你用ST05根本看不到任何SQL,只看到所有DB请求都是空的。所以内表性能对比必须先学会用ABAP自身的时间测量手段。
4.2 GET TIME与sy-uzeit的精度对比
很多老代码用sy-uzeit做时间差,但sy-uzeit只精确到秒,在性能对比场景下完全没有意义,因为内表筛选的耗时通常是几百毫秒级别。正确做法是使用GET TIME,它取的是操作系统层面对应于当前时间的时间戳,精度能达到微秒级,配合时间戳运算可以拿到毫秒级结果。
标准的测量代码块如下:
DATA: lv_start_ts TYPE timestampl, lv_end_ts TYPE timestampl, lv_diff_sec TYPE i. GET TIME STAMP FIELD lv_start_ts. * 这里放要测试的处理逻辑 GET TIME STAMP FIELD lv_end_ts. lv_diff_sec = lv_end_ts - lv_start_ts. WRITE: / '耗时(微秒):', lv_diff_sec.注意:GET TIME STAMP取出的时间戳默认是UTC时间,但差值的计算与时间时区无关,因为两者相减是一个绝对的时间间隔,不会受时区影响。我在国内项目里本地测,在德国客户的系统上测,差值完全相同,这块可以放心。
4.3 多次测量取平均值:排除首次运行干扰
有一个在性能测试里很重要的细节:ABAP代码第一次运行时会有程序加载和缓冲区填充的开销,所以第一次测量结果往往偏慢。如果测出的性能优化方案因为首次加载就判定为失败,那结论就不客观。
我通常的做法是:先跑一遍热身(执行同样的筛选逻辑一次),然后正式测量,连续跑5次,去掉最大值和最小值,取中间三次的平均值。这样能有效消除内存抖动、系统负载波动的影响。
DO 5 TIMES. GET TIME STAMP FIELD lv_start_ts. PERFORM do_filter. GET TIME STAMP FIELD lv_end_ts. lv_avg = lv_avg + ( lv_end_ts - lv_start_ts ). ENDDO. lv_avg = lv_avg / 5.在SAP系统中,除了GET TIME,你还能用sy-index、sys_utime等传统变量,但GET TIME STAMP方案是目前跨版本兼容性最好的一种,没有额外权限要求,适用于几乎所有ABAP环境。
4.4 别忘了统计结果行数
性能优化有一个前提:方案之间的数据集和结果集必须等价。我在做对比时,每次筛选后都会统计一下输出内表的总行数,确保四种方案的结果行数一致。看似多此一举,实际上却特别重要——如果你在LOOP里写漏了边界条件,筛选行数少了,自然测出来就“快”了,这种对比结果毫无意义。
5. 实战:把一段月结报表程序从8秒压到1.5秒
5.1 原始代码的问题诊断
我接手过一个真实的生产程序,功能是从自开发表ZMM_GOODS_MOVE里读取所有物料移动记录,然后按当月日期筛选出本月过账记录,再和其他明细表关联生成月结报表。起初这个程序需要8秒左右,数据量大概80万行。用户对8秒还算勉强接受,但每个月数据都在增长,等到120万行时页面超时,用户直接炸了。
查看原始代码,问题出在三个地方:
第一,数据读取用的是SELECT *,把全表所有字段全部取到内表,其中甚至包括几个巨大的备注字段。第二,日期筛选用的是LOOP和WHERE的常规写法,对120万行全表扫描。第三,筛选之后又做了一次SORT BY budat,但筛选前内表本来就已经按凭证号排序了,这次排序浪费了至少1秒。
5.2 优化步骤与耗时记录
我给的优化方案分三步走。
第一步,把SELECT *改成SELECT指定字段列表,只保留后续要用的字段。这一步对于内表筛选本身没有影响,但能减少内存拷贝,直接优化数据读取阶段。
第二步,把内表按日期加辅助字段排序,然后改用二分定位 + 范围遍历。这一步的核心是让筛选从全表扫描变成折半定位。
SORT lt_data BY budat bukrs. READ TABLE lt_data WITH KEY budat = lv_month_from BINARY SEARCH TRANSPORTING NO FIELDS. lv_start_idx = sy-tabix. LOOP AT lt_data FROM lv_start_idx INTO DATA(ls_data). IF ls_data-budat > lv_month_to. EXIT. ENDIF. APPEND ls_data TO lt_result. ENDLOOP.第三步,如果系统版本支持7.40+,我直接用FILTER + 二级排序键替换第二步里的手工LOOP。
SORT lt_data BY budat. lt_result = FILTER #( lt_data USING KEY srt_key WHERE budat >= lv_month_from AND budat <= lv_month_to ).最终结果:从120万行数据中筛选出本月约18万行记录,原始LOOP方案耗时约4.5秒,SORT+二分方案总耗时约2.1秒(包含排序1.4秒 + 读取0.7秒),FILTER方案约1.5秒。再配合前面的字段精简,整个报表从8秒降到了1.5秒。用户从抱怨变成惊讶,那个程序到现在还稳定地跑在月结批处理里。
5.3 边界条件的几个坑
日期筛选边界特别容易翻车,我说几个真实踩过的坑。
第一个是包含边界的处理。业务上“本月数据”通常包含当前月最后一天,所以在筛选条件里必须写成budat >= lv_from AND budat <= lv_to。因为日期是DATS类型,等于号成立的条件就是8位字符串一致,比较容易处理。但有些人习惯写成budat < lv_to + 1,这在ABAP里是合法的(日期+整数会按天数相加),可读性差,还可能被审查同事质疑。
第二个是空日期和初始值。如果内表里有些行的budat是初始值(00000000),它在升序排序中会排在最前面,二分定位时会落在起点之前。如果你的起始日期设置不正确,这些空日期行就会被错误选中或者错误排除,要看具体业务需求决定是先过滤掉空日期还是保留。
第三个是日期跨年和月末处理。你不能默认当前月份是12个月里的第任意一个,要用SY-DATUM算出当月第一天和最后一天,不能用硬编码的YYYY0101这种值。用字符串操作直接拼月末日期也不可靠,因为2月有28天和29天的差异。我建议直接调用函数RP_LAST_DAY_OF_MONTHS来获取月末,或者用下面这行可靠写法:
lv_last_day = lv_date + 30.不对,这样会跨到次月。正确做法是:
lv_last_day = lv_date. lv_last_day+6(2) = '01'. lv_last_day = lv_last_day + 1. lv_last_day = lv_last_day - 1.或者干脆用/运算符,CONV dats( ... )的方式处理太绕,实际项目里最常用的是RP_LAST_DAY_OF_MONTHS函数,接受日期参数,返回当月的最后一天,可靠且易读。我推荐这个。
5.4 情况分化的选型建议
我根据实际项目经验整理一个选型参考,适用数据量从几千到百万行不等:
| 数据量 | 推荐方案 | 理由 |
|---|---|---|
| 小于5000行 | LOOP + WHERE直接写 | 代码简单,性能差异可忽略 |
| 5000到10万行 | LOOP或FILTER均可 | 如果内表后续还要排序,优先FILTER |
| 10万到100万行 | SORT + 二分 或 FILTER + 副键 | 显著减少扫描成本 |
| 大于100万行 | FILTER + 二级排序键 + 字段精简 | 最大限度降低全表扫描和内存拷贝 |
| 多次重复筛选 | 一次性SORT建立二级排序键 | 排序成本可以摊薄到后续每次查询 |
| 精确单日等值查 | HASHED TABLE或排序键二分 | 单条读取O(1)或O(log n) |
6. 生产级代码里的细节打磨与性能陷阱
6.1 别把日期转换放在循环里
有些时候内表里存的是字符串日期(比如从外部接口传进来的YYYY.MM.DD格式),你需要在筛选前统一转成DATS格式。我见过有人直接在LOOP里调用CONVERT_DATE_TO_INTERNAL来做转换,每行一次函数调用,50万行就多出50万次函数调用,性能直接崩。
正确做法:筛选前先把内表复制一份,一次性把所有日期都转好,或者直接在SELECT时就转换。转换放循环外,筛选条件里做转换等于把循环的成本再乘一个系数,这个系数还相当大。
6.2 使用TRANSPORTING NO FIELDS加速定位
在二分查找时,如果只是想定位索引,不需要读取字段值,一定要加上TRANSPORTING NO FIELDS。它的作用是告诉ABAP运行时环境:不需要为工作区复制数据,只要返回索引位置即可。这样能减少一次字段复制和内存分配,在定位大内表记录时节省可观的毫秒级时间。这是我优化SORT+二分方案时最常用的一招。
6.3 使用二级排序键可以让表结构更灵活
如果你既想保留标准表插入效率,又想按日期快速筛选,可以给标准表显式声明一个SECONDARY KEY,专门为日期排序:
DATA: lt_data TYPE TABLE OF zgoodsmove WITH NON-UNIQUE SORTED KEY srt_budat COMPONENTS budat.这样即使主键不是日期,你也可以在LOOP/FILTER/READ中显式指定USING KEY srt_budat,让引擎走排序索引。添加副键的代价是插入和修改时要维护这个排序索引,但维护成本远低于每次全表扫描。我在月结程序里给大内表建了副键后,筛选效率提升非常明显。
6.4 大内表不要滥用DELETE ADJACENT DUPLICATES
在做日期筛选前如果还需要去重(比如只保留每个日期第一条或最后一条记录),很多人直接用DELETE ADJACENT DUPLICATES,但它的前提是内表按去重字段排序。如果你的主排序是日期,而去重字段是物料编码,那就必须重新排序,排序开销极大。这种情况下我建议要么在SELECT里用GROUP BY去重,要么按业务利用其他表的唯一索引提前去重。去重的时机应该早于日期筛选,因为去重后的记录数少了,后面的筛选成本也能下降。
6.5 请求追踪与代码评审中的硬指标
很多团队做性能评审时只关注数据库SQL的执行计划,忽略了内表循环的大头。我建议在代码评审中加入一个硬性指标:所有超过10万行内表的顺序循环,必须有明确的性能说明和可测量的基准数据。如果无法提供基准数据,就要先跑性能测量再提交评审。这个标准我在团队里推行了一年,确实让很多原本漏掉的性能问题在开发阶段就被拦住了。
7. 常见问题排查与避坑实录
7.1 为什么我用了BINARY SEARCH反而变慢了
常见原因有三个。第一种:内表没有SORT按查找键排序,二分查找在无序表上直接乱套,不仅结果错乱,性能还更差。第二种:SORT了但排序键和查找键不完全一致,比如你按budat BUKRS排序,却只用budat做二分查找,这只能部分生效,性能接近线性扫描。第三种:你的数据集太小,比如只有100行,二分查找的定位开销反而比顺序扫描还高。后续遇到这类问题,先确认排序状态,再看数据量级,最后检查TRANSPORTING FIELDS的冗余复制。
7.2 FILTER报错说没有合适键怎么办
FILTER表达式要求内表具有与筛选条件匹配的键。如果你用的是标准表,没有声明任何排序键,筛选条件里也没有按主键等值匹配,那么引擎只能退回全表扫描,有时候甚至直接编译错误。解决方法是给标准表添加NON-UNIQUE SORTED KEY,然后在FILTER里用USING KEY指定它。注意:副键的字段数量不能太多,太大的索引维护成本可能超过查询收益,一般只把日期和相关常用字段放进去即可。
7.3 日期筛选后数据少了,怎么排查
第一步检查边界:你的起始日期、结束日期是否包含当天,用lv_from <= budat <= lv_to对照。第二步检查排序:SORT + 二分时插入点计算是否有误,特别是起始日期不存在的情况。第三步检查空值:用两段式过滤逻辑,先把空日期行单独拎出来判断。第四步检查类型:两个比较字段是否都是DATS类型,还是一个是字符串一个是日期;ABAP在做不同字符类型比较时会有隐式转换,可能导致比较结果和预期不同。
我做过一个小工具,专门在调试时统计每天的行数分布,输出格式如下:
LOOP AT lt_total INTO ls_total. lv_date = ls_total-budat. ADD 1 TO lv_cnt. AT NEW budat. WRITE: / ls_total-budat, lv_cnt. ENDAT. ENDLOOP.这个调试输出一眼就能看到边界附近的行数分布,帮助你确认是筛选条件写错还是数据本身有问题。
7.4 数据量不大的情况下到底要不要优化
不要为了优化而优化。如果内表只有几百行,LOOP + WHERE写起来又清楚又好维护,完全没必要为了炫技改成FILTER + 副键。性能优化是有成本的:代码可读性、兼容性、是否引入额外索引维护开销,这些都要权衡。我在生产环境见过一个反面案例:有人为了省一次循环,写了三层嵌套的READ TABLE + BINARY SEARCH,性能没有提升多少,维护起来却非常痛苦,后来同事重构时花了两天时间才看懂。
7.5 大内表筛选后的内存释放
筛选之后,原始大内表如果不再使用,务必尽快释放。你可以在筛选后加上CLEAR lt_data或者FREE lt_data。这两个的区别在于:CLEAR重置内表但保留初始内存分配,FREE释放所有内存给系统。如果后续还要重新填充数据,用CLEAR更合适;如果彻底不再使用,用FREE。释放内存对月度批处理这种连续跑多个程序的环境尤其重要——前一个程序的临时大表不释放,后面程序的内存压力会越来越大,系统可能出现扩展存储短缺。
8. 一点实际体会
优化内表筛选性能这件事,给我最大的感受是:不要迷信任何一个单一方案,真正的关键在于理解数据量级、表类型和访问方式三者的匹配关系。你在5000行上用LOOP完全没问题,到了80万行还用它就是在给自己制造隐患。排序表和哈希表各有优劣,FILTER虽然顺手,也要看版本和环境。
我现在写新的报表程序,一开始就会先估一下数据量级,再决定用哪种方式做日期筛选。数据量超过十万行时,默认直接上SORT + 二分或FILTER + 副键。同时我会在代码注释里写上性能测试的基准数据,方便后续维护者理解为什么这么写,而不是等程序跑不动了再返工。
如果你正在优化一个因为内表日期筛选而卡顿的程序,我的建议是:先用GET TIME精确测出瓶颈在哪里,再对照这篇文章的方案做一两次对比,然后选一个最适合你系统和团队维护习惯的方式。踩过坑之后你就知道,ABAP的性能优化很少是什么高深算法,更多时候只是把该排序的排好序、该索引的建上索引、别在循环里做重复劳动,就够了。