业务层面的优化

前言

首先说一句,在 HANA 上开发程序也需要效率优化。其次是,提到效率优化的时候,很多人都觉得是开发顾问的事情,和业务层面有什么关系?但是,程序设计来源于业务需求,程序优化不但离不开业务,而且业务优化绝对是第一步!(这里不讨论业务层面优化、代码层面优化分别能给程序带来多少效率上的提升——这也因情况而异,我只是说明程序效率优化应该遵循的步骤)
我的 ABAP 程序效率优化系列,共分三部分:
1、业务层面的优化(全文字,但都是干货)
2、ABAP 代码(内容比较多,可能需要多篇)
3、标准程序优化后的代码分享

背景

企业实施 SAP 系统初期,业务数据量小,系统功能启用的相对较少,ABAP 程序的运行速度还都是闪电般的快。可随着系统应用逐渐深化,启用的功能越来越多,业务数据量越来越大,用户对系统的理解也日益深刻,对系统的开发需求也变的更多……这时候,如果 ABAP 程序结构设计的不好,程序执行效率就非常堪忧了。

策略

首先我们回顾一下在项目中程序开发的工作步骤,大概都是这样的:
需求产生;
业务顾问分析需求;
用户确认需求;
业务顾问根据需求进行功能设计;
开发顾问进行功能开发和基本测试;
业务顾问测试;
用户测试;
需求完成;

对于 SAP 的标准功能开发,甚至是其他软件的功能开发,大致上也是遵循这个工作流程的。而这个流程表述了这样一个概念:所有的功能都是以业务需求为基础的(即便业务需求是潜在需求,功能设计也是以假想需求为基础)。所以功能都是离不开需求的,在进行程序效率优化时,我们首先要进行的就是业务层面的优化。接下来,为了进一步说明这样做的必要性,我们从反面来举几个例子谈谈原因:

例 1:已经有一个开发好的报表,是查询销售订单、生产订单及其相关的成本、收入等信息,之前的查询条件包括销售组织、工厂、物料、销售订单、生产订单等字段。
现在用户提出一个需求,期望增加一个 WBS 字段的查询条件和展示字段。然后业务顾问把需求直接转移到开发顾问(像传话筒一样),开发顾问在代码中 SQL 语句的查询条件中加一行简单的条件。
之后,用户根据 WBS 查询时,一条数据都要执行好半天。

原因分析:并不是所有的销售订单、生产订单都和 WBS 有关系,简单的 SQL 更改可能带来的是灾难式的结果。用户增加一个查询条件和展示字段,这看似简单,但实际上是换了一种模式去查询,用户是希望通过限定 WBS 取满足条件的数据。而这需要开发顾问根据不同的查询条件输入情况来进行代码优化,可是开发顾问对各模块业务上的逻辑可能并不足够熟悉(足够熟悉是业务顾问的职责)

应对策略:
业务顾问:决不能当需求的传话筒,对用户提出的需求,首先要做好这一步——“业务顾问分析需求”。比如对于同一个报表需求,可能要根据不同的查询条件,进行有差别的查询设计,而不是纯粹的把查询条件罗列出来那么简单。
开发顾问:面对业务顾问提出的类似更改需求,不妨多问几个问题,深入的了解变更原因,根据用户的真实需求去做适合的更改。(比如,查询采购订单时,系统中有 ME2L、ME2M、ME2N、ME2J 等多种方式,就是为了满足不同的查询条件,以更高的效率让用户得到更符合需求的查询结果)

例 2:有这样一个报表需求:项目、项目总成本、项目总收入、项目下的生产订单、产品、生产成本、项目下的销售订单、客户、销售订单收入。用户要求在一个报表中展示,并且能汇总统计所有项目的成本和收入。

业务顾问拿到这个需求后,如果一点也不分析,直接转给开发,并且要求做到一个普通的 ALV 中,这实在是难为开发顾问了。即便是做出来,也肯定是没办法让用户愉快的使用的。而且如果在这个基础上再频繁变更需求,最后的结果肯定是谁也看不懂的代码,以及越来越差的性能体验!(用户提出的是在一个报表中,业务顾问粗略的设计完,传递给开发顾问的时候,要求的是明确的报表类型:ALV,普通的 ALV。)

原因分析:
这个报表中,前面三个字段是项目维度,中间三个字段是生产订单维度,后面三个订单是销售订单维度。根本不在一个维度上的数据,能放在一个普通的 ALV 里吗?很明显不能,这种报表,业务顾问只要稍微分析一下,尝试用 Excel 做个样表,就能发现问题了。

应对策略:
业务顾问:决不能当需求的传话筒(重复上面的策略)。面对用户的报表需求,一定要自己尝试做个样表,验证一下用户需求和自己的逻辑。上面举的这个例子,维度区别很明显。还有很多维度比较隐晦的例子,但是经过设计样表的过程之后,就能暴露出来了。
开发顾问:普通的 ALV 确实不能实现这样的需求,但是 ALV TREE 还是勉强可以的。但也仅限于勉强。因为在该例子中,销售订单和生产订单同属于项目维度的子维度,可以在 ALV 树中作为项目节点的子节点。一般情况下,生产订单维度可以作为销售订单维度的子维度。这样可以做一个项目->销售订单->生产订单维度的树状报表。

但是还有两个问题:
1、这样的报表是用户想要的吗?
2、如果生产订单不关联销售订单呢?
所以,问题又回归到业务顾问和用户的需求分析上。

例 3:某客户有个需求,计算每个销售订单上电缆的产品毛利率。为了计算毛利率,需要知道产品的销售收入、原材料的发货成本。

销售收入,在销售模块底表中可以轻松获取。而对于原材料,业务顾问给出的方案是,根据产品递归反推 BOM(BOM 有多层),直至找到原材料的物料号为止。可当一个开发顾问历尽千辛万苦完成程序开发后,却一个多小时执行不出结果,因为反推 BOM 频繁取数据库、以及大量的递归循环的计算过程实在是太耗时了。
之后,我带着开发去找业务顾问了解电缆产品和原材料的关系,了解到电缆产品的原材料只有”铜”和”铝”两个物料号,且只能为其中一个。然后我建议在物料主数据上找一个闲置的字段填原材料,但业务顾问解释,因为种种原因不允许更改物料的标准界面或使用物料的其他字段。

之后,我提出了我的解决方案:
1、建立自定义表,维护产品的原材料属性
2、对于所有存量的销售订单,梳理其销售的产品,确定其原材料属性,作为期初数据维护到自定义表中
3、程序每次执行时,取增量的销售订单上的产品数据,检测其是否维护了原材料属性。若有没维护的则提示维护,若都维护了则记录下增量日期。
而回过头来看,如果我们针对频繁取数据库和递归耗时的 ABAP 代码进行优化,在这样一个业务层面有设计缺陷的功能上进行单纯的代码优化,又能得到什么结果?

原因分析:
原因只有一个:方案做的太死板。开始的方案设计做不好,再多的优化都是徒劳。基础不对,努力白费!

应对策略:
系统是死的,人是活的,方案更应该是活的。
不是所有的需求都要通过标准的方式或逻辑去实现,更不是系统标准功能掌握的滚瓜烂熟,就可以给客户出一个好方案。
“根据需求进行功能设计”,还是需要用心去做。如果性能比较差,就分析性能差在哪里,想办法用”简单粗暴、直奔主题”的方式解决性能瓶颈。

总结:

我们项目中遇到的程序性能问题,在业务层面的表现,大致有以下几种:
1、报表查询维度多,不同的查询维度是完全不同的查询路径
如果大量的查询逻辑和处理代码都是不同的,就应该设计成不同的查询报表。否则将带来极大的运维成本

