ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SAP HANA存储过程开发:选型、语法与性能调优实战

SAP HANA存储过程开发:选型、语法与性能调优实战 做 SAP Hana 项目这十来年被问得最多的一类问题就是CDS View 和 AMDP 都摆在那儿了为什么还要回头写存储过程这个问题背后其实藏着一层误解——很多人把 Hana 存储过程当成 MySQL 或 Oracle 里那种把一堆 SQL 装进一个盒子的存储过程来对待结果写出来的东西能跑但跑得慢、改起来疼、上线后连自己都不敢动。Hana 的存储过程SQLScript Procedure本质上是跑在数据库计算引擎里的一段数据流描述它的编译、优化、执行和传统数据库完全是两套逻辑语法看着像 SQL骨子里是列式存储加并行执行的思维。这篇东西写给三类人看一类是从 ABAP 转过来、第一次在 Hana 上写 Procedure 的开发一类是手里已经有几百个老 Procedure、正在做 S/4Hana 迁移或者 Hana 2.0 升级的维护者还有一类是做 FICO、SD 这些模块顾问平时靠 CDS 取数遇到复杂对账、多表勾稽、跨年度汇总时不得不自己下到数据库层写逻辑的人。我会把环境选型、语法细节、性能调优、报错排查、部署传输这些环节都拆开讲尽量给出能直接抄的参数和代码也会把踩过的坑原样摆出来。1. 先想清楚Hana 存储过程到底解决什么问题1.1 它跑在哪儿和传统数据库存储过程的本质差别Hana 的存储过程是用 SQLScript 写的编译后交给 Calculation Engine 执行。这句话听上去像官方文档但真正影响写代码的地方在于它面对的数据是列式存储的一张几亿行的表做聚合走的是按列扫描加向量化运算而不是按行读。这就意味着你写SELECT SUM(AMT) FROM T WHERE BUKRS 1000的时候引擎考虑的不是怎么少读几行而是能不能把这列整块压到 CPU cache 里并行算。传统数据库里存储过程经常被用来减少网络往返——把十次查询合成一次调用。这个动机在 Hana 里依然成立但重要性下降了因为 Hana 的单次查询本身就足够快。真正让存储过程在 Hana 上不可替代的场景是那种中间结果需要被反复引用、且每一步的形态都得自己控制的复杂数据加工。CDS View 擅长声明式的、层级清晰的建模但一旦遇到根据上一步结果动态决定下一步取哪张表需要中间落临时表做多次迭代要做逐行的状态机判断声明式的表达就会变得极其别扭这时候 Procedure 才是顺手的工具。还有一点容易被忽略Procedure 是过程式的可以有变量、有分支、有循环、有异常捕获。CDS 和 AMDP 在这一点上都受限AMDP 虽然也是 SQLScript但它挂在 ABAP 类的方法上参数传递和调试链路要绕一圈。当你需要一段逻辑既能被 ABAP 调用、又能被数据库作业比如 Scheduled Job直接调度时独立的 Procedure 是最省事的选择。1.2 四类实现方式横向对比什么时候轮得到存储过程选型这件事写代码之前必须先做完。我见过太多项目是先动手写了三百行 Procedure写到一半发现其实用 CDS 加个union就完事了然后推倒重来。下面这张表是我自己整理的一个判断依据按实际项目经验填的不是抄文档。实现方式适合的场景不适合的场景调用入口CDS View含参数化视图维度建模、报表取数、给前端 OData 供数、逻辑层级清晰需要过程式分支、多次中间落表、动态表名ABAP Open SQL、OData、分析工具AMDPABAP 托管数据库过程逻辑需要 ABAP 类型系统参与、需要和 ABAP 侧共享结构、需要 CDS 表函数需要被非 ABAP 侧调用、需要独立调度ABAP 方法独立 SQLScript Procedure复杂对账、批量加工、数据迁移、动态 SQL、需要被 Job 调度纯粹的报表取数、简单的多表关联CALL 语句、Application Function、Job应用层处理ABAP 内表循环数据量小、逻辑依赖大量 ABAP 类库数据量超过几十万行、需要多次数据库往返应用服务器判断的核心就一句话如果这段逻辑用声明式的 SQL 表达出来会让读的人皱眉就考虑 Procedure如果用了 Procedure 之后大部分代码都在做简单的SELECT ... JOIN那就退回去用 CDS。还有一个现实约束是权限和部署。CDS View 走 HDI 或 ABAP 传输体系管控比较严Procedure 在经典 Repository 时代可以单独建包、单独传灵活但容易失控。做 S/4Hana 的项目如果是写在 ABAP 侧的走 AMDP 或者 CDS 表函数跟着 ABAP 传输请求走省心如果是纯数据库侧给数据仓库供数的独立 Procedure 更合适。1.3 一个反直觉的结论Procedure 写得越薄越好很多人以为存储过程就该厚把几百行逻辑全塞进去理由是减少调用次数。这个思路在 Hana 上其实是错的。我做过一次压测同一段加工逻辑一种写法是一个巨型 Procedure 一把梭另一种是拆成三层底层一个只做数据清洗的 Procedure中间一个做聚合的顶层一个做输出的。拆开之后总耗时反而低了大约 18%原因是每一层的结果集形态更清晰优化器能给每一步选出更合适的连接顺序而巨型 Procedure 里表变量一层套一层优化器经常在中途看不清数据规模选错连接方式。所以我的习惯是把 Procedure 当成有控制流的管道每一段只做一件事中间结果该落临时表就落临时表别心疼那点建表开销。这也直接影响到后面要讲的调试——出了问题时你能一眼看出是哪一层的数据不对而不是对着一个 800 行的巨物发愁。2. 开发环境与对象选型别一上来就写 CREATE PROCEDURE2.1 工具链从 HANA Studio 到 Business Application StudioHana 1.0 时代的标配是 Eclipse 装 SAP HANA Tools 插件大家口头都叫它 HANA Studio。2.0 之后这条线还在但新项目越来越多转到 SAP Business Application StudioBAS里的 HANA 开发空间配合 HDI 容器做开发。两者的差别不只是界面而是整个项目管理方式HANA Studio 面对的是 RepositoryXS Classic 那套.procedure文件加 Delivery Unit 传输BAS 面对的是 HDI代码就是文件走 Git 和 CI/CD。选哪个不是纯技术问题。如果你们公司还在用 SAP_BASIS 那套传输体系数据库对象挂在 ABAP 包下面那老老实实用 HANA Studio 加 ABAP 传输请求别折腾 HDI否则上线流程会变成灾难。如果是新建的数据中台、独立 Hana 实例没有 ABAP 侧那 HDI 是更现代的选择src/目录下放.hdbprocedure文件db/目录执行sap/hdi-deploy就能部署改动有版本记录回滚也容易。我的实操心得是**在一台机器上同时装 HANA Studio 和浏览器开 BAS两个都留着。**理由很实在——HANA Studio 的 SQLScript 编辑器虽然老旧但它的 PlanViz 集成和调试器Debugger到目前为止还是最顺手的尤其是存储过程里断点单步看表变量内容BAS 那边体验差一截。而 BAS 的 Git 集成和终端工具又比 HANA Studio 强得多。分工用互相补。不管用哪个有几个连接层面的参数必须提前确认不然写到一半会莫名其妙报错连接的数据库用户是否有目标 schema 的CREATE PROCEDURE权限而不是只有SELECT。开发库和测试库的 Hana 版本是否一致1.0 SPS12 和 2.0 SPS05 在语法支持上差别很大。会话的默认 schema 是否设置正确。很多人用ALTER SESSION SET CURRENT_SCHEMA ZDEV;来偷懒结果部署到生产时忘了设一堆对象找不到。2.2 Procedure、Function、Table Function、AMDP 的对象边界Hana 里存储过程这个词其实被泛化了实际开发中会碰到四类对象各自的用途和限制差别不小。Procedure存储过程用CREATE PROCEDURE创建可以有IN、OUT、INOUT参数参数既可以是标量也可以是表类型。它允许读写数据是最灵活的一类。缺点是默认不能出现在SELECT的FROM子句里只能靠CALL调用。Scalar Function标量函数接收标量参数返回单个值可以在 SQL 语句里当成普通函数用比如SELECT ZDEV.F_GET_RATE(BUKRS, CURTP) FROM DUMMY。适合做汇率取值、配置读取这类高频小逻辑。注意它的执行是按行触发的不要在里面写聚合查询否则会有性能问题。Table Function表函数返回一张表可以直接放在FROM后面用比如SELECT * FROM ZDEV.F_OPEN_ITEMS(1000, 2025)。这是把 Procedure 逻辑暴露给 CDS 的标准做法S/4Hana 里很多标准 CDS 表函数就是这一层。它只能是只读的不能做 DML。AMDP挂在 ABAP 类方法上用BY DATABASE PROCEDURE FOR HDB LANGUAGE SQLSCRIPT声明。它的优势是参数用 ABAP 类型系统定义能直接和 ABAP 内表、结构互转还支持READ-ONLY和CDS SESSION CLIENT DEPENDENT这类选项来控制客户端依赖。做 S/4Hana 扩展开发只要调用方是 ABAP我优先选 AMDP因为传输、调试、版本管理全都跟 ABAP 走团队协作成本最低。这里要特别提醒一句**不要在同一个业务逻辑上同时建 Procedure 和 Table Function 两套实现。**我接手过一个系统同一个对账逻辑有四个版本一个 Procedure、一个 Table Function、一个 AMDP、还有一个直接写在 ABAP 里后来改业务规则时漏改了其中一个对账结果差了七百多万查了三天。选一套用到底。2.3 包结构与命名规范给三年后的自己留条路Hana Repository 的包Package结构在经典模式下是要自己设计的很多项目随手建了个zdev就往下塞两年后打开一看三百多个对象堆在一起谁也不敢删。我的做法是按来源系统 业务域 对象类型三层来分比如zdev/ fi/ 财务域 proc/ 存储过程 func/ 函数 type/ 自定义表类型 sd/ 销售域 proc/ func/命名上我坚持几个规则这些规则看起来啰嗦但维护期的收益很大。存储过程统一PROC_前缀表函数F_标量函数SF_自定义表类型TT_。名字里带上业务含义和粒度比如PROC_FI_AR_RECON_BUKRS表示按公司代码粒度的应收对账而不是PROC_RECON_1。中间结果用的局部临时表统一#开头并且加上业务前缀比如#TMP_BSIS避免和别人的临时表重名。版本演进用后缀或者参数控制不要直接改老对象的签名。给OUT参数加字段是破坏性变更调用方会全部报错。有一个坑值得单独说Hana 的对象名如果在创建时加了双引号就会大小写敏感之后所有引用都必须原样带双引号。有些老项目建对象时随手写了Zdev.Proc_Recon结果在 ABAP 侧调用时写成大写就找不到。统一规则所有数据库对象名一律用大写创建时不加双引号或者加了也全大写能省掉大量排查时间。3. SQLScript 核心语法细节拆解3.1 表变量与标量变量最容易被忽视的坑SQLScript 里有两套变量标量变量和表变量。标量变量用DECLARE lv_name NVARCHAR(50);声明赋值用:或者SELECT ... INTO lv_name表变量用DECLARE lt_tab TABLE (...)声明或者直接用lt_tab SELECT ...隐式声明。新手最容易栽的地方是引用表变量时忘了加冒号。正确写法是SELECT * FROM :lt_tab那个冒号是必须的。忘了写编译时不一定报错但运行时会报 invalid table name: Could not find table/view LT_TAB然后你会盯着一个明明存在的表变量怀疑人生。我在团队里定了个死规矩凡是从表变量取数先写冒号再写名字形成肌肉记忆。第二个坑是表变量没有统计信息。优化器不知道lt_tab里是 10 行还是 1000 万行只能按默认估计来选连接顺序。这在多层嵌套时会直接导致执行计划跑偏。解决办法有两个一是把中间结果落到局部临时表CREATE LOCAL TEMPORARY TABLE #tmp (...)临时表会做数据统计优化器能拿到行数估计二是在关键查询上加提示WITH HINT (NO_INLINE)阻止内联或者在确定数据量小的场景下加WITH HINT (INLINE)让优化器把表变量展开。-- 表变量轻量适合小结果集 lt_small SELECT BUKRS, BELNR FROM BKPF WHERE GJAHR :iv_gjahr; -- 局部临时表重一点但有统计信息适合大结果集或多次引用 CREATE LOCAL TEMPORARY TABLE #tmp_bsis LIKE BSIS; INSERT INTO #tmp_bsis SELECT * FROM BSIS WHERE BUKRS :iv_bukrs;第三个坑是作用域。表变量在BEGIN ... END块里声明出了块就失效。嵌套块里同名变量会遮蔽外层变量而且不会有任何警告。我曾经因为一个同名变量让整个求和结果只覆盖了最后一个分组对账差异查了一整天。规避方法变量命名带业务前缀不允许出现重复名字这个规则比什么注释都管用。3.2 CE 函数还是 SQL 语法混合写法的取舍Hana 1.0 早期SQLScript 的主力是 CE 函数就是那些CE_COLUMN_TABLE、CE_JOIN、CE_OLAP_VIEW之类的东西。它们直接操作计算引擎的算子粒度细、可控性强写出来的东西像数据流图。后来 SQL 引擎和计算引擎统一了标准 SQL 语法成了推荐写法CE 函数虽然还能用但官方已经标记为过时。我的建议分两种情况。新写的代码一律用 SQL 语法理由很直接可读性好、团队成员都能看懂、优化器的自由度更高、未来版本一定会继续支持。维护老代码时不要顺手改如果那段 CE 逻辑跑得好好的没有性能问题就别动它。CE 函数改写成 SQL 语法不是机械替换两者的优化路径不同改完性能变差的案例我见过不止一次。如果确实遇到需要使用 CE 函数的场景通常是这两类一是需要用到某些 SQL 语法还没覆盖的算子比如特定的窗口计算二是在做极端性能调优需要手工控制连接算法。这种时候用就用了但要在代码里写清楚注释说明为什么不得不用 CE以及当时测试的数据规模。-- 老写法CE 函数保留但不再新增 lt_bkpf CE_COLUMN_TABLE(SAPABAP1.BKPF:#MANDT, #BUKRS, #BELNR, #GJAHR, #BUDAT); lt_bsis CE_COLUMN_TABLE(SAPABAP1.BSIS:#MANDT, #BUKRS, #BELNR, #GJAHR, #DMBTR); lt_join CE_JOIN(:lt_bkpf, :lt_bsis, [BUKRS, BELNR, GJAHR]); -- 新写法SQL 语法推荐 lt_join SELECT a.BUKRS, a.BELNR, a.GJAHR, a.BUDAT, b.DMBTR FROM SAPABAP1.BKPF AS a INNER JOIN SAPABAP1.BSIS AS b ON a.MANDT b.MANDT AND a.BUKRS b.BUKRS AND a.BELNR b.BELNR AND a.GJAHR b.GJAHR WHERE a.BUKRS :iv_bukrs AND a.GJAHR :iv_gjahr;有一个现实问题要面对SAP 标准自带的很多 Procedure 还是 CE 风格你在里面加代码时会被迫跟着它的风格走。这时候的策略是新增的查询块用 SQL 语法块与块之间通过表变量传递不要去改它原有的 CE 部分。这样既保持了一致性又不会引入未知风险。3.3 游标与循环性能分水岭游标循环是 SQLScript 里最容易写出性能问题的地方没有之一。Hana 是列式并行引擎天生适合集合操作而游标是逐行的本质上把并行能力废掉了。我做过一次对比测试对 200 万行数据做条件更新用一次UPDATE ... WHERE集合操作耗时 2.3 秒用游标逐行更新耗时 47 分钟。差了三个数量级。但游标不是完全不能用有几类场景确实需要它需要根据上一行的处理结果决定下一行怎么处理也就是有状态依赖的。需要对每一行调用一个外部过程或者写审计日志。需要动态决定表名或者执行语句的。即使在这些场景里我也会先把数据规模压到最小。比如先把符合条件的行筛到一张临时表再对这张临时表开游标数量可能从百万级降到几百级。-- 不推荐直接在大表上开游标 DECLARE CURSOR c_all FOR SELECT BELNR FROM BKPF WHERE GJAHR :iv_gjahr; FOR r AS c_all DO -- 每行做一次操作 END FOR; -- 推荐先筛选再循环 CREATE LOCAL TEMPORARY TABLE #tmp_todo AS (SELECT BELNR FROM BKPF WHERE GJAHR :iv_gjahr AND STBLG AND BSTAT ); DECLARE CURSOR c_todo FOR SELECT BELNR FROM #tmp_todo; FOR r AS c_todo DO -- 只处理必要的行 END FOR;还有一个语法糖值得用FOR r AS SELECT ... DO ... END FOR可以省掉DECLARE CURSOR那一步直接循环一个查询结果。它在内部实现上和游标基本一样但代码更短。用的时候注意循环体里如果对同一张表做修改可能会影响循环本身的行为所以最好先把数据取到临时表再循环。一句经验之谈写完一个 Procedure回头数一数里面有几个FOR ... DO。超过两个就该重新想想能不能改成集合操作了。3.4 异常处理EXIT HANDLER 与错误码存储过程跑在数据库里出错时如果直接把原始错误抛给前端调用方拿到的信息基本没法用。SQLScript 提供了DECLARE EXIT HANDLER FOR SQLEXCEPTION来捕获异常配合::SQL_ERROR_CODE和::SQL_ERROR_MESSAGE这两个系统变量可以拿到具体的错误码和消息。CREATE PROCEDURE ZDEV.PROC_DEMO (IN iv_gjahr NVARCHAR(4), OUT et_result TABLE (...)) LANGUAGE SQLSCRIPT SQL SECURITY INVOKER AS BEGIN DECLARE lv_code NVARCHAR(10); DECLARE lv_message NVARCHAR(1000); DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN SELECT ::SQL_ERROR_CODE, ::SQL_ERROR_MESSAGE INTO lv_code, lv_message FROM DUMMY; -- 把错误写入自定义日志表保留现场 INSERT INTO ZDEV.ZLOG_ERROR VALUES (CURRENT_TIMESTAMP, PROC_DEMO, :lv_code, :lv_message); -- 重新抛出让调用方知道失败了 RESIGNAL; END; -- 主体逻辑 et_result SELECT ... FROM ...; END;EXIT HANDLER的意思是执行到错误就退出当前块跳进处理程序处理完如果RESIGNAL就把错误继续往上抛。还有CONTINUE HANDLER遇到错误继续往下走一般只在特定场景用比如批量导入时跳过坏数据。我基本不用CONTINUE因为静默跳过的错误最难查。错误处理里有个细节要注意如果 Procedure 声明成SQL SECURITY DEFINER错误消息里可能包含定义者的对象名信息对外暴露时要判断是否需要脱敏。另外日志表写入本身也可能失败比如表满了或者权限不够所以处理程序里最好再包一层判断别让错误处理自己变成新的错误来源。3.5 SQL SECURITY 与权限模型创建 Procedure 时的SQL SECURITY选项决定了执行时用谁的权限去访问底层对象这个选项不写清楚会踩大坑。SQL SECURITY INVOKER默认表示用调用者的权限执行。好处是权限边界清晰谁调用谁负责。缺点是每个调用者都必须拥有 Procedure 里涉及的所有底层表的权限管理起来麻烦。SQL SECURITY DEFINER表示用定义者的权限执行。好处是调用者只需要有 Procedure 的执行权限不用管底层表。适合把复杂逻辑封装起来不给别人看细节的场景。风险是权限放大如果 Procedure 里写了动态 SQL而参数又是外部传进来的就可能被人利用来做未授权的数据访问。-- 只读逻辑用 INVOKER权限清晰 CREATE PROCEDURE ZDEV.PROC_QUERY (...) LANGUAGE SQLSCRIPT SQL SECURITY INVOKER READS SQL DATA AS BEGIN ... END; -- 需要写数据、且调用者不该有底层表权限用 DEFINER CREATE PROCEDURE ZDEV.PROC_UPDATE LANGUAGE SQLSCRIPT SQL SECURITY DEFINER AS BEGIN ... END;READS SQL DATA这个子句建议明确写上它声明了这个 Procedure 只读优化器能做一些额外的假设而且对调用方来说是个清晰的安全信号。如果里面确实要做INSERT、UPDATE、DELETE就别写这一句让它默认成可写。我见过有人复制粘贴的时候把READS SQL DATA一起带过来了然后在里面做更新编译期不报错运行时直接失败白折腾半天。授权方面Procedure 的执行权限是单独授的GRANT EXECUTE ON ZDEV.PROC_DEMO TO USER_A;。如果 Procedure 用了自定义表类型作为参数还要把那个类型的权限一起授出去这个很容易漏。4. 完整案例一张对账存储过程从需求到落地4.1 需求拆解与数据结构假设一个典型需求财务要对某公司代码某年度的应收明细做勾稽把总账ACDOCA 或者老系统的 BKPF/BSIS和子 ledger 的明细做比对找出金额不一致的单据输出差异清单。数据量假设是单公司代码单年度 50 万行级别。需求拆开来是这样几步从总账取指定公司代码、指定年度的应收类科目行项目。从子 ledger 取对应的行项目。按公司代码、会计凭证号、行项目号做匹配。匹配上的比对金额不匹配的也输出。输出结果带上差异类型标记方便前端分类。数据结构上主键组合是BUKRS BELNR GJAHR BUZEI金额字段用DECIMAL(23,2)差异类型用一个两位字符编码。这些细节看着琐碎但提前定死能避免后面返工。4.2 参数设计与主体实现参数设计上我坚持输入尽量窄、输出尽量全的原则。输入给公司代码、年度、可选的科目范围输出给完整的结果集加上汇总信息。CREATE PROCEDURE ZDEV.PROC_FI_AR_RECON ( IN iv_bukrs NVARCHAR(4), IN iv_gjahr NVARCHAR(4), IN iv_hkont_fr NVARCHAR(10), IN iv_hkont_to NVARCHAR(10), OUT et_detail TABLE ( BUKRS NVARCHAR(4), BELNR NVARCHAR(10), GJAHR NVARCHAR(4), BUZEI NVARCHAR(3), HKONT NVARCHAR(10), GL_AMT DECIMAL(23,2), SL_AMT DECIMAL(23,2), DIFF DECIMAL(23,2), RSTAT NVARCHAR(2) ), OUT ev_count INTEGER ) LANGUAGE SQLSCRIPT SQL SECURITY INVOKER READS SQL DATA AS BEGIN -- 第一层取总账侧数据 lt_gl SELECT BUKRS, BELNR, GJAHR, BUZEI, HKONT, DMBTR AS GL_AMT FROM SAPABAP1.ACDOCA WHERE MANDT SESSION_CONTEXT(CLIENT) AND BUKRS :iv_bukrs AND GJAHR :iv_gjahr AND HKONT BETWEEN :iv_hkont_fr AND :iv_hkont_to AND KOART D; -- 第二层取子账侧数据 lt_sl SELECT BUKRS, BELNR, GJAHR, BUZEI, HKONT, DMBTR AS SL_AMT FROM SAPABAP1.BSID WHERE MANDT SESSION_CONTEXT(CLIENT) AND BUKRS :iv_bukrs AND GJAHR :iv_gjahr AND HKONT BETWEEN :iv_hkont_fr AND :iv_hkont_to; -- 第三层全外连接两侧都要 et_detail SELECT COALESCE(g.BUKRS, s.BUKRS) AS BUKRS, COALESCE(g.BELNR, s.BELNR) AS BELNR, COALESCE(g.GJAHR, s.GJAHR) AS GJAHR, COALESCE(g.BUZEI, s.BUZEI) AS BUZEI, COALESCE(g.HKONT, s.HKONT) AS HKONT, IFNULL(g.GL_AMT, 0) AS GL_AMT, IFNULL(s.SL_AMT, 0) AS SL_AMT, IFNULL(g.GL_AMT, 0) - IFNULL(s.SL_AMT, 0) AS DIFF, CASE WHEN g.BELNR IS NULL THEN SL WHEN s.BELNR IS NULL THEN GL WHEN IFNULL(g.GL_AMT,0) IFNULL(s.SL_AMT,0) THEN DF ELSE OK END AS RSTAT FROM :lt_gl AS g FULL OUTER JOIN :lt_sl AS s ON g.BUKRS s.BUKRS AND g.BELNR s.BELNR AND g.GJAHR s.GJAHR AND g.BUZEI s.BUZEI; SELECT COUNT(*) INTO ev_count FROM :et_detail WHERE RSTAT OK; END;这段代码里有几个值得说的点。SESSION_CONTEXT(CLIENT)取的是当前会话的客户端在 S/4Hana 里比硬编码MANDT 100稳妥得多尤其是多客户端环境下。COALESCE和IFNULL在 Hana 里都能用IFNULL是 Hana 特有的COALESCE是标准 SQL我一般统一用COALESCE方便日后迁移。FULL OUTER JOIN是这段逻辑的核心用LEFT JOIN加UNION也能实现但代码会长一倍。4.3 动态过滤与 SQL 注入防护上面的实现里科目范围是用BETWEEN硬写在 SQL 里的这是最安全的写法。但实际项目里经常遇到用户想自己拼过滤条件的需求比如前端传过来一个类似HKONT 1122010000 AND BUDAT 20250101的字符串。这时候直接拼进 SQL 就是典型的注入漏洞。Hana 提供了APPLY_FILTER来处理这种场景-- 先取全集再用 APPLY_FILTER 动态过滤 lt_all SELECT BUKRS, BELNR, GJAHR, HKONT, DMBTR FROM SAPABAP1.ACDOCA WHERE BUKRS :iv_bukrs AND GJAHR :iv_gjahr; lt_filtered APPLY_FILTER(:lt_all, :iv_dynamic_filter);APPLY_FILTER的第二个参数会被当成过滤条件解析语法错误会直接抛异常不会执行危险语句。它比字符串拼接安全得多。但要注意它的限制不能用于所有场景某些复杂表达式不支持而且在数据量很大的时候先取全集再过滤的性能不如把条件下推。所以我的做法是能下推的条件一律写死在 SQL 里只有真正动态的部分才交给APPLY_FILTER。如果确实需要用EXECUTE IMMEDIATE执行动态 SQL那么在 Hana 2.0 里一定要用USING子句绑定参数-- 危险直接拼接 EXECUTE IMMEDIATE SELECT * FROM T WHERE BUKRS || :iv_bukrs || ; -- 安全参数绑定 EXECUTE IMMEDIATE SELECT * FROM T WHERE BUKRS ? USING :iv_bukrs;4.4 结果返回与调用方式存储过程的结果通过OUT参数返回。表类型的OUT参数有两种定义方式老版本要用CREATE TYPE单独定义表类型然后在参数里引用类型名Hana 2.0 支持直接内联定义像上面例子那样写OUT et_detail TABLE (...)就行。新项目建议用内联少建一个对象少一份维护成本。调用方式取决于调用方。在 SQL Console 里测试CALL ZDEV.PROC_FI_AR_RECON( IV_BUKRS 1000, IV_GJAHR 2025, IV_HKONT_FR 1122010000, IV_HKONT_TO 1122999999, ET_DETAIL ?, EV_COUNT ? );在 ABAP 里调用用CALL DATABASE PROCEDURE参数按位置或者按名字绑定。注意 ABAP 侧的内表结构和 Hana 的表类型要一一对应字段类型和顺序都不能错否则会报 inconsistent datatype。我踩过一次DECIMAL(23,2)在 ABAP 侧写成了DEC(15,2)编译期没报错运行到数据量大的时候才崩查了半天。5. 性能调优从 PlanViz 到一条慢查询的排查5.1 打开 SQLScript Plan Profiling 的正确姿势性能调优第一步是拿到执行计划。Hana Studio 里的做法是打开存储过程的编辑器在工具栏上找到 SQLScript Plan Profiling 的入口指定一个 trace 文件路径然后执行CALL触发一次运行运行结束后用 PlanViz 打开生成的.plv文件。PlanViz 的界面信息量很大刚开始看容易懵。我的建议是先看三个东西总耗时排前五的算子、每一层的实际行数和估计行数的偏差、有没有出现全表扫描。实际行数和估计行数差一个数量级以上的地方基本就是问题所在因为优化器是基于估计值选算法的。反过来说如果你看到某一步估计 1000 行、实际 500 万行那说明统计信息过期了对该表执行UPDATE STATISTICS或者让它自动收集一次。Hana 2.0 里还有一个轻量级的做法直接在 SQL Console 里对单条语句按CtrlShiftE看 Explain Plan。它没有实际运行的统计信息但胜在快改完一条 SQL 马上就能看计划变没变。调优的时候我是两个交替用Explain Plan 快速试PlanViz 最终确认。5.2 五类最常见的性能杀手调优这事与其讲方法论不如把最常见的坑列出来。下面这五类是我在几十个项目里反复遇到的。问题类型典型表现排查方法修复方式表变量嵌套太深PlanViz 里某一步行数估计严重偏低连接顺序奇怪看估计行数 vs 实际行数偏差改局部临时表或加WITH HINT (NO_INLINE)游标逐行处理总耗时集中在某个循环块看 PlanViz 里同一步被反复执行改集合操作或先筛选再循环统计信息过期同一条 SQL 昨天快今天慢对比估计行数和实际行数收集统计信息隐式类型转换带条件的查询无法走分区裁剪检查字段类型和参数类型是否一致参数类型和字段类型对齐结果集过大网络传输和内存占用高看返回行数和结果集大小分页、加过滤条件、只取必要列隐式类型转换这条特别容易被忽略。如果BUKRS字段是NVARCHAR(4)而传进来的参数是VARCHARHana 会做一次转换这个转换可能导致分区裁剪失效、索引失效甚至无法走列存优化。我养成的习惯是在 Procedure 的参数声明里类型和底层字段完全一致宁可多写几个字符。5.3 分区裁剪与并行度控制大表在 Hana 里通常按某个键做了分区比如按GJAHR或者BUDAT做范围分区。查询的时候如果条件能命中分区键引擎只会扫相关分区耗时可能相差十倍以上。所以写 SQL 时能带上分区键条件的一定要带上哪怕业务上不需要。-- 分区裁剪生效带上了 GJAHR SELECT ... FROM ACDOCA WHERE BUKRS :iv_bukrs AND GJAHR :iv_gjahr; -- 分区裁剪失效只带 BUKRS可能扫全部分区 SELECT ... FROM ACDOCA WHERE BUKRS :iv_bukrs;并行度方面Hana 会自动根据数据规模和系统资源决定并行度一般不用手动干预。但在存储过程里如果你发现某个步骤成了串行瓶颈可以检查一下是不是因为表变量导致引擎无法并行化。用临时表通常能恢复并行能力。另外WITH HINT (MAX_CONCURRENCY(n))可以强制指定并行度但这是个双刃剑设得太高会和其他会话抢资源我一般只在批处理作业里用而且要配合资源组限制。6. 常见报错与排查速查表6.1 编译期报错编译期报错相对好排查因为位置明确。但有几类报错的消息特别模糊值得单独拎出来。invalid table name: Could not find table/view XXX九成是三种情况——对象名大小写不对、表变量忘了加冒号、当前 schema 不对。排查顺序就是先确认 schema再看名字有没有加引号最后检查表变量引用。Column not found: XXX除了拼写问题还有一个隐晦的原因是SELECT *之后引用了不存在的列。比如从表变量里取数表变量里根本没有这一列但因为是SELECT *展开的报错位置会指到很远的地方。所以我坚持所有 SQL 都显式列出字段不用SELECT *。inconsistent datatype参数类型和字段类型不匹配。常见于DECIMAL精度不一致、NVARCHAR长度不够、INTEGER和BIGINT混用。Hana 2.0 编译时可能不报运行时才炸。6.2 运行期报错运行期报错才是真正费时间的因为数据量、并发、会话状态都可能影响。下面这张表是我整理的常客。错误信息关键词含义处理方式transaction is rolled back事务被回滚通常是 DML 出错或超时检查日志表、确认是否触发了资源限制lock wait timeout锁等待超时检查并发写同一张表的情况考虑改批处理顺序memory allocation failed内存不足检查是否有超大结果集、是否需要用分页处理division by zero除零加NULLIF或者CASE判断numeric overflow数值溢出检查中间计算的精度是否够特别是乘法和累加invalid number of parameters调用参数个数不匹配检查CALL的参数列表和 Procedure 定义除零和溢出这两类特别有欺骗性因为在测试环境小数据量下不会触发上生产才爆。我的做法是所有除法运算都用NULLIF包一层所有累加字段用足够的精度宁可多占点空间。6.3 调试手段与日志Hana Studio 的存储过程调试器是我最常用的工具。在 Procedure 上右键选择 Debug设置断点然后触发调用可以单步执行并实时查看每个变量的值。表变量也能看它会显示成一个表格能看行数和前若干行。这在排查数据在哪一步变错的时候效率极高。但调试器有个限制它只适合开发环境小数据量场景。生产环境不能开调试那就要靠日志。我的习惯是在关键的几个步骤后面插入日志写入把中间表的行数和关键汇总值记下来INSERT INTO ZDEV.ZLOG_STEP VALUES (CURRENT_TIMESTAMP, PROC_FI_AR_RECON, STEP_1_GL, :lv_gl_count);这个日志写入会增加一点开销所以只在开发测试阶段打开用参数控制上生产时关掉。有人喜欢用TRACE语句效果类似但 trace 文件的读取没有查表方便我更喜欢自己建日志表。7. 部署、传输与版本管理7.1 XS Classic 与 HDI 两条路部署方式取决于你的开发环境是经典 Repository 还是 HDI。经典模式下Procedure 保存在 Repository 的.procedure文件里通过 Delivery UnitDU或者 CTS 传输。DU 传输的问题是它只能整体传一个包包里的对象会全部覆盖不能做增量CTS 相对好一点能跟着 ABAP 传输请求走。HDI 模式下Procedure 写成src/目录下的.hdbprocedure文件用sap/hdi-deploy部署到 HDI 容器。它比经典模式强在几点一是可以用 Git 做版本管理每次改动都有记录二是部署是幂等的重复部署同样内容不会出错三是支持 CI/CD可以接到流水线里自动跑。新项目如果没有历史包袱我强烈建议走 HDI。不管走哪条路有一条必须守住开发和测试环境的 Hana 版本要与生产一致。Hana 的版本升级经常带来语法行为的变化1.0 和 2.0 之间差别尤其大同一个 Procedure 在测试环境跑得好好的上生产报语法错误这种事故我见过好几次。7.2 版本迁移1.0 到 2.0 的语法债从 Hana 1.0 升到 2.0存储过程这块有几类变化要提前知道。第一类是 CE 函数。1.0 里大量使用的CE_系列函数在 2.0 里仍然可用但已经被标记为 obsolete官方建议迁移到 SQL 语法。到底迁不迁我的判断是**有性能问题的、或者明确知道自己未来要长期维护的迁纯稳定运行、没人动的先不迁。**大规模机械迁移风险高于收益。第二类是权限模型。2.0 对SQL SECURITY DEFINER的检查更严格原来能跑的可能要补授权。升级前建议用SYS.OBJECTS和相关权限视图扫一遍所有 DEFINER 类型的 Procedure提前把权限补齐。第三类是客户端依赖。2.0 里 CDS 表函数和 AMDP 增加了CDS SESSION CLIENT DEPENDENT选项明确声明是否依赖客户端。如果老代码里用了SESSION_CONTEXT(CLIENT)但没声明这个选项升级后行为可能变化需要逐个确认。7.3 上线后的监控闭环存储过程上线不是终点。我一般会在上线前准备好几个监控项Procedure 的平均执行时长、失败次数、单次最大内存占用。这些在 Hana 的系统视图里都能查到比如M_SQL_PLAN_CACHE可以看执行计划缓存的历史信息M_CE_CALC_SCENARIOS这类视图能看计算场景的统计。有了基线数据之后一旦某个 Procedure 的执行时长突然翻倍就能立刻发现并介入而不是等业务方来投诉。我在一个项目里就是靠这个发现了某张表的数据量增长了十倍但统计信息没更新导致执行计划跑偏重新收集统计信息后恢复了正常。8. 我自己在实操中攒下的一些体会写了这么多年 SQLScript如果只让我留几条经验我会挑这几条。**先把逻辑画出来再动手写代码。**存储过程的过程式语法很容易让人边写边想写着写着逻辑就绕进去了。我现在的习惯是先用纸把数据流画一遍每个中间结果写成一张表每张表的字段和粒度写清楚然后再对着图敲代码。这样写出来的 Procedure后面改的人基本能看懂。**不要在同一段逻辑里混用 CE 和 SQL 语法。**块内统一块间通过表变量传递这是最稳的做法。mix 太多会让优化器无从下手。**中间结果落临时表这件事别抠那点开销。**一张临时表的创建成本可能只有几毫秒但它带给优化器的统计信息能省下几秒甚至几分钟。这笔账很好算。**尽量不用SELECT *。**这条规则看起来是老生常谈但在存储过程里尤其重要。因为 Procedure 下面挂的对象经常变动底层表加了个字段SELECT *就会把新字段带进来如果下游有INSERT ... SELECT *字段数量就错了报错位置往往和真正的问题点差着十万八千里。**给每个 Procedure 写清楚它的契约。**输入什么、输出什么、依赖哪些表、有没有副作用、预期数据规模是多少。这些信息写在对象的注释里或者维护一份单独的文档。维护期的效率高低很大程度上取决于当年写的人有没有留下这些信息。最后分享一个小技巧给 Procedure 加一个调试用的IN参数比如iv_debug CHAR(1) DEFAULT N当它为 Y 的时候把每一步的中间结果都写进日志表包括行数和关键汇总值。上线时调用方传 N出问题时让运维临时改成 Y 再跑一次能省掉大量来回沟通。这个参数在多环境部署时不会引起问题因为它有默认值老调用方不用改代码。
返回列表