
去年接手了一个从老系统往达梦数据库迁移的项目最头疼的不是表结构而是一大堆历史存储过程和自定义函数。团队里有人从 MySQL 过来习惯写DELIMITER //那一套有人从 Oracle 过来觉得 PL/SQL 直接搬就能跑结果在 DM8 上执行报错信息五花八门。那段时间我把达梦的《SQL 语言使用手册》翻得比小说都熟又在测试环境里反复试错才慢慢摸清了 DM8 在存储过程和函数上的脾气。达梦数据库 DM8 的存储过程语法整体偏向 Oracle 的 PL/SQL同时又保留了 MySQL 兼容模式这个双轨制是它的特点也是新手最容易踩坑的地方。如果你正准备在 DM8 上写存储过程或者正在把 Oracle、MySQL 里的老过程迁移过来这篇文章应该能帮你省下不少时间。我会从语法差异讲起再到变量、游标、异常三大件然后是函数实战案例最后聊聊调试排错和上生产前必须注意的事。全文基于我实际跑过的案例不写手册里已有的大段理论专挑那些容易忽略、容易报错的地方讲。1. 达梦存储过程的语法底子先搞清你写的是哪套方言1.1 兼容模式怎么查怎么选DM8 初始化实例的时候可以选择兼容 Oracle、兼容 MySQL 或兼容 SQL Server这个选择会直接影响存储过程的写法。但很多人不知道的是即使建库时选了 Oracle 兼容也不代表所有 Oracle 语法都能用反过来选了 MySQL 兼容也不意味着 MySQL 的存储过程写法能全盘照搬。踩过这个坑之后我养成了一个习惯拿到一个新的 DM8 环境第一件事先查兼容参数而不是急着写代码。查询方式很简单在 disql 或者 DM 管理工具里执行SELECT * FROM V$PARAMETER WHERE NAME LIKE %COMPATIBLE%;从实际使用来看COMPATIBLE_MODE参数的值通常对应不同的语法风格0 偏向 Oracle1 偏向 MySQL另外还有对应 SQL Server 的取值。注意这个参数有些版本是静态参数修改后要重启实例才生效所以别指望在已上线的环境里来回切换。我的建议是新项目直接统一用 Oracle 兼容模式因为达梦对 PL/SQL 的支持最成熟网上能查到的资料和踩坑记录也最多只有在存量 MySQL 业务需要平滑迁移、且团队实在改不动语法的情况下再考虑 MySQL 兼容模式。还有个容易被忽略的点管理工具里看到的编译报错和你用 disql 命令行执行时报错信息详略可能不一样。我一般先用 DM 管理工具做语法检查和单步调试确认没问题之后再拿到 disql 里做批量部署两边混淆着用容易把自己搞晕。1.2 最小可用的存储过程骨架不废话先看一个最简单的存储过程这是所有后续内容的底座CREATE OR REPLACE PROCEDURE proc_hello AS BEGIN PRINT(Hello DM8); END;在 disql 里执行之前建议先执行SET SERVEROUTPUT ON否则PRINT的输出你看不到容易误以为过程没跑。这个细节和 Oracle 的 SQL*Plus 完全一样很多人就是栽在这里过程明明执行成功控制台却什么都没有然后开始怀疑语法、怀疑环境折腾半天。调用方式有好几种最常用的是CALLCALL proc_hello();也可以在匿名块里调用BEGIN proc_hello(); END;注意CREATE OR REPLACE这个写法达梦和 Oracle 一样支持它好处是修改过程不需要先 DROP直接覆盖定义但也有个坑——如果你改了参数个数或参数类型最好先确认有没有其他地方通过旧签名调用这个过程因为CREATE OR REPLACE只替换过程本身的定义不会帮你检查所有调用点。过程的完整结构一般长这样CREATE OR REPLACE PROCEDURE proc_name(param1 IN INT, param2 OUT VARCHAR) AS -- 声明区变量、游标、异常 v_local_num INT : 0; BEGIN -- 执行区业务逻辑 NULL; EXCEPTION -- 异常区可选的异常处理 WHEN OTHERS THEN NULL; END;写的时候有三个容易出错的地方。第一参数有三种模式IN、OUT、IN OUT不写默认是IN但如果你有返回值要传给调用方必须显式写OUT或IN OUT而且调用时对应位置要传变量不能传常量否则编译都过不去。第二AS和IS在过程定义里可以互换使用别被两种写法搞懵。第三变量声明区里不能写执行语句所有初始化赋值要么在声明时写:要么放到BEGIN之后做这个跟其他编程语言的直觉不太一样新手最容易犯。1.3 Oracle、MySQL、DM8三方写法对照能力点Oracle PL/SQLMySQL 存储过程DM8Oracle 兼容模式过程骨架CREATE OR REPLACE PROCEDURE ... AS ...CREATE PROCEDURE ... BEGIN ... END需 DELIMITER同 Oracle变量赋值使用 :使用 SET 或 :同 Oracle字符串拼接使用 || 运算符使用 CONCAT() 函数同 Oracle但也支持部分函数异常处理EXCEPTION WHEN OTHERS THENDECLARE ... HANDLER同 Oracle输出信息DBMS_OUTPUT.PUT_LINESELECT 或 CONCAT 拼串PRINT / DBMS_OUTPUT自增实现序列 SEQUENCEAUTO_INCREMENT两者都支持IDENTITY 列/序列这张表是我在做迁移评估时整理出来的简化版。只看表会觉得 DM8 就是 Oracle 换皮实际用起来会发现细节差异很多。比如DBMS_OUTPUT在达梦里能用但有些版本的输出格式和 Oracle 略有差异再比如 MySQL 的CONCAT在达梦的兼容模式下也能用但参数个数、NULL 处理逻辑跟 MySQL 原版不完全一致。我的建议是不要在过程体里混用两套风格。选定一种比如 Oracle 风格全项目统一否则代码审查和后期维护都会非常痛苦。迁移的时候先拿一个典型过程做试金石把字符串拼接、日期格式化、NULL 处理这几个最容易不一致的点全部测一遍再决定批量改写策略。2. 三大件写扎实变量、游标与异常处理2.1 变量声明与赋值的细节变量声明看起来简单但我在 DM8 上踩过的坑不少。首先是类型匹配问题VARCHAR一定要带长度NUMBER要按精度写虽然达梦对某些不写长度的类型会做默认处理但一旦数据量上来、字符集是 UTF8 的时候默认长度很容易不够用跑着跑着突然报字符串截断排查起来特别费劲。DECLARE v_name VARCHAR(50); v_count INT : 0; v_price NUMBER(10,2); v_emp emp.emp_name%TYPE; -- 跟表字段类型保持一致 BEGIN v_name : 达梦; v_count : v_count 1; PRINT(v_name || , count || v_count); END;%TYPE这个写法是我比较推荐的习惯当变量要承接某张表某个字段的值时直接用表名.字段名%TYPE这样即使表结构字段长度变了过程也不会因为类型不匹配而崩。缺点是在大量过程里用%TYPE之后如果表结构反复变动过程重编译的次数会变多所以到底用不用要看你们表结构的稳定性。还有一个特别容易踩的坑变量名和列名同名。比如你声明了一个v_dept_id然后写SELECT dept_id INTO v_dept_id FROM dept WHERE ...这个没问题但如果你把变量也命名为dept_id再用WHERE dept_id dept_id达梦可能会把它解析成列等于列条件恒为真查出来一堆数据最后SELECT INTO报返回多行。避免办法就一条所有变量加统一前缀我用v_所有参数加前缀我用p_强制约定不要靠自觉。赋值的另一个细节是 NULL 处理。DM8 里NULL参与任何运算结果还是NULL所以v_result : v_result || A这种拼接如果v_result初始是 NULL拼出来的结果不会是 A 而是 NULL。我处理字符串累加时都会先做一步 NVL 兜底v_result : NVL(v_result, ) || v_ch;否则函数结果莫名其妙少一段非常隐蔽。2.2 显式游标与 FOR 循环游标怎么选如果说变量是存储过程的血肉游标就是灵魂。DM8 的游标写法和 Oracle 基本一致显式游标的标准四步是声明、打开、循环取数、关闭。我写一个实际的例子假设要逐行打印员工表里的 ID 和姓名DECLARE CURSOR cur_emp IS SELECT emp_id, emp_name FROM emp WHERE status 1; v_id NUMBER; v_name VARCHAR(50); BEGIN OPEN cur_emp; LOOP FETCH cur_emp INTO v_id, v_name; EXIT WHEN cur_emp%NOTFOUND; PRINT(v_id || - || v_name); END LOOP; CLOSE cur_emp; END;这段代码有两点要注意一是FETCH之后、使用变量之前一定要判断%NOTFOUND否则最后一条记录会重复处理一次二是手动OPEN的游标用完必须CLOSE如果中间发生异常跳到异常区CLOSE可能被跳过长期运行会累积游标资源直到数据库报游标数超限。这也是很多人写存储过程跑一阵子就出问题的隐藏原因。如果你只是要遍历结果集做简单处理我更推荐 FOR 循环游标代码量少一半而且不用手动打开和关闭FOR rec IN (SELECT emp_id, emp_name FROM emp WHERE status 1) LOOP PRINT(rec.emp_id || - || rec.emp_name); END LOOP;FOR 循环游标在循环结束时会自动关闭即使中途异常也会被框架处理省心很多。它的内部机制其实还是游标只是把打开、抓取、关闭都封装了所以性能上和显式游标没有本质差别。我的选择原则是逻辑简单直接用 FOR 循环需要在循环内动态拼接 SQL 或者需要精确控制抓取节奏的才用显式游标加动态 SQL。2.3 异常处理结构别让 OTHERS 吞掉一切异常处理是存储过程能不能上生产的关键。DM8 的异常结构跟 Oracle 一样BEGIN ... EXCEPTION ... END下面是一个完整的示例CREATE OR REPLACE PROCEDURE proc_divide(p_a NUMBER, p_b NUMBER) AS v_result NUMBER; BEGIN v_result : p_a / p_b; PRINT(result || v_result); EXCEPTION WHEN ZERO_DIVIDE THEN PRINT(除数不能为0); WHEN OTHERS THEN PRINT(发生未知错误: || SQLERRM); END;我要特别强调一个问题很多人图省事所有异常都写WHEN OTHERS THEN NULL把异常吞得干干净净。这在开发阶段省事到生产就变成灾难——过程执行失败但没有任何报错数据错了一半你都不知道。我的习惯是WHEN OTHERS分支里绝对不写空逻辑至少要输出SQLERRM或者写日志表把出错的过程名、参数、错误码、错误信息、时间都记录下来。这样哪怕真的出了没预料到的异常也留了排查线索。常见的内置异常名要记几个NO_DATA_FOUNDSELECT INTO 没查到数据、TOO_MANY_ROWSSELECT INTO 查到了多行、DUP_VAL_ON_INDEX唯一索引冲突、VALUE_ERROR类型转换或长度错误。其中NO_DATA_FOUND是最容易误用的很多人以为SELECT INTO查不到数据时结果变量会是 NULL然后在后面直接使用结果过程抛异常。如果你明确知道某条记录可能不存在要么先游标再判断要么在异常里单独处理这种情况。自定义业务异常可以用RAISE_APPLICATION_ERRORIF v_count 0 THEN RAISE_APPLICATION_ERROR(-20001, 当前没有可处理的数据); END IF;注意错误号要选在 -20000 到 -20999 之间这是 Oracle 风格的自定义错误区间达梦也兼容。调用方在捕获时可以直接用SQLERRM取出你写的提示信息也可以按错误码做定向判断。这个机制特别适合在批量处理里做约定式报错比如过程内部发现数据不满足前提条件直接抛业务错误让调用方知道该回滚还是该重试。3. 函数实战从首拼码生成到表值函数3.1 真实需求中文姓名生成首拼码函数FUNCTION在达梦里和存储过程最大的区别就是必须有返回值而且可以在 SQL 语句里直接调用。我拿一个真实需求来演示——给客户姓名生成首拼码也就是把张三变成ZS。这个需求在会员系统、对账系统里特别常见达梦本身没提供现成的中文字符转拼音能力网上搜达梦数据库 生成首拼码函数的人很多说明大家都在这上面踩过坑。最稳妥的做法不是自己去算内码而是建一张拼音码表把常用字和首字母映射关系存进去然后函数里去查表。好处是可控、不受数据库字符集影响坏处是码表需要初始化。步骤分两步第一步建码表并初始化数据CREATE TABLE t_pinyin_map ( ch CHAR(1) PRIMARY KEY, py VARCHAR(1) NOT NULL ); INSERT INTO t_pinyin_map VALUES (张,Z); INSERT INTO t_pinyin_map VALUES (三,S); -- 实际使用时把常用汉字一次性初始化可以用 GBK 区间计算脚本批量生成第二步写函数逐字取首字母并拼接CREATE OR REPLACE FUNCTION fn_py_first(p_str VARCHAR(200)) RETURN VARCHAR(200) AS v_result VARCHAR(200) : ; v_len INT; v_ch CHAR(1); v_py VARCHAR(1); BEGIN IF p_str IS NULL THEN RETURN ; END IF; v_len : LENGTH(p_str); FOR i IN 1..v_len LOOP v_ch : SUBSTR(p_str, i, 1); BEGIN SELECT py INTO v_py FROM t_pinyin_map WHERE ch v_ch; v_result : v_result || v_py; EXCEPTION WHEN NO_DATA_FOUND THEN v_result : v_result || #; END; END LOOP; RETURN UPPER(v_result); END;用起来很简单SELECT fn_py_first(张三) AS py FROM DUAL;返回结果就是ZS。这个函数有几个细节值得说一下一是LENGTH对单字节和双字节字符的处理在 UTF8 字符集下LENGTH返回的是字符数所以按位置SUBSTR是对的不会劈开半个汉字二是查不到映射的字统一返回#这样至少不会让整个函数报错后续清洗数据时也能看出哪些字没录入三是我强制转成了大写保证输出格式统一。可能有人会问为什么不用内码区间判断法我也试过思路是根据 GBK 或 GB2312 编码中汉字内码落在哪个区间来判断首字母比如啊的区位码对应 A 区间。这种方案不用建表但有两个致命问题一是数据库字符集如果不是 GBK 而是 UTF8ASCII()返回的值完全不是你想的那回事得先做转码二是多音字没法处理比如重庆的重按内码只能给一个固定结果。所以我最终选了码表方案拼音准确率完全由码表质量决定想要多音字支持就再加一个词组列做优先匹配。3.2 函数与存储过程的职责边界很多刚开始写达梦的人分不清什么时候用函数、什么时候用存储过程我的判断标准很简单能不能在 SQL 里直接调用。函数可以嵌在SELECT、WHERE、ORDER BY里比如上面那个fn_py_first我可以直接写SELECT emp_name, fn_py_first(emp_name) AS py_code FROM emp WHERE fn_py_first(emp_name) LIKE Z%;这种场景存储过程完全做不到你只能先把过程算出的结果放到临时表再跟业务表关联非常别扭。所以凡是输入参数、返回单个值的计算型需求比如格式化编号、计算统计指标、解析字符串都应该写成函数。反过来函数不适合做大批量 DML 操作。虽然达梦允许在函数里写INSERT、UPDATE、DELETE但函数本身没有事务独立性它是在调用方的事务里执行的如果函数里更新了几万行数据中途报错可能要连带外面整个事务一起回滚。而且官方手册对函数内写 DML 的限制在不同版本里有差异有些版本直接会报错。我的原则是函数里只做查询和计算DML 交给存储过程或应用程序除非你确实清楚自己在干什么否则别在函数里偷偷更新表。参数模式也同样适用函数只有IN参数没有OUT、IN OUT。如果你发现自己想在一个函数里返回多个值那说明这个需求应该拆成多个函数或者直接改成存储过程。硬把多个返回结果塞进一个函数代码会非常拧巴后面也没法维护。3.3 返回结果集的函数SYS_REFCURSOR 与管道函数除了返回标量值达梦的函数还能返回游标也就是把一张结果集交给调用方。最常用、兼容性最好的是SYS_REFCURSOR写法如下CREATE OR REPLACE FUNCTION fn_get_emp_list(p_dept_id INT) RETURN SYS_REFCURSOR AS cur SYS_REFCURSOR; BEGIN OPEN cur FOR SELECT emp_id, emp_name, salary FROM emp WHERE dept_id p_dept_id; RETURN cur; END;调用的时候先接收游标再循环取数DECLARE cur SYS_REFCURSOR; v_id NUMBER; v_name VARCHAR(50); BEGIN cur : fn_get_emp_list(10); LOOP FETCH cur INTO v_id, v_name; EXIT WHEN cur%NOTFOUND; PRINT(v_id || - || v_name); END LOOP; CLOSE cur; END;这种写法在报表系统里非常实用报表查询条件多变组合条件特别多你可以在函数里用动态拼接 SQL 的方式构造结果集然后统一返回游标应用程序拿到的就是一个标准结果集不用关心你的查询怎么拼的。注意返回的游标由调用方负责关闭函数内部不要提前CLOSE否则调用方拿到的就是一个已经关闭的空游标。达梦还支持带PIPELINED的管道函数可以像流水线一样逐行产出结果适合做数据拆分、行转列这类场景。不过我实际用下来发现管道函数在不同达梦版本里的细节差异比较多语法要求也比较严格所以我给团队的建议是优先用SYS_REFCURSOR它能覆盖绝大多数函数返回结果集的需求而且从 Oracle 迁过来的代码改造成本最低等确实遇到大数据量逐行产出的极端场景再回头研究管道函数以你们本地数据库版本的《DM8_SQL语言使用手册》为准。4. 调试与排错存储过程跑不起来的真实原因4.1 调试输出的三种手段写存储过程不可能一遍过调试是常态。达梦里我常用的调试手段有三种按优先级排PRINT、DBMS_OUTPUT.PUT_LINE、日志表。PRINT最简单在过程中直接打印变量值配合SET SERVEROUTPUT ON使用。它适合开发阶段快速看结果但不适合生产环境因为生产环境没人盯着控制台看输出。DBMS_OUTPUT.PUT_LINE的用法和 Oracle 一样达梦做了兼容但要注意需要在会话里先执行DBMS_OUTPUT.ENABLE否则PUT_LINE可能不输出。我习惯用PRINT因为少一个步骤命令也短。真正生产环境必须上的是日志表方案。我一般建一张简单的过程日志表CREATE TABLE proc_run_log ( log_id BIGINT IDENTITY(1,1) PRIMARY KEY, proc_name VARCHAR(100), log_level VARCHAR(10), msg VARCHAR(2000), log_time TIMESTAMP DEFAULT SYSDATE );然后在过程的关键节点写日志INSERT INTO proc_run_log(proc_name, log_level, msg) VALUES (proc_hello, INFO, 开始处理参数 || p_param);日志表方案的好处是出了问题可以直接按时间查日志配合事务提交策略甚至能看到过程执行到哪一步崩的。缺点是每个过程要写不少插入语句代码会变长。我的折中办法是关键业务过程和批量任务必写简单过程只写开始和结束两条把详细调试信息留在PRINT里开发阶段看。DM 管理工具还提供了图形化的调试功能可以设置断点、单步执行、查看变量当前值跟 IDE 里调试 Java 差不多。如果你的数据库版本支持建议开发阶段多用这个功能比打日志直观得多。但同样的过程上线后还是要靠日志表因为生产环境你是没权限随便开调试器的。4.2 高频报错与解决对照表把这段时间踩过的坑整理成一张表遇到报错先对号入座报错信息/现象常见原因解决办法无效的语句 / ORA-00900过程体内写了不能在此处的语句或用了错误的 SQL 拼写检查是否把SELECT单独当成语句使用DM8 中SELECT需要配合INTO或游标未找到数据 / ORA-01403SELECT INTO查不到记录先判断是否存在或在异常区处理NO_DATA_FOUND实际返回的行数超过请求的行数SELECT INTO查到多行检查条件是否唯一或用游标/聚合函数改写字符串截断VARCHAR长度不够扩大变量或字段长度检查字符集标识符太长对象名超过长度限制缩短对象名提前确认当前版本的标识符长度上限权限不足用户没有执行过程的权限用GRANT EXECUTE ON 过程名 TO 用户授权PRINT 无输出会话没打开SET SERVEROUTPUT ON执行SET SERVEROUTPUT ON再调用游标已经打开重复打开同一个游标检查是否漏了CLOSE或在OPEN前判断%ISOPENSQL 语句未结束缺少分号或结束符不匹配检查过程结尾是否写了/或分号注意 disql 的批处理规则这里特别想展开说的是无效的语句这个报错因为它的误导性极强。我遇到过一次过程逻辑看着完全没问题编译也过了一执行就报无效语句。后来一行一行排查才发现是过程体里某个IF分支中写了一个独立的SELECT语句没有INTO也没有接到游标上。在 Oracle 里有些版本允许这种裸查询存在但达梦直接判无效。这个差异在从 Oracle 迁移过来的存储过程里特别常见改写时要注意。另一个高频坑是标识符太长。达梦对标识符长度有上限具体数值不同版本有差异但如果你的项目有比较完整的前缀规范比如V_、P_、CUR_、TMP_再加上业务表名特别长很容易在中间变量上超限。我的对策是把过程名和变量名都控制在 30 个字符以内宁可多写注释说明含义也不要让名字长到报错。4.3 一次完整的排错链路讲一个真实的排错过程帮大家理解排查思路。当时有个数据同步过程proc_sync_order每周跑一次突然有一周失败了错误日志里只有一个ORA-01403: 未找到数据。如果直接去改过程八成会看走眼因为报错信息太泛了。我的排查链路是这样的第一步先看失败时间点附近有没有 DDL 操作查出有人在前一天重建了订单明细表改变了分区结构。第二步在测试环境复现用同一批数据跑一遍果然复现同样报错。第三步在过程中加日志把关键查询的结果条数记录到日志表定位到是某一条关联查询没查到数据导致后续SELECT INTO抛异常。第四步进一步查为什么查不到结果是重建表时历史分区数据丢了访问权限而不是数据本身没了。最终解决办法是调整同步过程的查询条件增加对分区和权限的校验并在NO_DATA_FOUND异常分支里把上下文记录下来。这个案例给我最大的教训是存储过程报错别急着改代码先回答三个问题——数据变了没有、结构变了没有、权限变了没有。大多数看起来莫名其妙的报错根因都是这三个之一。把排查的起点放在环境变化上而不是代码逻辑上往往能省几个小时。另外过程里使用的数据源表和目标表的权限在库迁移、用户重建之后特别容易丢授权操作一定要纳入部署脚本里统一管理不能靠手工一顿操作。5. 上生产之前批量处理、事务控制与部署规范5.1 大批量数据处理的存储过程模板存储过程最常见的生产场景就是批量数据处理比如定时同步、历史数据清理、报表数据预计算。这种过程和普通业务过程不一样循环几万甚至几十万行是常态如果按单行处理性能会非常难看。我提供一个经过实践验证的模板CREATE OR REPLACE PROCEDURE proc_batch_sync AS v_batch_size INT : 1000; v_processed INT : 0; v_error_count INT : 0; BEGIN -- 遍历待处理数据按批次提交 FOR rec IN (SELECT id FROM src_order WHERE status 0) LOOP BEGIN UPDATE dst_order SET sync_status 1, sync_time SYSDATE WHERE id rec.id; v_processed : v_processed 1; -- 每处理满一个批次就提交一次避免事务过大 IF MOD(v_processed, v_batch_size) 0 THEN COMMIT; PRINT(已处理 || v_processed || 条); END IF; EXCEPTION WHEN OTHERS THEN v_error_count : v_error_count 1; INSERT INTO proc_run_log(proc_name, log_level, msg) VALUES (proc_batch_sync, ERROR, ID || rec.id || , 错误 || SQLERRM); END; END LOOP; COMMIT; PRINT(完成成功 || v_processed || , 失败 || v_error_count); END;这个模板有几个设计思路。第一每行数据处理单独包一个内层BEGIN ... EXCEPTION ... END这样单条数据出错不会中断整个任务错误被记录到日志表后继续处理下一条任务跑完还能告诉你失败了多少条。第二按批次COMMIT防止事务累积过大导致回滚段爆掉也避免数据库锁持有时间过长。第三每一批次输出一次进度跑批的时候你能知道它到底死了还是活着。如果你的批量任务是纯增量的、对一致性要求极高比如账务类处理那就不能这样错一条继续下一条而应该反过来任何一条出错整个批次回滚避免半截数据。这个取舍没有标准答案完全取决于业务语义。我见过团队把财务批处理也做成出错继续结果月底对账对不上返工返到怀疑人生。所以写批量过程之前先跟业务确认清楚这个任务是可断点续跑型还是全有或全无型。5.2 事务提交策略不能拍脑袋定事务是存储过程上生产最容易出问题的地方而且往往要跑很久才暴露。达梦里存储过程里的COMMIT、ROLLBACK会直接作用于当前事务也就是说过程内部提交了外部调用方就再也回滚不了了。如果你的过程是被应用系统在业务事务里调用的过程里的COMMIT会破坏应用层的事务边界导致业务数据不一致。我建议的做法有两种。第一种存储过程内部不写任何COMMIT只处理数据提交统一由调用方控制这样最干净但对调用方的要求高应用必须自己管理事务。第二种过程本身就是一个独立批任务那就在过程内按批次提交但必须在设计文档里明确写清楚此过程自带事务管理请勿在外部事务中调用。两种方式没有对错关键是别混着用否则排查线上问题的时候你会被各种半提交的数据搞疯。还有一个细节异常分支里的ROLLBACK。如果你在外层EXCEPTION里捕获了OTHERS最好显式执行ROLLBACK把当前事务里未提交的变更全部回滚掉。否则异常被捕获之后你只打印了个日志事务还开着后面的代码可能基于一个半成品数据继续跑产生更离谱的结果。记住一条捕获异常不等于善后该回滚要回滚该记录要记录。5.3 部署、权限与代码管理的工程化建议存储过程也是代码也要走版本管理和部署流程。我见过很多团队把存储过程当成数据库里的临时脚本改完直接在生产库上执行没有版本记录没有回滚方案。等出了问题想看看上周那个过程改了什么根本无从查起。解决方案很简单所有存储过程和函数都以.sql文件方式保存在 Git 里命名带上模块前缀比如proc_order_sync.sql、fn_py_first.sql部署时用 disql 批量执行。批量部署脚本建议写成类似这样的形式disql SYSDBA/密码localhost:5236 SET SERVEROUTPUT ON START /opt/sql/proc_order_sync.sql; START /opt/sql/fn_py_first.sql;权限管理方面我强烈建议按最小权限原则来业务账号只授予它必需的EXECUTE权限不要把表的增删改查权限都放开。授权语句很简单GRANT EXECUTE ON proc_order_sync TO app_user;如果过程里访问了别的模式下的表还要确认过程所有者对这些表有相应权限因为达梦在 DEFINER 权限模型下过程是以所有者身份执行 SQL 的不是以调用者身份。这个跟 Oracle 的行为类似但很多从 MySQL 转过来的人不知道经常遇到调用者明明有权限过程却报权限不足的怪问题。最后再说一个容易被忽略的部署细节字符集和文件编码不一致。达梦导入 SQL 文件时报本地编码与文件编码不一致的情况并不少见比如数据库是 GBK而你的.sql文件是 UTF8 保存的存储过程里的中文字符串就全乱套了。我的习惯是项目里统一规定 SQL 脚本文件的编码格式跟数据库字符集保持一致保存文件时注意编辑器右下角的编码状态别稀里糊涂写出个 UTF8 文件部署到 GBK 库上。这篇东西写到这里差不多把我这半年在 DM8 上折腾存储过程和函数的主要心得都倒出来了。如果你正好在迁移或者刚接触达梦照着这些套路能少踩不少坑。最后再分享一个我的个人习惯不管过程写得多顺手上线前一定在测试库完整跑一遍并保留日志表数据这样就算生产出问题也有据可查。达梦的手册确实厚但遇到拿不准的语法翻手册永远比网上东拼西凑靠谱尤其是函数这类版本差异大的特性一定要以你们自己库的版本为准。