2、报表展示维度杂乱,用户希望在一个普通 ALV 中展示,导致运算处理逻辑混乱、低效
需求不规范,一般情况下一张普通的报表就应该只有一个维度,不同维度的应该用多个报表或者树状报表展示

3、业务逻辑复杂,取数过程繁琐,造成大量的性能问题
可以根据具体需求,通过增强、自定义表等方式,在不同的业务数据间建立桥梁,简化业务逻辑,简化取数过程。

其实以上三种都有一个共同的处理思路——即,大道至简!

需求和功能之间存在着很重要的一步——需求转化为功能设计。这是一项由业务顾问主导,开发顾问积极参与并提出建议的工作。它需要业务顾问多掌握一些程序设计的理念,也需要团队所有人都对用户需求更多一些业务理解,然后对该拆分的需求进行拆分,对该合并的需求进行合并,对用户不合理的需求进行引导和优化。总之,只有整个团队的紧密配合和互相支持,才能在满足用户需求的基础上做出好的功能设计,这是做出好的开发功能的第一步,也是最重要的一步!之所以说这一步最重要,还是那句话:基础不对,努力白费!

开发优化之读取数据库

重复项-DISTINCT

尽量避免使用 SELECT DISTINCT 语句,使用 ABAP 的 SORT + DELETE ADJACENT DUPLICATES 代替

排序-ORDER BY

使用 ABAP 的 SORT 代替 SQL 中的 ORDER BY,可以降低数据库服务器的负载。

写数据库-INSERT

使用 INSERT dbtab FROM TABLE itab,而不是在 LOOP 中 INSERT INTO dbtab VALUES wa.

读取数据到内表

1、尽量不使用 SELECT *,而是列出索取字段
2、一般写法为 SELECT FIELDS INTO TABLE itab FROM dbtab。如果后续有数据处理,还会用到 LOOP AT itab 的处理。但是如果我们只处理数据一次,比如取出 bseg 的金额,根据 shkzg 对金额进行简单的正负运算后存到内表中(高版本在 OPEN SQL 中提供了处理的语法,请忽略),那么建议使用 SELECT. APPEND. ENDSELECT 的循环处理方式。这样做的效率更高。

嵌套查询、子查询

嵌套查询:SELECT 1 / SELECT 2 / ENDSELECT / ENDSELECT.
子查询:类似于 SELECT WHERE EXISTS ( SELECT WHERE )的方式
子查询优于嵌套查询,也优于 SELECT … INTO TABLE ITAB / SELECT … FOR ALL ENTRIES IN ITAB 的二次查询方式

子查询写法示例:

SELECT * FROM SFLIGHT AS F INTO SFLIGHT_WA

WHERE SEATSOCC < F~SEATSMAX

AND EXISTS ( SELECT * FROM SPFLI

WHERE CARRID = F~CARRID

AND CONNID = F~CONNID

AND CITYFROM = ‘FRANKFURT’

AND CITYTO = ‘NEW YORK’ )

ENDSELECT.

(代码取自 SE30 提示与技巧)

RANGE 和 FOR ALL ENTRIES IN

1、FOR ALL ENTRIES IN itab 使用前,要判断 itab 是否为空
2、一般情况下(法无定法,具体情况具体分析),数据量小(小于 3000),用 for all 更快。数据量大(大于 3000)时,用 range 更快,即便是分多次处理也是 range 更快
(range 条目过多时,OPEN SQL 可能会 dump,需要分多次处理)

查询条件的选择

尽量使用索引字段,包括 KEY 字段、系统提供的索引字段、自己创建的索引字段

使用游标

使用游标的好处是:

1、避免重复打开 SQL 语句,避免重复让数据库为 SQL 语句生成执行计划,节省时间
2、避免一次取出大量的数据,避免程序因内存过大而崩溃
3、降低内表数据量也有助于提升内表循环/嵌套循环的处理速度

使用方法请参见 OPEN CURSOR 的 F1 帮助,或从网上查询使用示例。

索引字段

1、根据需要创建合适的索引字段,索引字段过多对系统也是累赘
2、指定索引字段时,按从左到右的顺序指定。比如表 SFLIGHT,包含主键 CARRID、CONNID、FLDATE。

按从左到右的顺序指定,不是说在 where 条件里要把 CARRID 写在 CONNID 和 FLDATE 的前面,where carrid = ‘’ and connid = ‘’ and fldate = ‘’和 where connid = ‘’ and carrid = ‘’ and fldate = ‘’没有区别。

从左到右的顺序,指的是对于索引中位于最前面的字段,优先限定选择条件。比如,根据 CARRID=AA 和 CONNID=0017 去检索,系统会根据索引检查 AA0017%的数据。如果根据 FLDATE=20051201 检索,系统会根据索引检查**__**20051201 的数据。后者比前者更慢。所以,采用索引字段时,位于左侧的字段尽量不要留空或使用占位符_或使用匹配符%。

运算符

1、对于索引字段,尽量避免使用 NOT、NE,尽量使用肯定表示法。
a) 逻辑非(NOT 运算符)通常用于在检索合适的索引时,防止优化器选择相关的字段。如果因此无法找到合适的检索范围,则确定相应命中列表的处理量可能变得非常高,从而导致运行时间变长。
b) 逻辑非所涉及的不包含在索引中的字段则不会造成该问题。它们仅用于减少命中数量。
c) 如果无法避免对索引字段使用逻辑非(例如,因为所需 IN 列表会变得过大),则您应该指定逻辑非,以减少必须传输的数据量。

3、运算符的速度排序

速度:= > IN > BETWEEN > LIKE > NOT

【但有时 SQL 语句执行时,系统自动分配的先后顺序可能会导致取数更慢。
如:根据 BWART 和 BUDAT_MKPF(建立索引)取 MSEG,如果 BWART 用 IN,BUDAT_MKPF 用 BT,系统可能会先根据 BWART 取数。这时,尽量避免对 BWART 使用 IN,应改为 BT,让 BWART 和 BUDAT_MKPF 处在同一层级的运算符上,让数据库方便优化 SQL 的执行(也可以使用下一条里介绍的%HINTS)】

SQL 语句的人工干预(以 ORACLE 为例)

1、指定 SQL 语句使用表的某个索引

如:

DATA:lt_mseg TYPE TABLE OF mseg.

SELECT * INTO TABLE lt_mseg FROM mseg

WHERE bwart = ‘261’

AND budat_mkpf BETWEEN ‘20170701’ AND ‘20170801’

%HINTS ORACLE’INDEX(“MSEG” “MSEG~Z05”)’.

(此例中,Z05 是我对 MSEG 自己创建的索引,索引字段是 budat_mkpf)

如果我不指定索引,系统可能会优先根据 bwart 去 access 表 mseg(因为=的速度大于 BT 的速度),然后再根据 budat_mkpf 进行 filter,而 261 移动类型的数据是相当多的,这样的 SQL 相当慢,需要我们指定索引人工干预 SQL 的执行。

【注:

a) 我这里说可能会,不是说一定会。ORACLE 生成 SQL 的执行计划时,会根据数据量的大小和以往 SQL 语句执行的统计信息,自动优化代码,但这种自动优化的结果并不一定是最优的,很多时候都需要我们的人工干预。

b) 上面提到了 access 和 filter,这些我们在下一期(ST05 的那些事儿)里重点分享。】

2、对于 For all entries in itab 的优化

首先理解 for all entries in 的机制。

