news 2026/9/12 3:44:06

ABAP性能与整洁代码:从数据读取到增强实现的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ABAP性能与整洁代码:从数据读取到增强实现的实战指南

做了十多年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”到底在提醒什么

我在团队里带新人时,会让他们遵守三条铁律:

  1. 任何一次数据库访问,必须知道会返回多少条数据;
  2. 任何一层循环,必须清楚循环次数和循环体内的单次代价;
  3. 任何一次内表操作,必须了解数据是否已排序、是否值得二分查找。

这三条看起来是性能要求,但真正执行起来,你会发现它同时是在逼你把代码写得结构清晰。因为如果你连数据量都不知道,说明你对业务模型的理解就是模糊的;如果你连循环嵌套次数都说不清,说明你的代码组织一定有问题。“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内表操作里,SORTREAD 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函数或者用正则表达式,但如果你在意性能,最地道的写法其实是CO(Contains Only)运算符。

IF lv_string CO '0123456789'. " 是纯数字 ELSE. " 不是纯数字 ENDIF.

这个写法的好处是:CO是ABAP语言级别的运算符,直接对字符逐位比较,不会有函数调用开销,也不会有正则引擎的启动成本。在多层循环里做校验时,这个性能差异会被放大。我测过,对一个十万字符的字符串做CO检查,比调用CL_ABAP_MATCHER快了两个数量级。

当然,CO检查的是“只包含指定字符集”,处理负数和小数时需要额外允许负号和小数点:

IF lv_string CO '0123456789.-'. " 数字或负数或小数(暂不管格式合法性) ENDIF.

如果还想更严格,可以用CS(Contains String)和CO组合。但核心思路不变:用最朴素的运算符,表达最清晰的意图,拿到最快的性能。这也是我认为“性能与整洁不矛盾”的最佳注脚——一个CO写在代码里,任何一个ABAP开发都看得懂,同时它比一堆复杂封装快得多。

3. VA03销售凭证增强:一次完整的实战推演

3.1 需求与增强点选择

“abap中va03销售凭证增强”这个热搜词说明现在做SD增强的人不少。VA03是销售订单显示事务,最常见的增强需求是:在抬头或行项目界面展示额外信息——比如物料的可用性状态、客户信用额度使用情况、或者自定义的字段描述。这类增强做起来不难,难的是做完了别把整个事务拖慢。

先说增强点选择。VA03的显示逻辑里,抬头屏幕增强一般用MV50AF01的用户出口或者BADI_SD_SALES,行项目屏幕增强可以用MV50AF01USEREXIT_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,或者直接用MARAMAKT的联合查询。但更好的思路是:出口代码里往往已经拿到了LIKPVBRK这些主表数据,你能从这些内表里直接READ TABLE出来的字段,就不要再去数据库读。

我见过一个增强,需要显示物料的“利润中心”。原做法是循环里查MARC表,每条查一次。改法很简单:先在循环外面一次性把所有物料号收集起来,用FOR ALL ENTRIES查回利润中心,再在循环里READ TABLE ... BINARY SEARCH。这里FOR ALL ENTRIES会生成一条SQL内联子查询,数据库只访问一次。这就是“Mind the performance”——把N次查询变成一次查询,同时代码意图更聚焦。

3.3 循环外的预处理:消灭N+1查询

上面说的“循环外预处理”,我再展开细讲一下,因为这是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,应该都踩过。

我还遇到过一个更隐蔽的N+1:用了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都会触发更新请求的处理。一次循环一百次,就触发了一百次更新处理,事务吞吐量直接崩掉。

正确的做法是:结束循环后统一提交。而提交之前,一定要把所有权检查做完。我见过一个反例——发送审批时循环检查权限,结果第三个循环被权限拦截,前面已提交的数据回滚不了,业务数据处于“半提交”状态,这种代码才是最脏的代码。

规范的做法是分三个阶段:

  1. 收集数据,检查权限;
  2. 全量校验数据合法性;
  3. 执行数据库修改并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”,而是“控制调用频率”。

具体来说,如果必须循环调某个标准更新函数,请确保:

  1. 循环前把所有必要的权限检查做完;
  2. 循环体里只做状态变更和IN UPDATE TASK的调用;
  3. 循环结束后统一COMMIT,避免事务日志无限膨胀。

有一次我优化一个产品版本发布程序,原来循环三百多次调更新函数,每次COMMIT,总共跑了四十分钟。改成循环里只打包,最后统一COMMIT,时间压到五分钟以内。改动只有几行,性能却提升了八倍——这就是“Mind the performance”最典型的回报。

说到底,性能优化和整洁代码本来就该是一件事。写代码时带着性能的警觉心,你会自然地把代码整理得层次分明;保持代码整洁,你也更容易发现哪一步在浪费资源。这两个习惯互相滋养,而不是互相妥协。

最后分享一个我自己坚持了很多年的习惯:每写完一个功能,我会先用运行时分析器(SAT)跑一遍,看数据库访问次数和循环调用次数。如果某个内表被循环读了超过三次,或者一个功能里数据库访问超过十次,我就会停下来重新审视。这个习惯救过我很多次,希望你也能用上。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 3:43:27

如何写出并发布你的第一篇技术博客:从选题到发布的完整指南

1. 我不做网站&#xff0c;第一篇博客就从"给同行写封信"开始 很多人一提"写博客"&#xff0c;第一反应就是&#xff1a;注册域名、买服务器、配数据库、选框架、部署上线……一套组合拳打下来少说两三个星期&#xff0c;结果博客还没写一个字&#xff0c;…

作者头像 李华
网站建设 2026/9/12 3:43:00

ThinkBook 15 G2对比P16v 2025:轻薄本与移动工作站如何选

把ThinkBook 15 G2 ITL和ThinkPad P16v 2025放在一起比&#xff0c;乍看有点“关公战秦琼”——一台是2021年前后的主流商务轻便本&#xff0c;另一台是2025年的专业移动工作站。但最近收了不少私信&#xff0c;发现好多人还真的在这两台机器之间纠结&#xff0c;尤其是预算卡在…

作者头像 李华
网站建设 2026/9/12 3:40:53

日更短剧分发工具替代方案:从TapNow迁移的实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 3:40:46

少样本点选识别实战:孪生神经网络的原理、训练与推理

简介&#xff1a;基于Python孪生神经网络的点选识别完整项目&#xff0c;自带数据集&#xff0c;面向希望学习深度学习与验证码识别技术的初、中级学习者&#xff0c;适合用于毕设、课程设计、工程实训或初期项目立项。项目通过孪生神经网络对点选文字区域进行相似度比对&#…

作者头像 李华