
做了十多年ABAP开发我一直听人说“代码要写整洁但性能优化难免要牺牲可读性”。这话我一度也信直到有一次被一个报表恶心到代码工工整整每个Function拆得清清楚楚结果跑一个多小时不出数。我一条条看下去发现问题恰恰出在“太整洁”了——为了复用一个通用取数函数循环里嵌了上百次SELECT。那一刻我才彻底明白所谓“Mind the performance”根本不是让你在性能面前放弃整洁而是要求你在写每一行代码时心里都有一本账这行代码在数据库层、内表层、循环层分别要付出多少代价。这篇东西我打算从实际踩坑出发把ABAP开发里“性能”和“整洁”真正统一起来的思路捋一遍覆盖数据读取、排序查找、增强实现、权限检查、缓存设计这些日常逃不掉的场景。适合正在做SAP实施或运维的ABAP开发者尤其适合那些已经能写“能跑”的代码、但总被性能问题追着跑的朋友。1. 性能与整洁从来不是一道选择题1.1 麻烦往往来自“为整洁而整洁”ABAP圈子里有一种典型的“整洁主义”把所有数据库访问都封装进一个通用方法业务逻辑里只管调用。听起来很美好但代价是——你永远不知道这个方法底层做了什么。我见过有人把SELECT *封装成get_data_by_matnr业务侧看起来只有一行调用数据库侧却把整张MARA表搬进内存。这种代码上线后谁都不敢动因为一动就崩最后只能靠加硬件硬扛。这种“整洁”其实是假整洁它只是把脏东西藏起来了。真正的整洁不是少写几行代码而是让代码的每一层意图都清晰可辨性能特征一眼能看出来。Mind the performance这个说法我特别喜欢它原本是地铁里的提示音放在ABAP开发里就是每次你准备访问数据、写循环、调函数之前先停一秒想想这一步会不会成为性能瓶颈。1.2 整洁代码让性能问题无处可藏我自己的经验是性能出问题的代码九成都是结构混乱的代码。反之如果代码结构清晰、数据流明确性能问题定位起来也快。举个例子两个报表做同样的事一个把读取逻辑、业务逻辑、输出逻辑混在一起另一个分层清楚。前者的性能问题你只能靠猜后者的性能问题你用SAT运行时分析器一跑哪一层慢一目了然。所以“整洁”和“性能”不是对立的整洁是性能优化的前提。代码乱成一团你连瓶颈在哪儿都找不到谈何优化反过来一个性能极差的整洁代码至少你还有机会优化它——而一个性能极差的混乱代码通常只能推倒重来。1.3 “Mind the performance”到底在提醒什么我在团队里带新人时会让他们遵守三条铁律任何一次数据库访问必须知道会返回多少条数据任何一层循环必须清楚循环次数和循环体内的单次代价任何一次内表操作必须了解数据是否已排序、是否值得二分查找。这三条看起来是性能要求但真正执行起来你会发现它同时是在逼你把代码写得结构清晰。因为如果你连数据量都不知道说明你对业务模型的理解就是模糊的如果你连循环嵌套次数都说不清说明你的代码组织一定有问题。“Mind the performance”这条地铁提示音放在代码里就是时时留意处处思量。2. 数据读取一行代码里的性能与整洁取舍2.1 SELECT * 是最大的整洁杀手几乎所有ABAP性能指南都会说不要用SELECT *但很多人只是机械地记着这条规则没想过它为什么错。SELECT *的问题不只是多传了几个字段——它会让数据库优化器放弃索引覆盖扫描把整行数据的所有字段都捞回来再通过网络传到应用服务器最后塞进内表。如果你只需要物料号、物料描述、基本单位这三个字段却把MARA的近百个字段全部搬一遍这张表有多少行你就白搬了多少份垃圾数据。在我经手的一个库存报表项目里原开发用了SELECT *读MARC表数据量大概八十万行程序跑了二十分钟。改成只取需要的十一个字段后时间直接压到三分钟以内。这是最典型的“整洁与性能统一”的案例字段列表清清楚楚写在代码里读者一眼就知道这段逻辑依赖哪些数据同时也让数据库省掉了80%以上的IO。列裁剪的正确姿势是SELECT matnr, werks, lgort, labst FROM marc INTO TABLE it_marc WHERE matnr IN s_matnr AND werks IN s_werks.这样写可读性和性能同时拿满分。如果有人告诉你“字段多写几个没关系反正数据库都快”你可以礼貌地请他离开项目组。2.2 排序和二分查找让内表操作快十倍ABAP内表操作里SORT和READ TABLE ... BINARY SEARCH是一对黄金搭档但现实中很多人把它们用成了“银样镴枪头”。最常见的问题是排序字段和查找字段不一致。比如你按matnr排序却用werks做二分查找结果就是死循环——二分查找找不到数据反而比顺序查找更慢。正确做法是让排序键和查找键严格对齐SORT it_marc BY matnr werks. READ TABLE it_marc INTO ls_marc WITH KEY matnr lv_matnr werks lv_werks BINARY SEARCH.这里想多说一句SORT本身。ABAP的SORT默认是稳定排序如果你对内表按多个字段排序要注意排序顺序的优先级——写在前面的字段是主排序键。我见过有人为了排序稳定性自己写冒泡排序这是典型的“为了整洁而整洁”反面教材SORT是C语言级别实现的排序性能比自己写循环高几个数量级直接用就好。还有一个进阶技巧如果内表数据量大且需要反复查找可以先用SORT建立有序内表再用READ TABLE ... BINARY SEARCH。如果查找次数少顺序查找就够了——不要为了二分而二分一次二分查找的预处理排序成本可能比十次顺序查找还高。这就是“Mind the performance”的另一种体现不是所有优化都有价值优化要看场景。2.3 SM30带出描述的常见做法与隐患SM30是ABAP开发者的老朋友表维护生成器生成的配置维护界面里最常被问到的就是“怎么带出描述”。比如你在ZMM001里维护了物料组希望操作人员在维护界面能看到物料组描述而不是只看一个编号。这背后其实涉及文本表关联。标准做法是在表维护生成器里维护外键关系和搜索帮助让配置界面自动带出描述。但很多项目图省事直接在Table Maintenance Generator里加了个自定义字段然后在PBO里循环读文本表带出描述。这个做法在小数据量时没问题一旦配置表超过几千行界面打开能卡十秒。更合理的做法是设计时就建好文本表关联SELECT a~matgr, b~bezug FROM zmm001 AS a LEFT JOIN zmm001t AS b ON b~matgr a~matgr AND b~spras sy-langu INTO TABLE it_config.让数据库在源头把描述带好而不是靠程序逐行补描述。这看起来只是把“循环里SELECT SINGLE”挪到了SQL里但性能差异是数量级的——前者是N次数据库往返后者是一次数据库往返。我记得之前有个项目配置表三千行原程序在循环里给每行读一次描述打开一次配置界面要八九秒。改成LEFT JOIN一次读端界面秒开。代码也从二十多行缩到几行这才是真正的整洁。2.4 数值检查用CO/CN替代绕弯子的写法“ABAP 检查是否为数值类型”这个热搜词我在很多群里看到过。常规做法是调用IS_NUMBER函数或者用正则表达式但如果你在意性能最地道的写法其实是COContains Only运算符。IF lv_string CO 0123456789. 是纯数字 ELSE. 不是纯数字 ENDIF.这个写法的好处是CO是ABAP语言级别的运算符直接对字符逐位比较不会有函数调用开销也不会有正则引擎的启动成本。在多层循环里做校验时这个性能差异会被放大。我测过对一个十万字符的字符串做CO检查比调用CL_ABAP_MATCHER快了两个数量级。当然CO检查的是“只包含指定字符集”处理负数和小数时需要额外允许负号和小数点IF lv_string CO 0123456789.-. 数字或负数或小数暂不管格式合法性 ENDIF.如果还想更严格可以用CSContains String和CO组合。但核心思路不变用最朴素的运算符表达最清晰的意图拿到最快的性能。这也是我认为“性能与整洁不矛盾”的最佳注脚——一个CO写在代码里任何一个ABAP开发都看得懂同时它比一堆复杂封装快得多。3. VA03销售凭证增强一次完整的实战推演3.1 需求与增强点选择“abap中va03销售凭证增强”这个热搜词说明现在做SD增强的人不少。VA03是销售订单显示事务最常见的增强需求是在抬头或行项目界面展示额外信息——比如物料的可用性状态、客户信用额度使用情况、或者自定义的字段描述。这类增强做起来不难难的是做完了别把整个事务拖慢。先说增强点选择。VA03的显示逻辑里抬头屏幕增强一般用MV50AF01的用户出口或者BADI_SD_SALES行项目屏幕增强可以用MV50AF01的USEREXIT_MOVE_FIELD_TO_VBAK这类传统出口或者用增强点Enhancement Spot在LIP循环处挂逻辑。我的建议是优先用官方推荐的BAdI或增强点少碰隐式增强——因为隐式增强在升级时会变成噩梦那不是整洁是给后人埋雷。选好增强点后最容易犯的错误是在显示循环的每行里做数据库查询。VA03的显示循环本身就是个性能敏感区一行销售订单有几十个项目如果每个项目都额外查询一次数据库界面响应时间立刻劣化。3.2 字段带出复用现成函数而不是重写在VA03里带出物料描述很多新手会直接SELECT maktx FROM makt WHERE matnr ...。且不说这样在循环里查询有多慢单是“重复造轮子”这一点就够糟了。实际上从SAP NetWeaver 7.40开始ABAP提供了很多开箱即用的读取帮助。比如要带物料描述可以用MB_MATERIAL_TEXT_READ或者直接用MARA、MAKT的联合查询。但更好的思路是出口代码里往往已经拿到了LIKP、VBRK这些主表数据你能从这些内表里直接READ TABLE出来的字段就不要再去数据库读。我见过一个增强需要显示物料的“利润中心”。原做法是循环里查MARC表每条查一次。改法很简单先在循环外面一次性把所有物料号收集起来用FOR ALL ENTRIES查回利润中心再在循环里READ TABLE ... BINARY SEARCH。这里FOR ALL ENTRIES会生成一条SQL内联子查询数据库只访问一次。这就是“Mind the performance”——把N次查询变成一次查询同时代码意图更聚焦。3.3 循环外的预处理消灭N1查询上面说的“循环外预处理”我再展开细讲一下因为这是ABAP性能优化里最核心、最常被忽略的思维。先看反面典型LOOP AT it_vbap INTO ls_vbap. SELECT SINGLE maktx FROM makt INTO lv_maktx WHERE matnr ls_vbap-matnr AND spras sy-langu. ls_vbap-maktx lv_maktx. MODIFY it_vbap FROM ls_vbap. ENDLOOP.这段代码逻辑一点问题没有但它对数据库发起了it_vbap行数次SELECT。假设VA03屏幕上一百个行项目就是一百次数据库往返。如果加上可用性检查、库存地点描述、客户描述……每个字段来一遍几百次往返下来用户等得都能喝杯咖啡了。改成循环外预处理DATA: lt_matnr TYPE STANDARD TABLE OF matnr. LOOP AT it_vbap INTO ls_vbap. COLLECT ls_vbap-matnr INTO lt_matnr. ENDLOOP. SORT lt_matnr. DELETE ADJACENT DUPLICATES FROM lt_matnr. IF lt_matnr IS NOT INITIAL. SELECT matnr, maktx FROM makt INTO TABLE DATA(lt_makt) WHERE matnr IN lt_matnr AND spras sy-langu. ENDIF. SORT lt_makt BY matnr. LOOP AT it_vbap INTO ls_vbap. READ TABLE lt_makt INTO DATA(ls_makt) WITH KEY matnr ls_vbap-matnr BINARY SEARCH. IF sy-subrc 0. ls_vbap-maktx ls_makt-maktx. ENDIF. MODIFY it_vbap FROM ls_vbap. ENDLOOP.初看代码长了但每一行的意图都直白而且性能是几何级的改善。FOR ALL ENTRIES这里用IN lt_matnr其实是Open SQL的新式写法在大型内表时要注意去重和空表判断否则会生成无用IN条件甚至导致SQL语句超长。这些小坑如果你写过几年ABAP应该都踩过。我还遇到过一个更隐蔽的N1用了SELECT SINGLE但其实可以用SELECT循环。某些老版本SAP里SELECT SINGLE没有UP TO 1 ROWS快因为SINGLE要产生额外的结果集判定。后来版本优化了这个问题但我依然建议——如果只取一行用SELECT SINGLE ... UP TO 1 ROWS或者干脆SELECT ... UP TO 1 ROWS代码意图更精确。4. 请求提交与权限检查细节里的性能分水岭4.1 权限检查为什么不能在循环里做“abap请求提交授权f_001”这个热搜词让我想到很多保存增强里常见的通病权限检查放在循环里。很多ABAP开发写保存逻辑时会对每一行数据调一次AUTHORITY-CHECK OBJECT F_001这种做法如果数据量大了性能会很差。更麻烦的是如果某一行权限不足你得决定是整单拒绝还是跳过逻辑越写越乱。权限检查的性能核心是一次AUTHORITY-CHECK就是一次权限对象实例化它需要加载权限对象定义和当前用户权限数据。如果循环里调用几百次就要重复加载几百次。正确做法是把需要的权限值先收集起来去重后一次性检查。比如保存发票时有多个公司代码你先收集所有公司代码然后检查用户对每个公司代码的权限LOOP AT lt_company INTO lv_bukrs. AUTHORITY-CHECK OBJECT F_001 ID BUKRS FIELD lv_bukrs ID ACTVT FIELD 01. IF sy-subrc 0. lv_not_authorized abap_true. EXIT. ENDIF. ENDLOOP.这里循环次数已经收敛到了“公司代码数量”而不是“行项目数量”。而且权限检查本身就是逻辑的一部分——你要先知道用户能不能操作这家公司才决定要不要处理它的数据。顺序排对了代码读起来也更顺。更进阶的写法是构建一个“权限内公司代码表”在循环里直接查表METHODS check_auth_bukrs IMPORTING iv_bukrs TYPE bukrs RETURNING VALUE(rv_ok) TYPE abap_bool. 内部缓存上次检查结果 CLASS-DATA: gt_auth_cache TYPE SORTED TABLE OF bukrs WITH UNIQUE KEY table_line. CHECK iv_bukrs IS NOT INITIAL. READ TABLE gt_auth_cache WITH KEY table_line iv_bukrs TRANSPORTING NO FIELDS. IF sy-subrc 0. rv_ok abap_true. RETURN. ENDIF. AUTHORITY-CHECK OBJECT F_001 ID BUKRS FIELD iv_bukrs ID ACTVT FIELD 01. IF sy-subrc 0. INSERT iv_bukrs INTO TABLE gt_auth_cache. rv_ok abap_true. ENDIF.这个写法兼顾了效率和整洁——用一个静态缓存记录已检查通过的公司代码。当然如果权限在会话期间可能变化比如SU53调试后缓存要慎用。但生产系统里权限数据在用户登录后基本固定这种缓存非常安全。4.2 请求提交的授权与COMMIT次数控制“请求提交”在SAP里一般指保存数据并提交工作区。这里有两个容易被忽略的性能点一是提交前的权限检查顺序二是COMMIT WORK的次数。很多保存增强是这样写的循环里做MODIFY每循环一次判断是否需要COMMIT最后再COMMIT一次。看似只提交了一次其实不对。ABAP里COMMIT WORK会结束当前数据库事务如果你在循环中间调用了CALL FUNCTION ... IN UPDATE TASK那每次COMMIT都会触发更新请求的处理。一次循环一百次就触发了一百次更新处理事务吞吐量直接崩掉。正确的做法是结束循环后统一提交。而提交之前一定要把所有权检查做完。我见过一个反例——发送审批时循环检查权限结果第三个循环被权限拦截前面已提交的数据回滚不了业务数据处于“半提交”状态这种代码才是最脏的代码。规范的做法是分三个阶段收集数据检查权限全量校验数据合法性执行数据库修改并COMMIT一次。三个阶段代码写下来结构清晰性能可控。所以你看“请求提交授权”这个热搜词本质上不是权限对象怎么配的问题而是检查点放哪里的问题。放对了性能自然好代码也整洁。5. 版本信息更新这类主数据场景缓存与批量策略5.1 为什么版本类主数据适合做缓存“abap cm_fv_prod_vers_db_update”这个热搜词让我想到一类主数据场景产品版本、物料版本、BOM版本之类的版本化数据。这类数据的特征是读取频率极高每次显示订单、检查可用性都会读更新频率低只在新版本发布时改一次。最典型的就是物料版本有效性判断——每次VA03显示项目都要判断当前物料版本是否有效。这种场景下最忌讳的写法是“每次需要就查一次数据库”。正确做法是把版本信息缓存在会话或共享内存里。简单的会话级缓存CLASS lcl_version_helper IMPLEMENTATION. METHOD get_active_version. IF mv_version_loaded abap_false. SELECT version FROM cm_fv_prod_vers WHERE matnr mv_matnr AND valid_to sy-datum ORDER BY valid_from DESCENDING INTO mv_active_version UP TO 1 ROWS. mv_version_loaded abap_true. ENDIF. rv_version mv_active_version. ENDMETHOD. ENDCLASS.这里用mv_version_loaded标志位控制只查一次。整个Session里即使VA03打开一百次也只访问一次数据库。这和前面权限检查里的缓存思路一脉相承——把重复的数据库访问降维成一次内表读取。5.2 缓存失效的设计既整洁又不出脏数据缓存最怕的是数据失效后还拿旧数据。版本类主数据的缓存失效时机很明确版本表被更新时。比如后台程序调用了CM_FV_PROD_VERS_DB_UPDATE这类更新函数更新完之后必须通知缓存失效。但ABAP的会话隔离决定了——你在这个会话里更新的数据其他会话的静态缓存根本不知道。所以跨会话缓存必须用共享内存对象Shared Memory Objects或缓存表。共享内存对象可以注册invalidation回调在数据变更时主动失效相关区域。简单场景下我更推荐“短时缓存”给缓存加时间戳超过一定时间自动重新读取IF sy-timlo mv_cache_time 60. 重新读取 ENDIF.这个做法不涉及跨会话同步逻辑简单代码可读性高。版本类数据本来一天最多变几次60秒的缓存窗口业务上完全可以接受。这又印证了“性能与整洁不矛盾”——一个带时间戳的缓存代码意图明确性能也达到了。5.3 数据库更新操作的批量思维回到“cm_fv_prod_vers_db_update”这个操作。如果程序里有批量版本发布的需求有两种做法一是循环里调用更新函数二是把数据打包批量更新。前者代码简单但性能差后者代码稍复杂但吞吐量高。在ABAP里做批量更新通常用的是MODIFY TABLE配合内表整批写入或者UPDATE ... SET配合FOR ALL ENTRIES条件。但有一种情况必须警惕如果调的更新函数是SAP标准函数它内部可能带了额外的业务逻辑比如创建变更记录这时候批量思维就不是“绕过函数自己写SQL”而是“控制调用频率”。具体来说如果必须循环调某个标准更新函数请确保循环前把所有必要的权限检查做完循环体里只做状态变更和IN UPDATE TASK的调用循环结束后统一COMMIT避免事务日志无限膨胀。有一次我优化一个产品版本发布程序原来循环三百多次调更新函数每次COMMIT总共跑了四十分钟。改成循环里只打包最后统一COMMIT时间压到五分钟以内。改动只有几行性能却提升了八倍——这就是“Mind the performance”最典型的回报。说到底性能优化和整洁代码本来就该是一件事。写代码时带着性能的警觉心你会自然地把代码整理得层次分明保持代码整洁你也更容易发现哪一步在浪费资源。这两个习惯互相滋养而不是互相妥协。最后分享一个我自己坚持了很多年的习惯每写完一个功能我会先用运行时分析器SAT跑一遍看数据库访问次数和循环调用次数。如果某个内表被循环读了超过三次或者一个功能里数据库访问超过十次我就会停下来重新审视。这个习惯救过我很多次希望你也能用上。