系统中有一个参数(可以用 TCODE:RZ11 查看)叫做 rsdb/prefer_in_itab_opt,当 where 条件只用到 itab 的一个字段时:

a) 如果 prefer_in_itab_opt 为 0,则 for all 语句执行时,对于 itab 中每条数据都转化为 or 的方式,即 field = itab[1]-value or field = itab[2]-value or field = itab[3]-value

b)如果 prefer_in_itab_opt 为 1,则 for all 语句执行时,itab 中每条数据都会转化为 in 的方式,即 field in (itab[1]-value, itab[2]-value, itab[3]-value)。而 in 的速度比 or 快。

系统中还有一个参数叫做 rsdb/max_in_blocking_factor。当 itab 中有 1 万条数据时,可能会被拆解成多个 sql 执行,而 max_in_blocking_factor 控制着 in 运算符能包含的最大的条目数(还有 min_in_blocking_factor 控制最小的条目数,不常用)。

【第二个参数不适用于簇表!!!】

(这两个参数,可以通过 RZ11 设置,也可以用%HINTS 指定,以后者的优先级为最高,且使用后者指定时,不需要写 rsdb/)

用法示例:

SELECT * FROM MSEG

FOR ALL ENTRIES IN itab

WHERE mblnr = itab-mblnr

%HINTS ORACLE ‘&prefer_in_itab_opt 1& &max_in_blocking_factor 500&’

它指定了当只使用 for all entries in itab 中的一个字段时,自动转为 in 的模式,并且每个 SQL 语句的 IN 运算符最多可以包括 500 条数据。如果 itab 有 2300 行,那么这个 SQL 语句在后台会实际执行 5 次。

尽量不在循环中使用 SELECT SINGLE,而使用 FOR ALL ENTRIES IN

依然法无定法,只是说尽量。而且有时使用 select single 还更快。

比如取 BSEG 的数据(不是簇表的版本请忽略),当确定 BSEG 的所有主键时,select into table from bseg for all entries in 要比 loop. select single. append. endloop.的方式花的时间多很多。而当不确定 BSEG 的所有主键时,FOR ALL 执行的时间也比 loop. select appending. endloop.的方式要多一些(差不多太多,但还是 for all 慢)。

不清楚这是不是簇表的原因,有知道的大顾们,欢迎留言告诉我原因。

测试代码(老白的 ABAP 群里的大 S 黄顾问的测试代码)放在百度网盘上了。

链接:https://pan.baidu.com/s/1jJ0VZc6

密码: 3ajk

但对于在稍大一点的循环(比如 1000 条以上)中取公司代码描述、工厂描述、科目描述、人员姓名一类的代码,我们是坚决不能忍的!

其它未尽的手段

除上述纯 ABAP 手段外,还有数据归档、优化表空间、碎片整理、索引重组、硬件扩容等,这些需要业务顾问、BASIS 顾问的介入甚至主导。我不是 BASIS 顾问,就不在此妄言了。但请记住,这些是整个服务器的性能瓶颈凸显后,极为极为重要的优化手段。

https://cloud.tencent.com/developer/news/78193

SD 表读取心得

SD 开发中经常会涉及到以下几张表:
• VBAK – 销售订单头表
• VBAP – 销售订单行项目表
• LIKP – 发货单头表
• LIPS – 发货单行项目表
• VBRK – 会计凭证头表
• VBRP – 会计凭证行项目表
• VBFA – 凭证流表
在读取以上 SD 表时,为提高数据读取效率,应遵循以下几条准则:

通过销售订单查询发货单 (前续凭证):

错误代码:

SELECT FROM lips WHERE vgbel = ...

正确代码:

SELECT FROM vbfa WHERE VBELV = ...
AND VBTYP_N = 'J'

SELECT FROM lips WHERE vbeln = vbfa-vbeln
AND posnr = vbfa-posnn

通过发货单查询发票凭证(前续凭证)

错误代码:

SELECT FROM vbrp WHERE vgbel = ...

正确代码:

SELECT FROM vbfa WHERE vbtyp_n = 'M'
AND vbelv = ...

SELECT FROM vbrp WHERE vbeln = vbfa-vbeln
AND posnr = vbfa-posnn

通过销售订单查询发票凭证(前续凭证)

错误代码:

SELECT FROM vbrp WHERE aubel = ...

正确代码:

SELECT FROM vbfa WHERE vbtyp_n = 'M'
AND vbelv = ...

SELECT FROM vbrp WHERE vbeln = vbfa-vbeln
AND posnr = vbfa-posnn

查询凭证流数据

在凭证流表 VBFA 中,可以通过前续凭证查找后续凭证,但是通过后续凭证查找前续凭证是没有意义的,因为前续凭证都直接保存在凭证表中(lips,vbrp 等)。
VBFA 表的主键:VBELV,POSNV(前续凭证)(HANA 数据库中主键已变为 RUUID)
字段 VBTYP_N 的固定值可以在 domain 中查看:A,B,C,D,E,F;
错误代码:

SELECT vbelv FROM vbfa WHERE vbeln ...

正确代码:

SELECT vgbel FROM lips WHERE vbeln = ...; or
SELECT vgbel FROM vbrp WHERE vbeln = ...; or
SELECT aubel FROM vbrp WHERE vbeln = ...

通过发货单读取 shipping unit

错误代码:

SELECT FROM vepo WHERE vbtyp = 'J'
AND vbeln = i_lips-vbeln

正确代码:

SELECT FROM vbfa WHERE vbtyp_n = 'X'
AND vbelv = i_lips-vbeln

SELECT FROM vepo WHERE venum = vbfa-vbeln

开发优化之内表操作

内表操作是 ABAP 程序中除了数据库读取之外,影响性能的第二大因素。在数据量大的场景下,不合理的内表操作可能导致程序运行时间成倍增长。

内表类型的选择

ABAP 提供三种内表类型,选择合适的类型对性能至关重要:

类型 关键字 查找方式 适用场景
标准表 STANDARD TABLE 线性查找 O(n) 数据量小,无需频繁查找
排序表 SORTED TABLE 二分查找 O(log n) 需要频繁按key查找,数据有序
哈希表 HASHED TABLE 哈希查找 O(1) 大量数据,频繁按完整key查找

经验法则:

  • 当内表超过 100 行且需要频繁 READ TABLE 时,优先使用 SORTED TABLE 或 HASHED TABLE
  • 只需要顺序处理(LOOP)而不需要随机查找时,使用 STANDARD TABLE
  • 需要按完整 key 精确查找时,HASHED TABLE 最优
  • 需要按 key 范围查找或前缀查找时,SORTED TABLE 最优
* 标准表 - 线性查找,数据量大时很慢
DATA: lt_standard TYPE STANDARD TABLE OF mara.

* 排序表 - 二分查找,适合频繁按key查找
DATA: lt_sorted TYPE SORTED TABLE OF mara
WITH UNIQUE KEY matnr.

* 哈希表 - O(1)查找,适合大量数据精确匹配
DATA: lt_hashed TYPE HASHED TABLE OF mara
WITH UNIQUE KEY matnr.

READ TABLE 的优化

对于 STANDARD TABLE,如果已经排序,可以使用 BINARY SEARCH 将查找从 O(n) 优化到 O(log n):

* 低效写法 - 线性查找 O(n)
LOOP AT lt_data INTO ls_data WHERE matnr = lv_matnr.
" 处理逻辑
ENDLOOP.

* 优化写法 - 先排序再二分查找
SORT lt_data BY matnr.
READ TABLE lt_data INTO ls_data
WITH KEY matnr = lv_matnr
BINARY SEARCH.
IF sy-subrc = 0.
" 处理逻辑
ENDIF.

