作为一个常年泡在SAP项目里的ABAP开发,我见过太多同事在“性能优化”和“代码整洁”之间来回拉扯。业务顾问上线前拉着你说报表太慢了,程序一多就崩;代码评审时,架构师又拎着你的SELECT *和循环内查库不放。于是很多人养成了“先写能跑的,再想能不能跑得快”的习惯,结果重构时发现比重写还累。
这篇东西其实是从我自己的项目经历里整理出来的一套打法,核心就一句话:性能与整洁不是单选题,它们本来就是同一件事的两面。一个结构干净的ABAP程序,往往更容易做性能分析,也更容易写出高性能的实现;反过来,刻意追求“快”的代码,如果写得很乱,后面反而会因为无法扩展、难以定位问题而拖慢整体效率。文章适合正在做报表优化、接口开发、增强开发的ABAP开发,也适合刚从其他语言转到ABAP、对Mind the performance这个概念还不够熟悉的同学。内容不是空谈,是能直接抄回项目里用的实战。
1. 性能与整洁:一对被误读的冤家
开局先泼一盆冷水:我见过很多“性能优化”其实是在给代码埋雷。比如为了减少一次数据库查询,把一大堆业务规则塞进一个方法里;为了让某个报表快几秒,把正常的数据结构拆成全局变量满天飞。这种代码上线后确实快了几天,等需求一变,改起来从半天变成一周,这账怎么算都不划算。
1.1 “二选一”的困局是怎么形成的
ABAP这个语言有个特点——它离业务太近了。SAP项目里绝大多数ABAP代码都是被业务需求推着走的:销售凭证增强、采购审批、生产订单状态检查、接口数据落库,每个需求都有明确的时间点,没人愿意等你把代码打磨到“完美”。于是不少开发形成了一种肌肉记忆:能用SELECT *就用SELECT *,循环里取数也能接受,变量命名能看懂就行。这种代码几周后连自己都看不懂,更别提优化。
另外一个原因是性能分析的滞后。通常在性能测试阶段才暴露问题,这时已经离上线很近,你的第一反应肯定是“哪里慢改哪里”,而不是“重构出干净的程序”。这种救火式的改法,往往换来的是另一个新坑,让人误以为优化和整洁天生互斥。
1.2 重新定义“性能”和“整洁”
性能不只是“跑得快”。真正的性能包含三层:单次执行速度、并发下的稳定性、以及当业务规模变化时的伸缩能力。整洁也不等于“注释多”。我见过注释比代码还多、逻辑还是一团乱麻的程序。整洁的标准是:读代码的人可以在脑子里跑通整个流程,并且能在不破坏其他功能的前提下做修改。
把这两个标准放在一起看,你会发现它们的交集非常大。一个方法只干一件事,你就能更快定位慢在哪个方法里;一个查询只取必要的字段,数据库侧的索引才能正常工作,这是性能和整洁共同的要求;一段循环内没有副作用,你才敢放心缓存数据。
1.3 整洁的代码降低性能劣化风险
这里有个很容易被忽视的点:业务是不断变化的。今天你的报表只有一万条数据,跑300毫秒,没人关心性能;明年数据量变成一千万,同样逻辑可能跑三分钟。这时候如果你的代码是整洁的,拆成清晰的模块,性能劣化的位置通常一眼就能锁定——某个SQL缺索引、某个循环内消耗高。反观那种一团糟的程序,你得先花一天搞清楚它在干什么,才谈得上优化。
所以,把整洁当成性能的“预防性维护”,一点都不夸张。
2. SQL查询层面:最直观的性能与整洁交汇点
ABAP应用慢,八成都慢在数据库访问上。而SQL写得好不好,恰好也是判断代码整洁与否的试金石。我在项目里做代码评审,先看程序里的SELECT长什么样,基本就能判断这个人的开发习惯。
2.1 不要在循环里反复查库
循环内查表是最常见、最容易被点评的性能反模式。比如遍历一个内表,在循环里用SELECT SINGLE取客户名称、物料描述,数据量一上来就是灾难。
LOOP AT lt_items INTO ls_item. SELECT SINGLE maktx FROM makt INTO ls_item-maktx WHERE matnr = ls_item-matnr AND spras = sy-langu. MODIFY lt_items FROM ls_item. ENDLOOP.这段代码读起来很直观,但每一次循环都是一次数据库往返。如果lt_items有5000行,就是5000次查询,本地网络好一点还能忍,放到生产环境基本要被骂。整洁的写法是先把这批数据准备好,一次性取回:
IF lt_items[] IS NOT INITIAL. SELECT matnr, maktx FROM makt INTO TABLE lt_makt FOR ALL ENTRIES IN lt_items WHERE matnr = lt_items-matnr AND spras = sy-langu. ENDIF. SORT lt_makt BY matnr. LOOP AT lt_items ASSIGNING FIELD-SYMBOL(<fs_item>). READ TABLE lt_makt INTO ls_makt WITH KEY matnr = <fs_item>-matnr BINARY SEARCH. IF sy-subrc = 0. <fs_item>-maktx = ls_makt-maktx. ENDIF. ENDLOOP.一次查询加一次SORT加一次二分查找,熟悉这套模式后,写起来也不比循环内SELECT麻烦多少。关键是,这种写法让数据库访问集中、清楚,读代码的人一眼看出“这里就查了一次库”,这就是整洁的力量。
2.2 字段列表和索引:从SELECT *到精确取列的性能跃迁
我在项目中几乎每次都强调:不要用SELECT *。很多人觉得ABAP语句写长了显得啰嗦,直接SELECT *省事。但省掉的并不是复杂度,而是把必要的约束从代码里抹掉了。
SELECT *带来几个问题:一是网络传输数据量大,二是ABAP层要分配大量不必要的内存,三是大概率无法命中最优索引。而明确列出字段列表,一方面让数据库优化器更容易选择覆盖索引,另一方面也逼着开发者思考“我到底需要哪些字段”,这本身就是个整理思路的过程。
配合索引时,字段列表更加重要。比如你经常用销售凭证号查一批数据,那么在凭证号字段上建索引,再写:
IF lt_header[] IS NOT INITIAL. SELECT vbeln, posnr, matnr, kwmeng FROM vbap INTO TABLE lt_vbap FOR ALL ENTRIES IN lt_header WHERE vbeln = lt_header-vbeln. ENDIF.相比SELECT *,这种写法既减少了传输量,也能让数据库在索引上扫描更少的页。关于索引,建议开发在优化阶段主动用SE14或SE11查看表上现有索引,而不是等DBA来提醒。创建索引是件需要权衡的事,但基本共识是:查询频率高且查询字段区分度高的列,值得建。
2.3 FOR ALL ENTRIES 和 JOIN:不是非此即彼
关于取数方式,一直有争议。老派的ABAP开发喜欢FOR ALL ENTRIES(FAE),因为它在某些版本和数据库上效率稳定,而且对数据量不敏感。而新一些的项目里,大家更愿意直接写JOIN,因为JOIN逻辑上更整洁:表之间的关系写在SQL里,而不是靠内存里的临时集合去匹配。
我的经验是,两者都有适用场景,别迷信任何一个。FAE的本质是把你临时表里的值展开成一个IN条件,但它有个坑:临时表为空时会全表扫描,所以必须先判断非空。
IF lt_items[] IS NOT INITIAL. SELECT vbeln, posnr FROM vbap FOR ALL ENTRIES IN lt_items WHERE vbeln = lt_items-vbeln. ENDIF.JOIN的好处是SQL清晰,但也要小心笛卡尔积和连接结果过大。多个内连接改成显式JOIN可以提升可读性,不过ABAP里的OPEN SQL对JOIN的支持比数据库原生SQL弱一些。在复杂的业务场景里,我通常建议:超过三个表的关联不要硬写成一个JOIN,拆分两步查询会更好维护。
2.4 别小看配置表:SM30带出描述的性能陷阱
SAP项目里到处都是配置表,比如状态、类型、文本描述。这些表数据量不大,但被访问的频率极高。我见过一个销售凭证增强,循环里对每行调用函数读取状态文本,结果状态多了,报表直接从2秒拖到20秒。
SM30配置表经常只需要“带出描述”,最简单的是调用功能模块,或者用配置视图直接查。但在批量场景里,最高效的做法还是把所有描述一次性缓存到内部表。
TYPES: BEGIN OF ty_status, stsma TYPE j_status-stsma, txt04 TYPE j_status-txt04, txt30 TYPE j_status-txt30, END OF ty_status. DATA lt_status_cache TYPE SORTED TABLE OF ty_status WITH UNIQUE KEY stsma txt04. SELECT stsma, txt04, txt30 FROM j_status INTO TABLE lt_status WHERE spras = sy-langu. INSERT LINES OF lt_status INTO TABLE lt_status_cache.之后每次取描述就用READ TABLE,不会再碰数据库。这种“预加载+缓存”的模式,在ABAP里非常通用,而且代码写出来很整洁:数据从哪来、缓存在哪、怎么用,看代码就能懂。
3. 内部表操作的性能与整洁
数据库访问优化好之后,真正的战场就转到内存里的内部表处理。很多ABAP程序慢,不是数据库的问题,而是内表选型、排序、查找方式出了岔子。
3.1 内部表类型选型:STANDARD、SORTED还是HASHED?
刚开始学ABAP的时候,大家最常用的是STANDARD TABLE,因为创建起来不用动脑。但到了性能敏感的场景,类型选不对,代码再整洁也没用。
| 内表类型 | 适用场景 | 性能特征 | 整洁度影响 |
|---|---|---|---|
| STANDARD | 顺序处理、频繁修改、按索引访问 | 二分查找需要先排序 | 写法最自由,但需要自己维护排序 |
| SORTED | 按关键字查找、数据量中等 | 自动排序,READ TABLE默认二分 | 声明后自动保证有序,代码更简单 |
| HASHED | 大数据量、按唯一键精确查找 | 哈希索引,按关键字查找极快 | 只能按完整关键字访问,语义清晰但受限 |
一个常见的误区是:数据量大就用HASHED。其实HASHED的查找速度虽然快,但无法按非主键排序遍历,也无法部分匹配。如果后续需要顺序处理,还得转成STANDARD,反而多余。SORTED TABLE很多时候是“均衡之选”:有序可用于批量顺序访问,也可默认二分查找。
我个人的建议:如果表创建后主要按一个唯一键精确查找,使用HASHED;如果既要按关键字找,又要做顺序处理,使用SORTED;如果只是中间结果或者需要频繁修改,用STANDARD,然后SORT + READ TABLE ... BINARY SEARCH。
3.2 二分查找不是自动发生的
在STANDARD TABLE上使用READ TABLE ... WITH KEY,默认是线性查找,数据量上千后性能就会肉眼可见地变差。很多人忘了先SORT再BINARY SEARCH。
SORT lt_data BY matnr. READ TABLE lt_data INTO ls_data WITH KEY matnr = lv_matnr BINARY SEARCH.这个细节说起来简单,却是排查性能问题时最容易忽略的一个点。尤其数据来自多次APPEND,顺序是乱的,直接READ TABLE线性扫,扫到几万行时就会卡顿。
为了让这套逻辑保持整洁,我通常把“排序+查找”封装成一个局部方法:
METHODS get_by_matnr IMPORTING !iv_matnr TYPE matnr RETURNING VALUE(rs_data) TYPE ty_data. METHOD get_by_matnr. SORT me->mt_data BY matnr. READ TABLE me->mt_data INTO rs_data WITH KEY matnr = iv_matnr BINARY SEARCH. ENDMETHOD.所有对内部表的访问都走这个方法,调用方代码简洁,且稳定高效。
3.3 避免不必要的复制和转换
ABAP对大数据量的复制开销非常敏感。比如用=把内表整体复制一遍,如果后续发现只是读取,就不如在变量声明时直接引用或使用FIELD-SYMBOL。
LOOP AT lt_result ASSIGNING FIELD-SYMBOL(<fs_result>). " 修改 <fs_result> 直接作用于内表,不需要 MODIFY ENDLOOP.使用字段符号可以减少一次数据拷贝,代码上也少写MODIFY,看起来清爽。在循环内调用方法传内表时,如果方法不需要修改,用IMPORTING传引用比COPY更高效。循环里也尽量少做类型转换,尤其是字符串拼接和数值格式化,能提到循环外就提到循环外。
3.4 用运行计时量化性能改善
谈到性能优化,数据比感觉靠谱。ABAP里获取毫秒级的运行时间,通常用GET RUN TIME:
DATA: lv_start TYPE i, lv_end TYPE i. GET RUN TIME FIELD lv_start. " 要测量的业务逻辑... GET RUN TIME FIELD lv_end. DATA(lv_elapsed_ms) = ( lv_end - lv_start ) / 1000. WRITE: / '执行耗时(毫秒):', lv_elapsed_ms.GET RUN TIME返回的是运行计数,精度足以覆盖毫秒级比较。在优化前后各打一个点,轻轻松松就能量化优化效果。
我习惯每次做优化时,在修改前后各跑一次,记录耗时,这样才有说服力。不要只凭感觉说“快了很多”。拿报表真实数据量做对比,比如循环内查库改为批量查库后,从43秒降到2.1秒,这种数据一摆出来,谁都能理解改动价值。
4. 接口传输与集成场景:性能与整洁如何握手
SAP项目中接口开发占了很大比重,实际操作中性能问题也最容易在这里爆发。文件传输、HTTP接口、异步队列、权限校验,一个不慎就会造成大量数据堆积或超时。而这个场景恰恰需要代码整体设计整洁,才能让性能优化不失控。
4.1 大文件不要一次性读入内存
ABAP接口传文件是项目里特别常见的需求。比如定时从FTP读取供应商主数据,或者向银行发送付款文件。很多新手会习惯性用OPEN DATASET + READ TABLE把整个文件读进内存,文件一大就内存暴涨,甚至程序dump。
以文本文件为例,正确做法是逐行读取:
OPEN DATASET lv_filepath FOR INPUT IN TEXT MODE ENCODING DEFAULT. IF sy-subrc = 0. DO. READ DATASET lv_filepath INTO lv_line. IF sy-subrc <> 0. EXIT. ENDIF. " 处理当前行 ENDDO. CLOSE DATASET lv_filepath. ENDIF.这种逐行处理方式,把内存占用压到最低,而且代码结构也非常清晰:打开文件、循环处理、关闭文件一目了然。配合批量提交,比如每100行或者每1000行调用一次BAPI写入,既避免了单次写入事务过大,也让处理进度可控。
传大文件时另外要注意:文件传输本身往往比ABAP处理更耗时,最好用异步Job定时拉取,而不是在同步对话中做。把“读文件”和“写数据库”拆成两个独立的步骤,也便于失败后重跑。
4.2 异步处理与队列:别让同步事务背锅
很多接口设计成同步,是因为业务要求即时返回结果。但如果是大批量数据,同步处理会让用户端干等,还容易超时。这时候可以考虑异步队列:用户请求先进队列,后台Job批量消化。
ABAP中可以用SMQR等队列机制,也可以自己写后台报表调度。无论走哪种,一个很重要的整洁原则是:队列处理程序的状态和日志必须完整。否则一旦某个批次失败,连开发都查不出问题在哪。
我通常在接口处理表里加一个状态字段(如PROC_STATUS:0待处理、1成功、2失败)和重试次数。每处理一条更新一次,这样出问题时可以直接查表。日志完整,排查方便,这本身就是对性能的一种保障——定位慢、定位失败的成本,往往比查一次程序慢还要高。
4.3 权限校验和业务校验尽量前置
接口开发中,权限校验是个典型的“又慢又乱”的点。有人写一个方法,在处理过程中反复做权限检查,结果每个循环都调一次AUTHORITY-CHECK。
更好的做法是:在程序入口处做一次权限校验,整体校验失败就不继续处理。权限检查一般都很快,但不能放在循环里放大100倍。同理,业务校验(比如订单状态、物料合法性)也应该尽量批量查询出来,在循环里只做内存判断,不要循环里调BAPI或查询业务表。
这里可以结合ABAP请求提交中常见的授权校验来思考,虽然场景各有不同,但原则一样:能把校验数据一次捞出来,就别循环访问。
5. 增强开发中的性能与整洁边界
SAP项目基本绕不开增强。销售凭证VA03、采购订单ME23N、生产订单CO03,这些界面上到处是增强点。增强开发里性能问题特别隐蔽,因为用户感知到的往往是“界面卡一下”,而不是报表慢。而且增强代码分散在标准程序各处,一旦不规范,对后续维护就是噩梦。
5.1 找对增强点:性能与维护的最优解
以VA03销售凭证增强为例,如果业务需要显示额外的字段或按钮,可能在PBO/PAI、USEREXIT、BADI等不同入口都可以实现。选哪个增强点,直接决定了代码性能和后续可维护性。
我的建议是:能用BADI或显式增强点,就别用隐式增强;能在一次遍历中完成的事情,就别在界面的多个位置各写一段。增强点选得对,后续不会出现“这个字段在显示时改了,打印时却还是旧值”这种低级问题。而选错增强点,往往要靠多次调用和重复取值来补救,性能自然好不到哪去。
5.2 别在增强里画蛇添足
另一个常见问题是,增强代码里放了太多无关逻辑。特别是用户看到标准界面上有某个字段,就想顺手把其他相关的值也带出来,结果一个简单增强越写越长,每次显示都执行一大段SQL。
我见过一个VA03增强,只是为了显示一行备注,结果代码里又查了物料表、客户表、定价表,前后加起来四五次数据库访问。优化起来其实很简单:把所有的字段查询合并成一次SELECT,或者用缓存。但在增强点里,更重要的是克制:只做增强需求要求的事情,把复杂度挡在外面。
5.3 低调而整洁:隐式增强的纪律
隐式增强点(Implicit Enhancement)有时是不得不用的。用它的好处是侵入小,不需要修改标准代码,升级时不会冲突。但坏处是它看不见摸不着,维护的人常常不知道这里有逻辑。
所以用隐式增强时必须有纪律:第一,注释写清楚这个增强解决什么问题、为什么放在这里;第二,代码只增强,不改造标准逻辑;第三,定期排查有没有废弃增强。我见过一个系统里好几个隐式增强互相覆盖,每次查问题都要把所有增强点翻一遍。后来团队定了一个规矩:每个隐式增强必须写注释标明责任人、需求单号和修改日期,否则不允许提交。这其实就是把“整洁”制度化,而干净之后,性能定位也快了很多。
6. 性能分析实操:让工具替你说话
讲了这么多原则,落地时离不开工具。很多ABAP开发者一遇到性能问题就直接翻代码硬看,效率很低。正确的顺序应该是先用工具定位瓶颈,再有针对性地看相关代码。
6.1 SE30 / SAT:ABAP运行时分析的入门
SE30(新版本SAT)是最基础也最常用的ABAP性能分析事务码。跑完一个程序后,它会列出各个语句的耗时占比,比如某个SELECT占了总执行时间的60%,那么优化目标就有了。
整洁实践的思路是:在分析之前,先保证被测数据量和真实用户场景一致。拿一个只有几百条数据的测试环境去分析,结论基本没有参考价值。记录基线数据,优化后再跑一次,对比差异,这是给开发团队演示“性能提升”的最有说服力的材料。
6.2 ST05:SQL追踪的精准定位
ST05可以追踪数据库访问,看到每条SQL是怎么执行的、访问了多少行、耗时多少。它特别适合查循环内查询和缺失索引的问题。
使用流程也不复杂:
- 在ST05激活追踪,勾选SQL Trace;
- 运行或操作目标程序;
- 停止追踪,查看追踪结果。
在追踪结果里,重点看每个SQL语句的执行次数和总耗时。如果同一句SQL出现了几千次,那就可以判断是循环内查询了。修改为批量查询后,再追踪一次,执行次数会大幅下降。
6.3 代码评审中的性能嗅觉
最后想说,性能优化不只是跑工具的事。日常代码评审里,看到下面这些信号就要警觉:
- 循环体内出现SELECT、CALL FUNCTION、外部调用;
- 大数据量内部表用标准线性查找;
- 使用
SELECT *且未过滤关键条件; - 没有判断空表就使用FOR ALL ENTRIES;
- 全局变量满天飞,方法之间隐式传值。
这些信号往往是性能和整洁同时出问题的征兆。把它们列成评审检查清单,比事后救火有效得多。
7. 常见性能问题排查实录
写到这里,分享几个我实际踩过的坑。这些问题在项目里反复出现,基本可以当成速查表来用。
7.1 问题一:报表越跑越慢
现象:某张库存报表,早期秒开,数据量增长后变成十几秒,用户频繁投诉。
初步排查:SE30显示主要耗时在某个内部表的READ TABLE上。进一步看代码,原来是STANDARD TABLE直接READ TABLE WITH KEY,没有排序也没有二分查找。
解决:把表声明改成SORTED TABLE,或先SORT再READ TABLE ... BINARY SEARCH。耗时从12.3秒下降到2.4秒。
心得:很多“报表越跑越慢”不是数据库问题,而是内存内表查找方式没有随数据量增长同步优化。内表类型一开始就要想清楚,别等到卡了才想到查。
7.2 问题二:接口处理数据大量堆积
现象:每日付款接口处理数万条记录,原本设计同步调用,用户反馈经常超时;队列里堆积大量消息,处理速度跟不上。
解决:把同步逻辑改造成异步队列,批量读取文件、批量写入数据库;同时增加重试机制和日志表。
心得:接口性能优化不能只盯着单次SQL或单次函数调用,要看整体处理链路是否匹配数据规模。同步调用天生不适合大批量,设计接口时就要定好“大文件分段、异步、断点续传”这些基调。
7.3 问题三:增强代码导致界面卡顿
现象:VA03显示销售凭证时明显卡顿。
排查:发现一个隐式增强在PBO中循环内多次调用BAPI和查询函数,界面每次刷新都触发。
解决:把这个增强移到用户操作时才触发,同时把循环内的大量取数改为批量读取。
心得:增强开发尤其要克制。界面每次刷新都执行的代码,必须保证极低的耗时;实在无法保证,就要想办法推迟执行或做缓存。
7.4 性能问题排查速查表
| 症状 | 典型原因 | 快速定位 | 解决思路 |
|---|---|---|---|
| 报表响应慢 | 循环内查库/READ TABLE线性查找 | SE30看语句耗时 | 批量查询+缓存+二分查找 |
| 接口堆积 | 同步处理大数据量 | 队列日志/后台Job日志 | 异步化、分批处理、重试机制 |
| 界面卡顿 | 增强代码PBO频繁执行 | ST05追踪调用次数 | 延迟加载、缓存、减少数据库往返 |
| 内存溢出 | 大文件一次性读入内表 | ABAP dump类型 | 逐行处理、分块提交 |
8. 写在后面的话
我在项目里反复讲一个观点:性能优化不是写到一半才发现的事,而是从设计代码那天起就该有的习惯。而保持整洁,正是让这个习惯持续下去的最有效方式。一个程序员可以把代码写得看起来“很猛”,各种花哨语法,但也正因为猛,后面没有人敢动。真正能长期维护、还能保持高性能的ABAP代码,往往读起来很朴素。
如果你正在接手一个别人留下的“又快又乱”的老程序,别急着全盘重写。先跑SE30/ST05定位真正的瓶颈,把循环内查询、线性查找这类问题逐个修掉,守住“改动最小、收益最大”的原则。等你手头的代码结构理顺了、运行时性能也稳了,自然就会认同这句话:性能与整洁从不矛盾,它俩本来就是同一个目标的两张面孔。
最后再分享一个习惯:每次写完一段性能优化代码,我都会在注释里记一下优化前后的耗时和当时的业务数据量。这看起来是小事,但等三个月后别人问起“这段逻辑为什么这样写”,你会庆幸当时的记录留下了答案。ABAP开发这行,最贵的永远不是CPU时间,是人的注意力和排查成本。把这两样省下来,性能自然就到位了。