
我见过太多这样的 HANA 存储过程打开定义一看先是七八个游标声明接着一层 FOR 循环套一层 FOR 循环循环体里再塞几条 SELECT最后把结果一行行 INSERT 进临时表。在开发机上跑几千行数据秒回一上生产几十万行就把会话卡死调用方超时、连接被回收DBA 那边只看到一条跑了十几分钟的活动语句。写这类代码的人往往是从 ABAP 或者 Oracle 转过来的行式思维根深蒂固——在 PL/SQL 里逐行处理是常态在 HANA 里逐行处理是事故。这个X档案系列想聊的就是那些官方文档里写得很克制、但项目上一定会撞到的细节。这篇围绕 SAP HANA 存储过程开发把骨架怎么写、为什么 SQLScript 不能当 PL/SQL 用、慢在哪里、怎么查、怎么部署传输、ABAP 侧怎么调用一条线走完。刚接手 HANA 项目的开发能照着抄作业写了几年但总觉得过程跑得不对劲的人也能找到几个值得回头检查的点。1. 为什么 HANA 的存储过程不能照着 PL/SQL 的思路写1.1 计算下推才是它存在的理由要理解 SQLScript 的写法得先理解 HANA 里存储过程存在的意义。传统架构里数据库负责存和取业务逻辑放在应用服务器上。数据量到了一定规模瓶颈就出现了应用要从数据库捞几十万行上来在内存里算再把结果写回去。网络传输、结果集序列化、应用层内存占用每一环都在烧资源而且数据量翻倍时这些成本是线性甚至超线性增长的。HANA 的思路是把计算搬到数据旁边——数据不动逻辑动这就是计算下推。存储过程是实现下推最直接的手段把一段多步骤的加工逻辑整体交给数据库引擎引擎在一个执行计划里统一调度中间结果只存在于内存中的表变量或临时表不需要跨进程搬运。理解了这一层很多写法上的取舍就自动有了答案任何让你在数据库和应用之间来回搬数据的写法都是在跟这套架构对着干。所以判断一段逻辑该不该写成存储过程我会问三个问题这段逻辑处理的数据量是不是远大于输出量它是不是被多个上层应用重复调用它对延迟是不是敏感三个都是是写过程划算只要有一条是否就该重新考虑放在应用层还是用视图。1.2 SQLScript、PL/SQL、MySQL 存储过程的三张面孔很多人第一次写 SQLScript 会觉得很熟悉这不就是 PL/SQL 换个语法吗。跑通第一个 Demo 确实像但真到复杂场景两者的差异会立刻把人打回原形。SQLScript 的设计哲学是SQL 为主、过程逻辑为辅它更像是一组被拆开的 SQL 查询加上少量的控制流胶水而不是一门完整的编程语言。维度HANA SQLScriptOracle PL/SQLMySQL 存储过程语言定位SQL 优先过程逻辑少量完整过程语言过程语言 SQL表变量地位一等公民可参与查询集合类型/游标主要靠临时表优化方式整体重写、内联、并行逐段执行逐段解释/执行事务控制尽量交给调用方可自由 COMMIT可自由 COMMIT返回结果必须声明 OUT 表参数REF CURSOR直接 SELECT 即结果集调试支持有调试器需授权工具链成熟支持有限这张表里最容易吃亏的是优化方式和返回结果两行。SQLScript 的优化器会把你写的一串表变量赋值当成一个整体来看能内联就内联、能合并就合并所以你写出来的中间步骤未必真的会一步步执行反过来如果你的写法让它没法重写比如混入游标循环性能就直接塌方。而 MySQL 那边习惯的在过程里写一句 SELECT客户端就能收到结果集在 HANA 里不成立必须显式声明 OUT 表参数调用方才能拿到数据这一点我在跨数据库迁移的项目里见过不止一次翻车。1.3 哪些逻辑放进存储过程是给自己挖坑不是所有逻辑都适合下沉。我自己划的红线有这么几条都是踩过之后加上的。第一条是状态机式的复杂分支逻辑。SQLScript 有 IF、WHILE、FOR语法上什么都能写但一旦分支层级超过三层可读性和可维护性会断崖式下跌而且这类逻辑通常无法被优化器重写成集合运算性能也不会好。这种逻辑老老实实放在应用层用真正的编程语言写测试也更好写。第二条是需要和外部系统实时交互的逻辑。存储过程里没有合适的机制去调外部接口硬要用动态 SQL 或者外部程序绕最后得到的是一段没人敢改的黑盒。第三条是频繁变更的业务规则比如费率、阈值、映射表。把这类规则硬编码在过程里意味着每次调整都要走一遍传输流程业务方等不起。注意判断标准不是这段逻辑能不能写进过程而是写进去之后改动成本、排查成本、性能收益三者是不是平衡。能跑通从来不是理由。2. 一个能在生产上活下来的过程骨架2.1 参数签名、SQL SECURITY 与读写声明先看骨架。下面这个结构我用了很多年改动最多的只是中间的加工逻辑CREATE OR REPLACE PROCEDURE ZSCM.P_CALC_STOCK_AGE ( IN IV_BUKRS NVARCHAR(4), IN IV_DATE DATE, OUT ET_RESULT TABLE (MATNR NVARCHAR(40), WERKS NVARCHAR(4), AGE_DAYS INTEGER) ) LANGUAGE SQLSCRIPT SQL SECURITY INVOKER READS SQL DATA AS BEGIN DECLARE lv_date DATE : IFNULL(:IV_DATE, CURRENT_DATE); lt_stock SELECT MATNR, WERKS, SUM(MENGE) AS MENGE FROM ZSCM.STOCK_DETAIL WHERE BUKRS :IV_BUKRS GROUP BY MATNR, WERKS; ET_RESULT SELECT MATNR, WERKS, DAYS_BETWEEN(MIN_BUDAT, :lv_date) AS AGE_DAYS FROM :lt_stock; END;几个细节值得单独说。参数名加前缀IV_、EV_、ET_、IT_不是审美问题是因为 HANA 里标量变量和表变量同名冲突、参数和局部变量同名遮蔽都是真会报错的场景前缀能让这类问题在写的时候就被眼睛拦下来。参数没有默认值这一点跟很多数据库不一样别想着写IN IV_DATE DATE DEFAULT CURRENT_DATE得在过程体里用 IFNULL 之类兜底。SQL SECURITY这行建议永远显式写出来。不写会走版本默认值历史上默认是 DEFINER而 DEFINER 意味着过程以创建者的权限运行绕过调用者自己的权限检查安全上是个大开口子。READS SQL DATA这类读写声明也别省略纯读取的过程一定要标 READS标了之后引擎才能放心做优化而且带 READS 声明的过程里不允许出现事务控制语句这个限制其实是帮你守规矩。2.2 表变量、标量变量与作用域的几个硬规矩表变量是 SQLScript 区别于其他数据库过程语言的灵魂。它不是临时表更像一个命名好的查询结果用法上要记住这几条引用时必须带冒号前缀SELECT * FROM :lt_stock写漏了会当成物理表去找然后报对象不存在。列名重复会直接报错。SELECT a.MATNR, b.MATNR FROM ...这种在别的数据库可能只是列名冲突在 HANA 里是硬错误必须用别名区分。声明必须集中在 BEGIN 之后、任何可执行语句之前不能写到一半再 DECLARE。中间步骤写ORDER BY是不保证顺序的。要保证最终顺序把 ORDER BY 放在最后一步或者交给调用方处理。什么时候用表变量、什么时候用局部临时表#开头的会话级临时表我的经验分界线是数据量和复用次数。表变量适合小结果集和逻辑分段好处是不占元数据、不需要清理、优化器看得更透一旦中间结果动辄几百万行、或者要被多个步骤反复 JOIN就换临时表——它是真实的物理对象有统计信息可以建索引优化器能更准确地估算。提示较新的 HANA 2.0 版本支持对表变量直接做 INSERT/UPDATE/DELETE但很多老项目和历史环境不支持只能靠重新赋值来改写。如果你的代码要跨多套环境交付中间结果要反复改写的场景直接用临时表更稳。2.3 异常处理让调用方拿到确定的错误语义生产上的过程必须处理异常但处理方式要克制。我一般只在过程最外层包一个出口处理器DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ET_RESULT SELECT AS MATNR, AS WERKS, 0 AS AGE_DAYS WHERE 1 0; RESIGNAL; END;这里的关键是RESIGNAL。很多人的写法是捕获异常之后打个日志、返回空结果调用方拿到空结果以为没数据实际上是出错了问题就这样被吞掉几周。正确做法是把原始错误往上抛让调用方拿到真实的 SQLSTATE 和错误码同时保证输出参数是个结构完整的空集合避免调用方解析空对象时再崩一次。业务规则上的错误要不要用异常我倾向于用返回码字段而不是 SIGNAL。比如该工厂未配置库存规则这种事属于预期内的分支用 SIGNAL 抛出去会让整套调用链上的人都收到告警在输出表里带一个状态码和一列消息文本反而更好处理。只有真正不该发生的情况——比如参数为空的必填项、数据完整性被破坏——才用 SIGNAL 中断。2.4 结果集返回的两种范式HANA 过程返回数据只有一条路声明 OUT 表参数。但组织方式上我常用两种范式选择取决于数据量和调用方习惯。第一种是纯结果集范式过程只负责加工输出一张宽表逻辑全部在过程内闭环。适合报表取数、批处理任务调用方拿到就能用。第二种是状态 数据双输出范式除了数据表再加一个只有一两行的状态表或标量 OUT 参数用来回传影响行数、处理批次、跳过条数这些信息。批处理场景下第二种更实用因为出现部分成功的时候运维需要知道到底处理到了哪一步。要提醒的是一个过程声明多少个 OUT 表参数都行但每个都会在调用链上产生一次结果集反序列化的开销。如果一个过程输出了七八张表八成是设计上出了问题该拆成多个过程或者该用计算视图/ABAP CDS 来组合。3. 把行式思维改成集合式性能分水岭就在这一步3.1 一段游标循环的实测改写全过程我接手过一段库存账龄计算的过程客户反馈月初跑批要两个小时。原始逻辑简化后长这样-- 改写前典型的行式写法 DECLARE CURSOR c_mat FOR SELECT DISTINCT MATNR FROM ZSCM.STOCK_DETAIL WHERE BUKRS :IV_BUKRS; FOR row AS c_mat DO SELECT SUM(MENGE) INTO lv_menge FROM ZSCM.STOCK_DETAIL WHERE BUKRS :IV_BUKRS AND MATNR row.MATNR; INSERT INTO #tmp_result VALUES (row.MATNR, lv_menge); END FOR;问题非常典型外层取所有物料内层对每个物料扫一次明细表。物料有八万多个等于把一张两千万行的表扫了八万遍。即使有索引八万次游标推进、八万次语句调度、八万次结果集构造的开销是省不掉的——这些开销跟数据量无关纯粹是次数带来的。改写后的版本把所有逻辑压成一条聚合-- 改写后一次集合运算 lt_result SELECT MATNR, WERKS, SUM(MENGE) AS MENGE FROM ZSCM.STOCK_DETAIL WHERE BUKRS :IV_BUKRS GROUP BY MATNR, WERKS; ET_RESULT SELECT * FROM :lt_result;实测下来同样的数据量从两小时降到四十几秒。差别不在算法在于执行次数从八万次变成一次。这是我在 HANA 性能优化里反复验证过的规律绝大多数慢过程的第一根因不是SQL 写得不够好而是SQL 被执行了太多次。3.2 UNNEST、JOIN 与窗口函数替代逐行累加不是所有游标都能这么简单地压成一条聚合。有些场景是对每个元素做一件稍有不同的事这时候需要换个思路。如果循环是在处理一个参数列表比如对传入的十个工厂分别计算正确做法是用 UNNEST 把数组展开成表然后用 JOIN 把计算逻辑一次性套上去而不是循环十次。如果是逐行的累加、排名、找上一行用窗口函数OVER、PARTITION BY、ROWS BETWEEN替代变量自增的写法不但快而且逻辑更容易验证。如果是逐行判断后写入不同目标用带条件表达式的 INSERT ... SELECT 一次完成把判断条件写进 CASE 或者 WHERE 里。真正没法集合化的场景只剩下极少数需要调用另一个有副作用的存储过程、需要按顺序做带外部依赖的动作。遇到这种我的做法是把循环范围压到最小——外层集合化只对真正需要逐个处理的少数对象开循环并且把循环体里的逻辑做到最薄。3.3 表变量内联与物化时机优化器帮倒忙的时候SQLScript 优化器有一个反直觉的行为它可能把你在过程里定义的中间表变量内联掉直接展开到下游语句里。多数时候这是好事少了一次物化、少了一次扫描。但有两种情况会变成麻烦。一种是某个中间结果被多条下游语句共用内联之后等于每条下游语句都重算一遍总成本反而上升。另一种是中间结果本身有很强的过滤效果先算出来再 JOIN 能大幅减少下游数据量内联之后过滤条件被推到了后面执行顺序被改写。这两种情况需要通过执行计划确认必要时用 hint 干预内联行为。要提醒的是不同版本支持的 hint 名称和语义不完全一致动手前先在测试库上用执行计划验证效果别直接照抄网上的 hint 写法。3.4 列裁剪为什么比加索引还管用有一个几乎零成本、收益又特别明显的优化点只取需要的列。我见过太多中间步骤写着lt_tmp SELECT * FROM big_table;下游只用了其中三列。列存数据库的最大优势是按列读取SELECT *等于把这个优势直接扔掉。同样重要的还有三件事。一是确认表真的在列存里通过系统视图检查表的存储类型如果关键大表被建成了行存后面所有优化都是白费力气。二是确认分区裁剪生效分区表查询如果没带分区键会把所有分区扫一遍执行计划里能看出来。三是避免隐式类型转换参数声明成 NVARCHAR 却拿去和 DATE 字段比较可能让索引和分区裁剪同时失效这类问题在拼接字符串查询的条件下尤其常见。4. 跑得慢怎么查从执行计划到语句级监控4.1 EXPLAIN PLAN 与 PlanViz 的正确读法排查慢过程的第一步是拿到执行计划。单条语句可以直接用 EXPLAIN PLAN 看过程整体则要用计划分析工具它会生成可视化的计划树。看计划重点看三件事。第一是估算行数和实际行数的偏差。如果某个算子估算一百行、实际出来一百万行说明优化器对数据分布判断错了后面的算子选择基本都不靠谱往上找是哪张表的过滤条件没被正确识别。第二是有没有出现不该出现的算子。比如行式引擎相关的算子混进来通常意味着链路上有行存表或者有无法集合化的逻辑再比如循环、游标相关的算子说明代码里还有逐个处理的结构没被消除。第三是中间结果的大小变化。找到数据量最大的那个中间节点那通常就是优化收益最大的地方——要么提前过滤要么改变 JOIN 顺序要么把它物化下来避免重复计算。注意计划分析会生成体积不小的跟踪文件在生产系统上开之前先确认磁盘空间和影响范围用完及时关掉。这事跟改配置一样是要留下痕迹的。4.2 存储过程调试器的可用边界HANA 的调试器能单步执行、查看变量、观察表变量内容排查逻辑错误非常有效。但它有几个边界得先搞清楚不然会白折腾半天。调试需要相应权限生产环境还需要显式开启调试会话并获取调试令牌这个动作会留下审计记录动手之前最好跟负责基础架构的同事打个招呼。调试会占用独立会话本来并发就紧张的系统上要挑时间。最容易被忽略的一条是带加密选项创建的过程无法调试也无法查看定义如果代码是从别人手里接过来并且加密了只能让原团队重新提供未加密版本。调试器也不是性能工具。它能告诉你逻辑对不对但告诉不了你哪个算子慢。逻辑和性能要分开查这两件事的工具链不重合。4.3 慢因对照表与各自的处置方式下面这张表是我这些年排查积累下来的速查表出现对应现象时可以直接往根因上靠。现象常见根因处置方向小数据量秒回大数据量爆炸游标逐行循环改写为 JOIN / 集合运算执行出现行式算子链路中有行存表检查并迁移到列存中间步骤反复被扫描SELECT *无列裁剪只取必要列分区表查询很慢未带分区键全分区扫描补上分区键条件每次调用稳定地慢编译或优化耗时长看计划缓存的命中情况偶发超时并发、锁等待、内存看昂贵语句与阻塞视图只读过程也变慢参数类型不匹配导致转换统一参数与字段类型用法上有个小技巧先看现象属于哪一类再去看对应的系统监控视图。昂贵语句视图能告诉你哪条语句累计消耗最大计划缓存视图能告诉你语句是不是被重复编译这两个视图配合起来排查效率比漫无目的地开跟踪高得多。5. 部署、传输与权限代码离开开发机之后的事5.1 HDI 容器与 XS Classic 仓库的两条路线存储过程的部署方式取决于你的系统版本和架构。老一代是 XS Classic 的仓库模式过程作为源文件放在指定包路径下激活后进入系统传输通过交付单元配合变更管理系统完成。新一代是 HDI 容器模式过程是一个独立的部署文件放在容器项目里靠构建工具部署依赖关系由框架自动解析。HDI 相比老模式最大的好处是依赖自动解析和容器隔离你不用再手工维护先建表类型再建过程的顺序构建工具会算好拓扑顺序不同应用的对象在各自容器里命名冲突少了很多。代价是部署流程变了以前激活一下就能用的东西现在要走构建命令或者流水线。在 S/4HANA 侧还有个更省事的选择用 AMDP 把过程挂在 ABAP 类的方法上代码跟着 ABAP 传输走和业务对象同生命周期。这个后面单独讲。5.2 依赖顺序与重建时的对象失效不管走哪条路线有一类问题一定会遇到被依赖对象重建后依赖它的过程失效。典型场景是表类型改了字段所有引用这个表类型的过程都需要重新编译否则调用时会报错。在 HDI 模式下这个问题被框架挡住了大半在 XS Classic 模式下就得靠人记住。我的应对办法是在包里维护一份依赖清单表类型和共享函数单独建包不要在业务包之间互相引用。除此之外过程重建时如果涉及参数签名变化加参数、改参数类型、改参数顺序一定要同步通知所有调用方包括 ABAP 侧、其他过程、以及可能存在的作业调度配置。参数变更在 HANA 里不是向后兼容的调用方会直接报参数个数或类型不匹配。5.3 动态 SQL 与 SQL 注入的防线过程里难免有需要动态拼接的场景比如按传入的日期区间动态构造条件、按配置决定查哪张分表。动态 SQL 在 HANA 里通过 EXEC 或 EXECUTE IMMEDIATE 执行这里的风险点和 Web 应用里完全一样。第一道防线是用参数绑定而不是字符串拼接。新的版本支持在动态语句里用占位符绑定值能用就一定用值永远不要拼进 SQL 文本里。第二道防线是标识符白名单。表名、列名这类没法绑定的部分如果必须动态就在过程里做一次明确的合法性校验只允许配置表里存在的对象名通过绝不接受调用方传进来的任意字符串。第三道防线是拒绝把外部输入直接当对象名这一条听起来像常识但在业务系统里各种灵活配置的需求下破例的次数远比想象中多。5.4 权限设计DEFINER 该不该用回到前面提过的 SQL SECURITY。INVOKER 模式下调用者必须有访问过程内部所有对象的权限过程本身只是一段被授权的逻辑。DEFINER 模式下过程以创建者身份运行调用者只要拿到过程的执行权限就能间接访问底层数据表不需要被单独授权。DEFINER 不是不能用它的正当场景是有意的权限封装把底层明细表锁死只通过几个精心设计的过程对外提供聚合结果。但在业务系统里它经常被当成省事的手段——授权报错改成 DEFINER 就好了。代价是权限边界变得模糊谁通过哪条路径能看到哪些数据事后很难审计清楚。我自己的原则是默认 INVOKER把权限通过角色和权限对象正经授到位确实要做数据封装的时候才用 DEFINER并且把这个决定写进设计文档。另外授权的时候我倾向于把执行权限授给角色而不是用户员工的岗位一变动就改角色分配过程这边不用动。6. 从 ABAP 到 HANAAMDP 与直接调用过程的选型6.1 AMDP 适合什么样的活AMDP 的写法是把 SQLScript 直接写在 ABAP 类方法里方法声明时指明目标数据库和语言然后用 USING 列出要访问的表。它在 S/4HANA 项目里几乎是标配原因很实际它跑在与 ABAP 相同的那条数据库连接和事务里。METHOD get_stock BY DATABASE PROCEDURE FOR HDB LANGUAGE SQLSCRIPT OPTIONS READ-ONLY USING zstock_detail. et_out SELECT matnr, SUM( menge ) AS menge FROM zstock_detail WHERE bukrs :iv_bukrs GROUP BY matnr; ENDMETHOD.适合用 AMDP 的场景有共同特征数据量在 ABAP 侧处理明显吃力的聚合和连接需要在 ABAP 逻辑链中间插一段大数据量运算或者要给 ABAP CDS 视图做参数化的复杂逻辑。反过来如果这段逻辑只有几十行数据、纯粹是字段加工放 ABAP 里就行为了用 AMDP 而用 AMDP只会让代码更难读、更难调试。AMDP 有两个需要注意的限制。一是语法检查能力有限ABAP 编译期基本看不到 SQLScript 里的错误很多问题要到运行时才暴露所以写完必须实际跑一遍。二是它绑定了数据库类型虽然现在的项目大多只跑 HANA但迁移或者双数据库环境的项目要提前确认。6.2 ABAP 侧调用 HANA 过程的性能陷阱在 ABAP 里直接调数据库过程一般通过 SQL 语句执行接口来做。这里有个特别容易被忽略的点用这套接口拿到的连接可能不是 ABAP 当前那条事务连接。如果它是另开的一条连接那你实际上开了个新事务锁的范围、回滚的边界、提交的时机和 ABAP 的业务逻辑全都不在一条线上。后果是排查起来非常痛苦的现象ABAP 侧报错回滚了数据库里数据却还在或者 ABAP 侧还没提交数据库这边已经能看到中间状态。我自己的规则很简单能用 AMDP 或默认连接完成的事就不要另开连接。确实需要独立连接处理长时间作业时要在设计文档里把事务边界写清楚并且确保异常路径上有明确的清理动作。6.3 S/4HANA 场景下的取舍这几年做 S/4HANA 相关项目报表加速和期间结账是绕不开的两块。我的选型顺序通常是这样的能用 ABAP CDS 视图表达的优先用 CDS因为它在 ABAP 生态里、能被权限和传输体系完整覆盖需要多步骤中间结果、复杂聚合、参数化逻辑的用 CDS 加 AMDP 组合把复杂部分沉到 AMDP 方法里只有在需要多步骤写入、需要被非 ABAP 系统调用、或者要独立部署的时候才考虑在数据库侧单独建过程。这个顺序背后的判断逻辑是可维护性和团队能力AMDP 跟着 ABAP 传输、被 ABAP 调试工具覆盖团队里会的人多独立的数据库过程能力更强但运维和排查的成本也更高一旦写代码的人离职接手成本会成倍上升。选型的时候把这两件事放在一起算比单纯比性能数字更接近实际情况。最后分享一个我自己踩过的坑。早年我处理一个跨年度账龄计算为了少写几行代码把中间结果默认成了全部 NVARCHAR 类型想着反正后面还要转。结果一条涉及几千万行的关联因为两边类型不一致做了隐式转换执行计划里多出一大片转换算子跑了四十多分钟。把字段类型改成与源表严格一致之后同样的逻辑二十几秒出结果。从那以后我加了一条硬规矩过程里的参数类型、变量类型、目标字段类型三者必须严格对齐一个 DECIMAL 精度、一个字符长度都别将就。这类问题在开发环境的小数据量下永远看不出来只有到生产才现原形改起来却要重新走一遍传输流程。