注意: 使用 BINARY SEARCH 前必须先按查找字段排序,否则结果不可靠。

* 最佳实践:直接使用哈希表
DATA: lt_materials TYPE HASHED TABLE OF mara
WITH UNIQUE KEY matnr.

* 查找效率 O(1)
READ TABLE lt_materials INTO ls_material
WITH TABLE KEY matnr = lv_matnr.

读取多条记录的优化

* 低效:循环中逐条读取
LOOP AT lt_input INTO ls_input.
READ TABLE lt_master INTO ls_master
WITH KEY matnr = ls_input-matnr.
IF sy-subrc = 0.
ls_input-maktx = ls_master-maktx.
MODIFY lt_input FROM ls_input.
ENDIF.
ENDLOOP.

* 高效:使用 SORTED TABLE + LOOP WHERE
DATA: lt_master TYPE SORTED TABLE OF mara
WITH UNIQUE KEY matnr.

LOOP AT lt_input ASSIGNING FIELD-SYMBOL(<fs_input>).
READ TABLE lt_master INTO ls_master
WITH TABLE KEY matnr = <fs_input>-matnr.
IF sy-subrc = 0.
<fs_input>-maktx = ls_master-maktx.
ENDIF.
ENDLOOP.

LOOP 的优化

使用 FIELD-SYMBOLS 替代工作区

* 低效:每次循环都复制数据到工作区
LOOP AT lt_data INTO ls_data.
ls_data-price = ls_data-price * 1.1.
MODIFY lt_data FROM ls_data.
ENDLOOP.

* 高效:直接通过字段符号修改内表,避免数据复制
LOOP AT lt_data ASSIGNING FIELD-SYMBOL(<fs_data>).
<fs_data>-price = <fs_data>-price * 1.1.
ENDLOOP.

性能对比: 对于 10 万行数据,FIELD-SYMBOLS 方式通常比 INTO + MODIFY 快 30%-50%。

使用 WHERE 条件过滤

* 低效:在循环体内判断
LOOP AT lt_data INTO ls_data.
IF ls_data-matnr = lv_matnr.
" 处理逻辑
ENDIF.
ENDLOOP.

* 高效:使用 WHERE 条件,数据库层面过滤
LOOP AT lt_data INTO ls_data WHERE matnr = lv_matnr.
" 处理逻辑
ENDLOOP.

减少嵌套循环

* 低效:嵌套循环 O(n*m)
LOOP AT lt_orders INTO ls_order.
LOOP AT lt_items INTO ls_item WHERE order_id = ls_order-order_id.
" 处理逻辑
ENDLOOP.
ENDLOOP.

* 高效:使用 SORTED TABLE 优化内层查找
DATA: lt_items_sorted TYPE SORTED TABLE OF order_item
WITH NON-UNIQUE KEY order_id.

LOOP AT lt_orders INTO ls_order.
LOOP AT lt_items_sorted INTO ls_item
WHERE order_id = ls_order-order_id.
" 处理逻辑
ENDLOOP.
ENDLOOP.

避免在循环中修改内表结构

* 低效:频繁 MODIFY
LOOP AT lt_data INTO ls_data.
ls_data-status = 'X'.
MODIFY lt_data FROM ls_data.
ENDLOOP.

* 高效:使用 FIELD-SYMBOLS 直接修改
LOOP AT lt_data ASSIGNING FIELD-SYMBOL(<fs>).
<fs>-status = 'X'.
ENDLOOP.

内表操作的最佳实践

批量操作替代逐行操作

* 低效:逐行 APPEND
LOOP AT lt_source INTO ls_source.
APPEND ls_source TO lt_target.
ENDLOOP.

* 高效:批量 APPEND LINES OF
APPEND LINES OF lt_source TO lt_target.

* 或者使用 INSERT LINES OF
INSERT LINES OF lt_source INTO TABLE lt_target.

合理使用 COLLECT

* COLLECT 自动按非数值字段汇总,适合分组统计
DATA: ls_sum TYPE ty_sum.

LOOP AT lt_detail INTO ls_detail.
ls_sum-group = ls_detail-group.
ls_sum-amount = ls_detail-amount.
COLLECT ls_sum INTO lt_summary.
ENDLOOP.

使用 CORRESPONDING 减少字段映射代码

* 低效:逐字段赋值
ls_target-field1 = ls_source-field1.
ls_target-field2 = ls_source-field2.
ls_target-field3 = ls_source-field3.

* 高效:CORRESPONDING
ls_target = CORRESPONDING #( ls_source ).

DELETE 与 FILTER 的高效使用

* 删除重复行
SORT lt_data BY matnr.
DELETE ADJACENT DUPLICATES FROM lt_data COMPARING matnr.

* 使用 FILTER 快速筛选
DATA: lt_filtered TYPE SORTED TABLE OF mara WITH UNIQUE KEY matnr.
lt_filtered = FILTER #( lt_master WHERE matnr = lt_filter_keys ).

开发优化之字符串操作

字符串操作在 ABAP 中也常常成为性能瓶颈,尤其是在处理大量文本数据时。

字符串拼接

CONCATENATE vs && vs 模板字符串

* 方式1:CONCATENATE(传统方式,兼容性好)
CONCATENATE lv_first lv_second lv_third INTO lv_result SEPARATED BY space.

* 方式2:&& 操作符(简洁,但多次拼接效率低)
lv_result = lv_first && space && lv_second && space && lv_third.

* 方式3:模板字符串(推荐,代码简洁且效率高)
lv_result = |{ lv_first } { lv_second } { lv_third }|.

性能对比:

  • 单次拼接:三者差异不大
  • 多次拼接(>5次):模板字符串最优
  • 循环中拼接大量字符串:使用 CONCATENATE 或模板字符串

循环中的字符串拼接

* 低效:循环中频繁拼接(每次创建新字符串对象)
DATA: lv_result TYPE string.
LOOP AT lt_data INTO ls_data.
CONCATENATE lv_result ls_data-text cl_abap_char_utilities=>newline INTO lv_result.
ENDLOOP.

* 高效:先收集到内表,最后一次性拼接
DATA: lt_lines TYPE TABLE OF string.
LOOP AT lt_data INTO ls_data.
APPEND ls_data-text TO lt_lines.
ENDLOOP.
CONCATENATE LINES OF lt_lines INTO lv_result SEPARATED BY cl_abap_char_utilities=>newline.

字符串查找

FIND vs CS/NS/CP/NP

* 方式1:FIND(推荐,功能强大)
FIND FIRST OCCURRENCE OF lv_pattern IN lv_text
MATCH OFFSET DATA(lv_offset)
MATCH LENGTH DATA(lv_length).

* 方式2:CS/NS(简单包含检查)
IF lv_text CS lv_pattern.
" 包含
ENDIF.

* 方式3:CP/NP(模式匹配)
IF lv_text CP '*pattern*'.
" 匹配
ENDIF.

使用正则表达式

* 复杂模式匹配使用正则
FIND FIRST OCCURRENCE OF REGEX '\d{4}-\d{2}-\d{2}' IN lv_text.

* 批量替换
REPLACE ALL OCCURRENCES OF REGEX '\s+' IN lv_text WITH space.

字符串分割

* 低效:循环 + SUBSTRING
DATA: lv_offset TYPE i VALUE 0.
WHILE lv_offset < strlen( lv_text ).
FIND FIRST OCCURRENCE OF ',' IN lv_text+lv_offset
MATCH OFFSET DATA(lv_pos).
IF sy-subrc <> 0.
EXIT.
ENDIF.
APPEND lv_text+lv_offset(lv_pos) TO lt_parts.
lv_offset = lv_offset + lv_pos + 1.
ENDWHILE.

* 高效:使用 SPLIT
SPLIT lv_text AT ',' INTO TABLE lt_parts.

STRING 与 XSTRING 的选择

* 纯文本处理使用 STRING
DATA: lv_text TYPE string.

* 二进制数据使用 XSTRING
DATA: lv_binary TYPE xstring.

* 转换
lv_binary = cl_abap_conv_codepage=>create_out( )->convert( lv_text ).
lv_text = cl_abap_conv_codepage=>create_in( )->convert( lv_binary ).

开发优化之字段符号与数据引用

字段符号(FIELD-SYMBOLS)的高效使用

避免不必要的数据复制

* 低效:数据复制到工作区
DATA: ls_mara TYPE mara.
READ TABLE lt_mara INTO ls_mara INDEX 1.
ls_mara-matnr = 'NEW'.
MODIFY lt_mara FROM ls_mara INDEX 1.

* 高效:通过字段符号直接操作内表
FIELD-SYMBOLS: <fs_mara> TYPE mara.
READ TABLE lt_mara ASSIGNING <fs_mara> INDEX 1.
<fs_mara>-matnr = 'NEW'.

动态字段访问

* 动态访问结构字段
DATA: lv_fieldname TYPE string VALUE 'MATNR'.
ASSIGN COMPONENT lv_fieldname OF STRUCTURE ls_mara TO FIELD-SYMBOL(<fs_value>).
IF sy-subrc = 0.
WRITE: / <fs_value>.
ENDIF.

嵌套表的高效处理

* 处理嵌套内表
FIELD-SYMBOLS: <lt_inner> TYPE ANY TABLE.
LOOP AT lt_outer ASSIGNING FIELD-SYMBOL(<ls_outer>).
ASSIGN COMPONENT 'ITEMS' OF STRUCTURE <ls_outer> TO <lt_inner>.
IF sy-subrc = 0.
LOOP AT <lt_inner> ASSIGNING FIELD-SYMBOL(<ls_inner>).
" 处理内层数据
ENDLOOP.
ENDIF.
ENDLOOP.

数据引用(DATA REFERENCE)的使用

动态创建数据对象

DATA: lo_data TYPE REF TO data.
CREATE DATA lo_data TYPE mara.
ASSIGN lo_data->* TO FIELD-SYMBOL(<fs_data>).

泛型容器

CLASS lcl_container DEFINITION.
PUBLIC SECTION.
METHODS: set_data IMPORTING ir_data TYPE REF TO data,
get_data RETURNING VALUE(rr_data) TYPE REF TO data.
PRIVATE SECTION.
DATA: mr_data TYPE REF TO data.
ENDCLASS.

CLASS lcl_container IMPLEMENTATION.
METHOD set_data.
mr_data = ir_data.
ENDMETHOD.
METHOD get_data.
rr_data = mr_data.
ENDMETHOD.
ENDCLASS.

开发优化之 HANA 数据库

在 SAP HANA 数据库环境下,ABAP 程序的优化策略与传统 Oracle/DB2 有所不同。HANA 是列式存储数据库,擅长聚合计算和大规模数据扫描,但也有一些特殊的优化要点。

计算下沉(Push Down)

HANA 的核心优势在于将计算下沉到数据库层执行,减少应用层与数据库层之间的数据传输。

使用 CDS View 实现计算下沉

* 定义 CDS View,将聚合计算下沉到数据库
@AbapCatalog.sqlViewName: 'ZSALESSUM'
define view Z_SALES_SUMMARY
as select from vbak
inner join vbap on vbak.vbeln = vbap.vbeln
{
key vbak.vkorg as SalesOrg,
key vbak.vtweg as DistrChannel,
@Semantics.amount.currencyCode: 'Currency'
sum(vbap.netwr) as TotalNetValue,
count(*) as OrderCount,
vbak.waerk as Currency
}
group by vbak.vkorg, vbak.vtweg, vbak.waerk;

使用 CDS View Entity (新语法)

define view entity Z_MATERIAL_STOCK
as select from mseg
{
key matnr as Material,
key werks as Plant,
@Semantics.quantity.unitOfMeasure: 'Meins'
sum( case when shkzg = 'S' then menge else -menge end ) as StockQty,
meins as Meins
}
group by matnr, werks, meins;

AMDP (ABAP Managed Database Procedures)

AMDP 允许在 ABAP 中直接编写 HANA 存储过程,适合复杂的计算逻辑。

定义 AMDP 类

CLASS zcl_sales_analytics DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES: if_amdp_marker_hdb.
TYPES: BEGIN OF ty_result,
sales_org TYPE vkorg,
material TYPE matnr,
total_qty TYPE menge_d,
total_amt TYPE netwr,
END OF ty_result,
tt_result TYPE TABLE OF ty_result.

METHODS: get_sales_data
IMPORTING
VALUE(iv_date_from) TYPE dats
VALUE(iv_date_to) TYPE dats
EXPORTING
VALUE(et_result) TYPE tt_result.
ENDCLASS.

CLASS zcl_sales_analytics IMPLEMENTATION.
METHOD get_sales_data
BY DATABASE PROCEDURE FOR HDB
LANGUAGE SQLSCRIPT
USING vbak vbap.

et_result = SELECT
a.vkorg AS sales_org,
b.matnr AS material,
SUM(b.kwmeng) AS total_qty,
SUM(b.netwr) AS total_amt
FROM vbak AS a
INNER JOIN vbap AS b
ON a.vbeln = b.vbeln
WHERE a.erdat BETWEEN iv_date_from AND iv_date_to
GROUP BY a.vkorg, b.matnr;

ENDMETHOD.
ENDCLASS.

AMDP 的适用场景

  1. 复杂计算逻辑:多表关联、窗口函数、递归查询
  2. 大量数据聚合:GROUP BY + 聚合函数
  3. 存储过程调用:已有 HANA 存储过程需要从 ABAP 调用
  4. 性能关键路径:CDS View 无法满足的复杂业务逻辑

HANA 特有的 SQL 优化

使用窗口函数

-- 在 CDS View 或 AMDP 中使用窗口函数
SELECT
vbeln,
posnr,
matnr,
netwr,
SUM(netwr) OVER(PARTITION BY vbeln) AS order_total,
ROW_NUMBER() OVER(PARTITION BY vbeln ORDER BY netwr DESC) AS rank
FROM vbap;

使用 CE 函数(Calculation Engine)

-- 在 AMDP 中使用 CE 函数获得更好性能
CE_COLUMN_TABLE("VBAP", ["VBELN", "POSNR", "MATNR", "NETWR"])
CE_PROJECT(["VBELN", "MATNR", "NETWR"])
CE_AGGREGATION([SUM("NETWR") AS "TOTAL"], ["VBELN"])

利用列式存储特性

* HANA 列式存储对以下操作友好:
* 1. 单列聚合(只读取需要的列)
SELECT matnr, SUM(menge) FROM mseg
GROUP BY matnr.

* 2. 高选择性过滤(快速定位行)
SELECT * FROM mseg WHERE matnr = '000000000000000001'.

* 3. 大表扫描(列式压缩减少IO)
SELECT COUNT(*) FROM bseg WHERE gjahr = '2024'.

CDS View 性能优化技巧

使用 @Consumption.filter 实现动态过滤

define view entity Z_ORDERS
with parameters
@Consumption.filter: { selectionType: #INTERVAL, mandatory: true }
p_date : sydatum
as select from vbak
{
key vbeln,
erdat,
netwr
}
where erdat >= $parameters.p_date;

避免 CDS View 中的 N+1 问题

* 低效:关联时使用子查询
define view entity Z_BAD_EXAMPLE
as select from vbak
{
key vbeln,
( select sum(netwr) from vbap where vbeln = vbak.vbeln ) as total
}

* 高效:使用 JOIN
define view entity Z_GOOD_EXAMPLE
as select from vbak
inner join vbap on vbak.vbeln = vbap.vbeln
{
key vbak.vbeln,
sum(vbap.netwr) as total
}
group by vbak.vbeln;

使用 @ObjectModel 实现懒加载

@ObjectModel.representativeKey: 'Material'
define view entity Z_MATERIAL
as select from mara
{
key matnr as Material,
mtart as Type,
@ObjectModel.association.type: [#TO_COMPOSITION_CHILD]
_Descriptions
}

HANA 执行计划分析

使用 ST05 分析 SQL 执行

  1. 运行 ST05,激活 SQL Trace
  2. 执行目标程序
  3. 停止 Trace,查看执行计划
  4. 关注以下指标:
    • Execution Time:SQL 执行时间
    • Records:处理的记录数
    • Plan Operations:执行计划中的操作类型

常见性能问题及解决

问题 表现 解决方案
全表扫描 TABLE SCAN 操作 添加合适的过滤条件或索引
数据传输过大 返回大量行到应用层 使用聚合减少返回数据量
多次数据库访问 循环中执行 SQL 合并为单次查询或使用 FAE
不必要的列 SELECT * 只选择需要的列

开发优化之性能分析工具

ABAP 提供了丰富的性能分析工具,帮助开发者定位和解决性能瓶颈。

SE30/SAT - 运行时分析

SE30(新版本为 SAT)是 ABAP 最核心的性能分析工具,可以精确测量程序中每个方法、每条 SQL 的执行时间。

使用步骤

  1. 启动分析:输入事务码 SE30/SAT
  2. 配置参数
    • 输入要分析的程序/事务码
    • 选择分析类型(统计/详细/限制)
    • 设置测量范围(完整程序/特定部分)
  3. 执行程序:系统自动记录执行数据
  4. 分析结果:查看热点列表、调用层次、时间分布

关键指标解读

+-----------------------------------------------------------------------+
| 热点列表 (Hit List) |
+-----------------------------------------------------------------------+
| 位置 | 类型 | 事件数 | 时间(μs) | 占比 |
|-------------------------------|---------|---------|----------|--------|
| ZCL_REPORT->GET_DATA | Method | 1 | 2,345,678| 78.2% |
| SQL: SELECT FROM VBAK | DB | 15,234 | 1,234,567| 41.2% |
| LOOP AT lt_data | Loop | 50,000 | 456,789 | 15.2% |
| READ TABLE lt_master | Read | 50,000 | 234,567 | 7.8% |
| ZCL_REPORT->FORMAT_OUTPUT | Method | 1 | 654,321 | 21.8% |
+-----------------------------------------------------------------------+

重点关注:

  • 时间占比最高的方法/SQL:最大的优化目标
  • 事件数过多的循环:可能存在不必要的重复操作
  • 数据库访问占比:理想情况应低于 30%

限制分析范围

* 在代码中精确控制分析范围
SET RUN TIME ANALYZER ON. " 开始测量
" 需要分析的代码段
PERFORM critical_process.
SET RUN TIME ANALYZER OFF. " 停止测量

提示与技巧(Tips & Tricks)

SE30 中集成了大量性能优化技巧,按类别组织:

类别 技巧数量 典型内容
SQL 30+ SELECT优化、索引使用、FAE技巧
内表 20+ 表类型选择、排序、查找优化
字符串 10+ 拼接、查找、转换优化
ABAP Objects 10+ 对象创建、方法调用、接口使用

ST05 - SQL 跟踪

ST05 用于分析程序执行过程中的数据库访问,是优化 SQL 语句的必备工具。

使用步骤

  1. 激活 Trace:ST05 → Activate Trace
  2. 执行程序:运行需要分析的程序
  3. 停止 Trace:ST05 → Deactivate Trace
  4. 显示 Trace:ST05 → Display Trace
  5. 分析执行计划:选择 SQL 语句 → Display Execution Plan

关键指标

指标 说明 优化方向
Duration SQL 执行时间 减少不必要的查询
Records 返回/处理的行数 缩小查询范围
Operation 执行计划操作 避免 TABLE SCAN,使用 INDEX
Buffer 缓存命中率 合理使用表缓冲

执行计划解读

OPERATION          | OBJECT    | COST | ROWS | BYTES | FILTER
-------------------+-----------+------+------+-------+--------
NESTED LOOPS | | 15 | 100 | 5000 |
INDEX RANGE SCAN | VBAK~001 | 3 | 10 | 300 | ERDAT = '20240101'
TABLE ACCESS BY | VBAP | 12 | 100 | 4700 |
INDEX RANGE | VBAP~001 | 4 | 100 | 300 | VBELN = VBAK~VBELN

优化建议:

  • COST 值越高,优化空间越大
  • INDEX RANGE SCAN 优于 TABLE ACCESS FULL
  • 减少 NESTED LOOPS 的外层行数

SQL 执行计划图形化分析

ST05 支持图形化显示执行计划,直观展示:

  • 数据流向
  • 各节点耗时
  • 索引使用情况

SQLM - SQL Monitor

SQLM 用于监控生产环境中 SQL 语句的执行情况,适合长期性能监控。

配置步骤

  1. 启动监控:SQLM → Start Monitoring
  2. 设置过滤:指定用户、程序、时间范围
  3. 收集数据:系统持续记录 SQL 执行
  4. 分析结果:SQLM → Display Monitoring Data

监控结果分析

+--------------------------------------------------------------------+
| SQL Monitor 结果 |
+--------------------------------------------------------------------+
| 程序名 | SQL 语句摘要 | 执行次数 | 平均时间 | 总时间 |
|-----------------+----------------------+----------+----------+--------+
| ZSDR001 | SELECT FROM VBAK | 12,345 | 15ms | 185s |
| ZSDR001 | SELECT FROM VBAP | 12,345 | 8ms | 99s |
| ZMMR002 | SELECT FROM EKPO | 8,901 | 45ms | 401s |
+--------------------------------------------------------------------+

优化优先级:

  • 总时间最高的 SQL 语句优先优化
  • 执行次数过多的 SQL 考虑合并或缓存
  • 平均时间过长的 SQL 需要检查执行计划

SCI - 代码检查器

SCI(Code Inspector)用于静态代码分析,可以在开发阶段发现潜在的性能问题。

检查类别

类别 说明 典型问题
Performance 性能相关 SELECT *、循环内SQL、缺少索引
Security 安全相关 SQL注入、权限检查
Robustness 健壮性 空指针、异常处理
Search Functions 搜索功能 废弃语法、兼容性问题

自定义检查变式

检查变式: Z_PERF_CHECK
├── Performance
│ ├── SELECT * 使用 → 错误
│ ├── 循环内数据库访问 → 警告
│ ├── 嵌套循环 → 警告
│ ├── 缺少 BINARY SEARCH → 信息
│ └── 不必要的数据复制 → 信息
├── Robustness
│ ├── 未检查 sy-subrc → 警告
│ └── 异常处理不当 → 警告
└── Search Functions
└── 废弃语法使用 → 信息

与 ATC 集成

ATC(ABAP Test Cockpit)是 SCI 的增强版,集成在开发流程中:

  • 传输请求发布时自动检查
  • Eclipse ADT 中实时检查
  • 持续集成(CI)中自动运行

代码审查 Checkpoint

在性能优化中,以下检查点应作为代码审查的标准项:

数据库访问

  • 避免 SELECT *
  • 避免在循环中执行 SELECT
  • FOR ALL ENTRIES IN 前检查内表是否为空
  • 使用合适的索引字段
  • 子查询优于嵌套查询

内表操作

  • 选择合适的内表类型(STANDARD/SORTED/HASHED)
  • READ TABLE 使用 BINARY SEARCH 或 SORTED/HASHED 表
  • LOOP 中使用 FIELD-SYMBOLS
  • 批量操作替代逐行操作
  • 避免不必要的内表复制

字符串操作

  • 多次拼接使用模板字符串
  • 循环中拼接先收集再合并
  • 使用 SPLIT 替代手动解析

算法设计

  • 避免不必要的嵌套循环
  • 使用哈希表优化查找
  • 减少数据库与应用层的数据传输
  • 考虑计算下沉(CDS/AMDP)

开发优化之内存管理

ABAP 程序的内存管理对性能有直接影响,尤其是在处理大量数据时。

内存分配原则

ABAP 内存架构

+---------------------------+
| ABAP 程序内存区域 |
+---------------------------+
| 栈(Stack) | ← 方法调用、局部变量
| 堆(Heap) | ← 内表、字符串、对象
| 共享内存(Shared Memory) | ← 全局数据、缓冲区
+---------------------------+

内存限制

  • 单个内表:最大 2GB(理论值)
  • 单个工作进程:默认 2GB,可通过参数调整
  • 扩展内存:通过 ztta/roll_extension 参数配置

内表内存优化

使用 COMPACT 内表

* 标准内表 - 每行有对齐填充
DATA: lt_standard TYPE STANDARD TABLE OF mara.

* 紧凑内表 - 减少内存占用
TYPES: BEGIN OF ty_compact,
matnr TYPE matnr, " 18 bytes
maktx TYPE maktx, " 40 bytes
mtart TYPE mtart, " 4 bytes
END OF ty_compact.
DATA: lt_compact TYPE STANDARD TABLE OF ty_compact.

合理使用 HEADER LINE

* 旧语法(不推荐)
DATA: lt_mara TYPE TABLE OF mara WITH HEADER LINE.

* 新语法(推荐)
DATA: lt_mara TYPE TABLE OF mara,
ls_mara TYPE mara.

及时释放内存

* 处理完大内表后及时释放
FREE lt_large_table.

* 或者清空后等待垃圾回收
CLEAR lt_large_table.

字符串内存优化

STRING vs C 类型

* STRING - 动态分配,适合变长文本
DATA: lv_text TYPE string. " 动态内存

* C - 固定长度,适合定长字段
DATA: lv_code TYPE c LENGTH 10. " 固定 10 字节

避免不必要的字符串复制

* 低效:创建多个临时字符串
lv_result = lv_a && lv_b && lv_c.

* 高效:使用 CONCATENATE 减少临时对象
CONCATENATE lv_a lv_b lv_c INTO lv_result.

对象内存管理

对象池模式

CLASS lcl_object_pool DEFINITION.
PUBLIC SECTION.
CLASS-METHODS: get_object RETURNING VALUE(ro_obj) TYPE REF TO lcl_object.
PRIVATE SECTION.
CLASS-DATA: gt_pool TYPE TABLE OF REF TO lcl_object.
ENDCLASS.

CLASS lcl_object_pool IMPLEMENTATION.
METHOD get_object.
IF gt_pool IS NOT INITIAL.
ro_obj = gt_pool[ 1 ].
DELETE gt_pool INDEX 1.
ELSE.
CREATE OBJECT ro_obj.
ENDIF.
ENDMETHOD.
ENDCLASS.

避免循环引用

* 低效:循环引用导致内存泄漏
* Object A -> Object B -> Object A

* 高效:使用弱引用或手动断开
DATA: lo_weak TYPE REF TO cl_abap_weak_reference.
lo_weak = NEW cl_abap_weak_reference( lo_object ).

大数据量处理策略

分批处理

* 分批读取大数据量
DATA: lt_chunk TYPE TABLE OF mara,
lv_batch_size TYPE i VALUE 10000.

OPEN CURSOR WITH HOLD DATA(lv_cursor) FOR
SELECT * FROM mara.

DO.
FETCH NEXT CURSOR lv_cursor
INTO TABLE lt_chunk
PACKAGE SIZE lv_batch_size.

IF sy-subrc <> 0.
EXIT.
ENDIF.

" 处理当前批次
PERFORM process_chunk USING lt_chunk.

" 释放当前批次内存
FREE lt_chunk.
ENDDO.

CLOSE CURSOR lv_cursor.

使用临时表

* 使用全局临时表(GTT)减少内存压力
CREATE GLOBAL TEMPORARY TABLE zgtt_temp (
matnr TYPE matnr,
menge TYPE menge_d
) ON COMMIT DELETE ROWS.

* 在数据库层处理,减少应用层内存
INSERT zgtt_temp FROM TABLE lt_source.
SELECT * FROM zgtt_temp INTO TABLE lt_result.

开发优化之并行处理

并行处理可以充分利用多核 CPU 资源,显著提升程序执行效率。

异步 RFC 调用

基本用法

* 定义 RFC 函数模块
FUNCTION z_parallel_process.
IMPORTING
VALUE(iv_range_from) TYPE matnr
VALUE(iv_range_to) TYPE matnr
EXPORTING
VALUE(et_result) TYPE ty_result_tab.

" 处理指定范围的数据
SELECT * FROM mara
INTO TABLE et_result
WHERE matnr BETWEEN iv_range_from AND iv_range_to.

ENDFUNCTION.

* 主程序:并行调用
DATA: lt_result TYPE TABLE OF mara,
lv_tasks TYPE i VALUE 4. " 并行任务数

* 拆分数据范围
DATA: lt_ranges TYPE TABLE OF rsrange.
" ... 生成范围拆分逻辑

* 异步调用
LOOP AT lt_ranges INTO DATA(ls_range).
CALL FUNCTION 'Z_PARALLEL_PROCESS'
STARTING NEW TASK CONV sytabix( sy-tabix )
DESTINATION IN GROUP DEFAULT
EXPORTING
iv_range_from = ls_range-low
iv_range_to = ls_range-high
EXCEPTIONS
system_failure = 1
communication_failure = 2
resource_failure = 3
OTHERS = 4.

IF sy-subrc = 0.
" 任务启动成功
ELSE.
" 处理启动失败
ENDIF.
ENDLOOP.

* 等待所有任务完成
WAIT UNTIL gv_completed = lv_tasks UP TO 300 SECONDS.

接收结果

* 在回调函数中接收结果
FORM callback USING taskname.
RECEIVE RESULTS FROM FUNCTION 'Z_PARALLEL_PROCESS'
IMPORTING
et_result = gt_partial_result
EXCEPTIONS
system_failure = 1
communication_failure = 2
OTHERS = 3.

IF sy-subrc = 0.
APPEND LINES OF gt_partial_result TO gt_final_result.
gv_completed = gv_completed + 1.
ENDIF.
ENDFORM.

处理资源限制

控制并发数

* 使用 RFC 资源限制
DATA: lv_max_tasks TYPE i VALUE 8.

" 使用 DESTINATION IN GROUP 限制并发
CALL FUNCTION 'Z_PARALLEL_PROCESS'
STARTING NEW TASK lv_taskname
DESTINATION IN GROUP 'PARALLEL_GROUP'
...
EXCEPTIONS
resource_failure = 3.

IF sy-subrc = 3.
" 资源不足,等待后重试
WAIT UP TO 1 SECONDS.
" 重试逻辑
ENDIF.

监控并行任务

* 检查任务状态
CALL FUNCTION 'Z_PARALLEL_PROCESS'
STARTING NEW TASK lv_taskname
DESTINATION IN GROUP DEFAULT
PERFORMING callback ON END OF TASK
...

并行处理的最佳实践

适用场景

场景 适用性 说明
大数据量查询 ✓ 优秀 按范围拆分并行查询
批量数据处理 ✓ 优秀 独立数据块并行处理
复杂计算 ✓ 良好 CPU密集型任务
事务性操作 ✗ 不适合 需要数据一致性
有依赖的操作 ✗ 不适合 步骤间有依赖关系

注意事项

  1. 数据一致性:并行处理的数据必须相互独立
  2. 资源控制:避免创建过多并行任务导致资源耗尽
  3. 错误处理:每个并行任务都需要独立的错误处理
  4. 结果合并:考虑结果合并的性能开销
  5. 调试困难:并行程序调试复杂,建议先串行验证逻辑

开发优化之实战案例

案例一:销售报表优化

问题描述

原始程序执行时间超过 30 分钟,用户无法接受。

性能分析

使用 SE30 分析发现:

  • 70% 时间在数据库访问
  • 20% 时间在内表循环
  • 10% 时间在数据输出

优化方案

* 优化前:循环中多次查询
LOOP AT lt_orders INTO ls_order.
SELECT SINGLE * FROM vbak INTO ls_vbak
WHERE vbeln = ls_order-vbeln.
SELECT SINGLE * FROM vbap INTO ls_vbap
WHERE vbeln = ls_order-vbeln.
" ... 更多查询
ENDLOOP.

* 优化后:一次性查询 + 哈希表关联
SELECT * FROM vbak INTO TABLE lt_vbak
FOR ALL ENTRIES IN lt_orders
WHERE vbeln = lt_orders-vbeln.

SELECT * FROM vbap INTO TABLE lt_vbap
FOR ALL ENTRIES IN lt_orders
WHERE vbeln = lt_orders-vbeln.

DATA: lt_vbak_h TYPE HASHED TABLE OF vbak WITH UNIQUE KEY vbeln.
lt_vbak_h = lt_vbak.

LOOP AT lt_orders INTO ls_order.
READ TABLE lt_vbak_h INTO ls_vbak
WITH TABLE KEY vbeln = ls_order-vbeln.
" 处理逻辑
ENDLOOP.

优化结果

  • 执行时间:30 分钟 → 45 秒
  • 数据库访问:500 次 → 3 次
  • 内存使用:减少 60%

案例二:物料主数据批量处理

问题描述

批量更新 10 万条物料数据,程序运行缓慢。

优化方案

* 使用并行处理
DATA: lv_batch_size TYPE i VALUE 5000,
lv_tasks TYPE i VALUE 4.

* 拆分数据
LOOP AT lt_materials INTO ls_material.
APPEND ls_material TO lt_chunk.
IF lines( lt_chunk ) >= lv_batch_size.
PERFORM start_parallel_task USING lt_chunk lv_task_id.
CLEAR lt_chunk.
lv_task_id = lv_task_id + 1.
ENDIF.
ENDLOOP.

* 处理剩余数据
IF lt_chunk IS NOT INITIAL.
PERFORM start_parallel_task USING lt_chunk lv_task_id.
ENDIF.

* 等待所有任务完成
WAIT UNTIL gv_completed = lv_task_id.

优化结果

  • 执行时间:2 小时 → 25 分钟
  • 系统资源利用:单核 100% → 多核 80%

案例三:库存查询报表

问题描述

查询特定时间段的库存变动,数据量约 500 万条。

优化方案

* 使用 CDS View + 参数化查询
define view entity Z_STOCK_MOVEMENT
with parameters
p_plant : werks_d,
p_date : sydatum
as select from mseg
{
key matnr,
key werks,
sum( case when shkzg = 'S' then menge else -menge end ) as stock_qty
}
where werks = $parameters.p_plant
and budat_mkpf >= $parameters.p_date
group by matnr, werks;

* ABAP 调用
SELECT * FROM Z_STOCK_MOVEMENT( p_plant = '1000', p_date = '20240101' )
INTO TABLE @DATA(lt_result).

优化结果

  • 执行时间:15 分钟 → 8 秒
  • 数据传输量:减少 95%
  • 应用层处理:减少 90%

开发优化之 Checklist

开发前检查

需求分析

  • 明确数据量级(千/万/百万)
  • 确定查询条件和索引字段
  • 评估是否需要并行处理
  • 确认报表展示维度是否统一

技术选型

  • 选择合适的技术栈(ALV/CDS/AMDP)
  • 确定内表类型
  • 评估是否需要分批处理
  • 确认数据库特性(HANA/Oracle)

开发中检查

数据库访问

  • 避免 SELECT *
  • 使用合适的索引
  • FOR ALL ENTRIES IN 前检查内表
  • 避免循环中执行 SQL
  • 使用子查询替代嵌套查询

内表操作

  • 选择合适的内表类型
  • READ TABLE 使用 BINARY SEARCH
  • 使用 FIELD-SYMBOLS
  • 批量操作替代逐行操作
  • 避免不必要的数据复制

字符串操作

  • 使用模板字符串
  • 循环中先收集再合并
  • 使用 SPLIT 替代手动解析

算法设计

  • 避免不必要的嵌套循环
  • 使用哈希表优化查找
  • 减少数据传输量
  • 考虑计算下沉

开发后检查

性能测试

  • 使用 SE30 分析执行时间
  • 使用 ST05 分析 SQL 执行
  • 测试大数据量场景
  • 验证内存使用情况

代码审查

  • 通过 SCI 检查
  • 同行代码评审
  • 文档更新

性能基线

数据量 目标响应时间 技术建议
< 1,000 < 1 秒 标准 ALV
1,000 - 10,000 < 5 秒 优化 SQL + 哈希表
10,000 - 100,000 < 30 秒 分批处理 + CDS
100,000 - 1,000,000 < 2 分钟 并行处理 + AMDP
> 1,000,000 < 5 分钟 HANA 优化 + 分页

总结

ABAP 程序效率优化是一个系统工程,需要从多个维度综合考虑:

优化层次

+---------------------------+
| 业务层面优化 | ← 最重要的第一步
+---------------------------+
| 数据库访问优化 | ← 减少数据传输
+---------------------------+
| 内表/内存优化 | ← 减少资源消耗
+---------------------------+
| 算法/并行优化 | ← 提升处理效率
+---------------------------+
| 工具/监控优化 | ← 持续改进
+---------------------------+

核心原则

  1. 业务先行:方案设计比代码优化更重要
  2. 数据库为王:80% 的性能问题源于数据库访问
  3. 用对工具:善用 SE30/ST05/SCI 等分析工具
  4. 持续优化:性能优化是持续的过程,不是一次性的任务

学习路径

  1. 入门:掌握基本的 SQL 优化和内表操作
  2. 进阶:学习 HANA 特性和 CDS/AMDP
  3. 高级:掌握并行处理和性能分析工具
  4. 专家:能独立完成复杂系统的性能优化

记住:基础不对,努力白费。在开始优化之前,先确保方案设计是合理的。然后从数据库访问、内表操作、算法设计三个维度逐步优化,最终达到理想的性能目标。