1. 迁移前先搞清楚:达梦的条件处理能力分两套体系
上个月接手一个从 MySQL 迁到达梦数据库的改造任务,客户那边的开发同事给的判断是"条件判断最没技术含量,改改语法就行"。结果第一批 SQL 跑下来,报错最密集的恰恰就是IF和CASE WHEN这两个看起来最不起眼的东西。有的语句在 MySQL 里跑得飞快,搬到达梦数据库直接报"无效的表达式";有的存储过程建都建不上;还有的能跑但结果全是 NULL,查半天发现是空值比较写错了。
问题的根源在于,很多人把 MySQL 的IF和达梦的IF当成同一个东西。实际上在达梦里,条件处理能力被切成了两条完全独立的线:一条是过程体内部的分支语句,写法和 Oracle 的 PL/SQL 一脉相承,IF ... THEN ... ELSIF ... ELSE ... END IF,只能在存储过程、函数、触发器、匿名块里用;另一条是查询表达式里的条件取值,也就是CASE WHEN,它属于标量表达式,可以出现在 SELECT 列表、WHERE、ORDER BY、UPDATE 的 SET 子句里。MySQL 那种"一个IF(cond, a, b)函数走天下"的习惯,到了达梦身上会直接撞墙,因为达梦在默认配置下并没有提供这个标量函数。
这篇内容就是把这套东西拆开讲清楚:MySQL 里习以为常的IF()、IFNULL()、布尔表达式当列返回、CASE WHEN隐式类型转换,在达梦上分别该怎么落地。适合三类人看——正在做数据库迁移的后端开发、刚接触达梦的 DBA、以及平时写 SQL 但没深究过方言差异的同学。里面会给可直接抄的语句、差异对照表、报错速查表,还有几个我自己踩坑之后总结出来的规避手法。
1.1 为什么 MySQL 的 IF 到了达梦就跑不通
得先说明白 MySQL 里的IF其实有两个身份。第一个身份是流程控制语句,写在存储过程里,IF ... THEN ... ELSE ... END IF;,这个达梦也有,但结尾必须补齐END IF和分号,少一个都建不上。第二个身份是标量函数IF(表达式, 真值, 假值),可以直接塞进 SELECT 里,比如SELECT IF(AMT > 100, '大', '小') FROM T。第二个身份是 MySQL 的特色,达梦默认没有对应的函数。
我实测过,在达梦默认配置下直接写SELECT IF(AMT > 100, 'A', 'B') FROM T_ORDER,客户端会抛出一个关于函数名无效的报错,因为解析器把它当成一个不存在的自定义函数去找了。有些版本开了 MySQL 兼容开关之后能识别一部分,但我不建议依赖这个,原因后面会讲。稳妥的写法就是老老实实换成CASE WHEN AMT > 100 THEN 'A' ELSE 'B' END,语义完全等价,而且是标准 SQL,换任何数据库都能跑。
这里有个容易被忽略的认知:IF()函数看着简洁,但它会让 SQL 失去可移植性。我在改造过程中统计过,一个中等规模的业务系统里IF()出现的频率远高于想象,尤其是那种"金额大于多少显示什么状态"的报表 SQL。这类语句统一替换成CASE WHEN之后,顺带还解决了一批可读性问题,因为CASE WHEN支持多分支,不用嵌套成一坨。
1.2 兼容模式开关不等于语法免死金牌
达梦在初始化实例的时候可以选择兼容模式,让它在部分语法上向 MySQL 或者 Oracle 靠拢。很多人的第一反应是"那我开个 MySQL 兼容模式不就完事了"。我的建议是能不开就别依赖,原因有三个。
第一,兼容模式是实例级的配置,建库那一刻定下来,后面改起来代价很大,不是每张表、每条语句都能随便切换的。第二,兼容覆盖的是语法糖层面,比如某些函数名、某些引号规则,但像隐式类型转换的严格程度、布尔表达式的返回行为这些更深层的东西,兼容模式管不了。第三,也是最现实的一点,你在开发环境测通了,不代表生产环境用的是同一套配置,这种"环境差异导致的玄学报错"排查起来极其痛苦。
我一般的处理原则是:把兼容模式当成过渡期的缓冲垫,用来让老代码先跑起来,但新写的 SQL 一律按达梦原生语法来。这样后面哪怕把开关关掉,业务也不会崩。这个思路在迁移项目里很关键,它决定了你是在"修修补补"还是在"真正完成迁移"。
2. 过程体里的 IF:达梦只认 THEN 和 END IF
存储过程、自定义函数、触发器这些程序单元里,达梦的分支语句是完完整整的 PL/SQL 风格。这块跟 MySQL 相比,差异主要在收尾符号和条件写法上,逻辑结构反而是接近的。写过 Oracle 的人上手会觉得很顺,纯 MySQL 背景的同事则经常在END IF和分号上栽跟头。
2.1 基本语法骨架与三种分支形态
最基础的单分支长这样,注意THEN不能省,END IF后面的分号也不能省:
CREATE OR REPLACE PROCEDURE P_CHECK_AMT(P_AMT IN DECIMAL(12,2)) AS BEGIN IF P_AMT IS NULL THEN DBMS_OUTPUT.PUT_LINE('金额为空,跳过处理'); END IF; END; /双分支加一个ELSE,多分支用ELSIF——注意是ELSIF不是ELSE IF,这是新手最容易写错的地方之一。写成ELSE IF语法上也能过,但语义会变成嵌套的 IF,你就得配两个END IF,少一个就报错。我见过有同事为了这个报错找了半小时,最后发现只是少了一层收尾。
CREATE OR REPLACE PROCEDURE P_ORDER_TAG( P_AMT IN DECIMAL(12,2), P_TAG OUT VARCHAR(20) ) AS BEGIN IF P_AMT IS NULL THEN P_TAG := '未知'; ELSIF P_AMT >= 10000 THEN P_TAG := '大额'; ELSIF P_AMT >= 1000 THEN P_TAG := '中额'; ELSE P_TAG := '小额'; END IF; END; /这段逻辑看着简单,但里面藏了两个细节。一是分支顺序不能乱,>= 10000必须写在>= 1000前面,否则大额订单会先被中额条件截走。二是空值判断放在最前面,因为后面那些数值比较遇到 NULL 一律返回未知,既不进真分支也不进假分支,会直接掉到ELSE里,把空值误判成小额。这个坑我在真实项目里见过一次,报表上多出一批金额为空的"小额订单",业务方追着问了两天才定位到。
2.2 匿名块、分隔符与执行方式
达梦的客户端工具(比如 disql 或者管理工具的命令行窗口)执行过程体时,需要明确告诉它"这一段到哪结束"。标准做法是用一个斜杠/单独占一行作为结束符,或者在开头显式写BEGIN ... END;的匿名块。
匿名块的形式很适合做临时验证,不用建过程就能测分支逻辑:
DECLARE V_AMT DECIMAL(12,2) := 5500; V_TAG VARCHAR(20); BEGIN IF V_AMT >= 10000 THEN V_TAG := '大额'; ELSIF V_AMT >= 1000 THEN V_TAG := '中额'; ELSE V_TAG := '小额'; END IF; DBMS_OUTPUT.PUT_LINE('结果:' || V_TAG); END; /这里要特别提醒:DBMS_OUTPUT的输出默认是不显示的,需要先执行一句开启命令,否则你会以为程序没跑。这个行为和 MySQL 里SELECT直接吐结果不一样,很多人第一次用会懵。
注意:过程体内每条语句结尾都要有分号,包括
END IF;、END;和过程整体的结尾。分号缺失是最常见的建过程失败原因,报错信息往往只指向最后一行,但实际上错误可能在前面的某个分支里。
2.3 IF 里最容易写错的四个细节
第一个细节是空值比较。IF P_AMT = NULL THEN这种写法在达梦里永远不会成立,因为空值参与等值比较的结果是未知。必须写IS NULL或者IS NOT NULL。这条规则在 CASE WHEN 里同样适用,属于跨语句类型的通用铁律。
第二个细节是字符串拼接。MySQL 里习惯写CONCAT(a, b),达梦更推荐a || b,虽然达梦也认CONCAT,但||是标准写法,而且在多段拼接时更清爽。上面那段匿名块里我就用了||。
第三个细节是条件里的类型。达梦对类型的严格程度比 MySQL 高不少,IF V_NUM = '100'这种拿数字和字符串比的写法,在 MySQL 里会悄悄转成数字,在达梦里可能报类型不匹配。稳妥做法是显式转换,用CAST(V_NUM AS VARCHAR(20)) = '100',或者干脆两边都用同一种类型。
第四个细节是分支嵌套的深度。达梦对嵌套层数有上限,业务逻辑里如果出现四五层 IF 嵌套,说明这段逻辑本身就需要重构了。我的习惯是把复杂判断拆成多个带返回值的函数,主过程只负责调用和组合,可读性和可维护性都会好很多。
3. 查询里的条件取值:CASE WHEN 的两种写法与达梦的脾气
查询里做条件判断,CASE WHEN是绕不开的。达梦完整支持标准 SQL 的两种形态——简单 CASE 和搜索 CASE,语法结构跟 MySQL 一致,但在类型处理和布尔结果上差异明显,这两块是迁移时的主要雷区。
3.1 简单 CASE 与搜索 CASE 的区别
简单 CASE 是先给一个表达式,然后逐项做等值匹配,适合做字典映射:
SELECT ORDER_ID, CASE STATUS WHEN 'PAID' THEN '已支付' WHEN 'SHIPPED' THEN '已发货' WHEN 'DONE' THEN '已完成' ELSE '其他' END AS STATUS_NAME FROM T_ORDER;搜索 CASE 是每个分支独立写条件,适合区间判断和多条件组合:
SELECT ORDER_ID, CASE WHEN AMT IS NULL THEN '未知' WHEN AMT >= 10000 THEN '大额' WHEN AMT >= 1000 THEN '中额' ELSE '小额' END AS AMT_LEVEL FROM T_ORDER;两者的选择标准很直白:同一列跟一堆固定值比,用简单 CASE,代码短;涉及区间、函数结果、多列组合,用搜索 CASE,灵活。有个小陷阱是简单 CASE 里不能写条件,CASE STATUS WHEN AMT > 100 THEN ...这种写法是错的,等值匹配的右侧只能是一个值,不能是表达式。
还有一点值得单独说:简单 CASE 遇到 NULL 时行为比较特殊。CASE COL WHEN NULL THEN 'X' ELSE 'Y' END这种写法,NULL 分支永远不会命中,因为等值比较遇空即为未知。要用空值分支必须走搜索 CASE 的WHEN COL IS NULL。
3.2 达梦与 MySQL 的语法差异对照
把迁移过程中最常撞到的差异整理成一张表,遇到报错可以先扫一眼这张表定位:
| 使用场景 | MySQL 常见写法 | 达梦对应写法 | 关键差异说明 |
|---|---|---|---|
| 单行二选一取值 | IF(cond, a, b) | CASE WHEN cond THEN a ELSE b END | 达梦默认无 IF 标量函数 |
| 空值兜底 | IFNULL(x, y) | NVL(x, y)或COALESCE(x, y) | COALESCE 支持多参数 |
| 布尔表达式当列 | SELECT a > b FROM t | CASE WHEN a > b THEN 1 ELSE 0 END | 达梦不返回布尔型列 |
| 多值映射 | CASE / ELT / FIELD | DECODE或CASE | DECODE 认为两个空值相等 |
| 行转列 | CASE WHEN+GROUP BY | 同左,也可用PIVOT | 达梦支持 PIVOT 关键字 |
| 字符串拼接 | CONCAT(a, b) | a || b | 双竖线是标准写法 |
| 空值判断 | = NULL无意义 | IS NULL | 两库一致,但常被忽略 |
| 自定义排序 | ORDER BY FIELD(...) | ORDER BY CASE ... END | 达梦无 FIELD 函数 |
这张表里最需要重点理解的是第二行和第三行。布尔表达式不能当列返回这一条,杀伤力很大,因为很多迁移过来的报表 SQL 里会有SELECT AMT > 0 AS IS_POSITIVE这种写法,到了达梦一律要改成CASE WHEN AMT > 0 THEN 1 ELSE 0 END。改的时候注意别名要给上,否则列名会变成一长串表达式。
3.3 返回类型必须统一:达梦不给你隐式兜底
这是我个人认为最值得写进团队规范的一条。MySQL 对CASE WHEN各分支返回类型不一致的情况非常宽容,CASE WHEN AMT > 100 THEN 1 ELSE 'N/A' END能跑,它会把数字悄悄转成字符串。达梦不行,这种写法大概率直接报类型不一致或者转换失败。
正确的处理是让所有分支返回同一类型,要么都转字符串,要么都保持数字并用一个特殊值表示无数据:
-- 写法一:统一成字符串 SELECT CASE WHEN AMT > 100 THEN '1' ELSE 'N/A' END AS FLAG FROM T_ORDER; -- 写法二:统一成数字,用 NULL 表示无数据 SELECT CASE WHEN AMT > 100 THEN 1 ELSE NULL END AS FLAG FROM T_ORDER; -- 写法三:显式 CAST,最保险 SELECT CASE WHEN AMT > 100 THEN CAST(1 AS DECIMAL(10,0)) ELSE CAST(NULL AS DECIMAL(10,0)) END AS FLAG FROM T_ORDER;写法三是我的默认选择,虽然啰嗦,但它把类型信息写死了,不会因为某个分支的类型推断出偏差。还有一种更隐蔽的情况:所有分支都是 NULL,达梦推断不出这个表达式到底该是什么类型,有些场景会报错。这时候必须用CAST(NULL AS 类型)兜住。我在做数据清洗脚本时遇到过,一个CASE的分支全是空值,跑出来报了个让人摸不着头脑的错误,加上 CAST 立刻就通了。
4. 实战改造:几条典型 MySQL 语句到达梦的落地过程
光看语法对照表容易产生"我懂了"的错觉,实际动手还是会卡。挑几个改造过程中出现频率最高的场景,把改造前后的完整语句摆出来,包括我当时的判断依据。
4.1 打标语句:把 IF 函数换成 CASE WHEN
改造前,MySQL 版本是这样,一列套一列,看着还行:
SELECT ORDER_ID, IF(AMT >= 10000, '大额', IF(AMT >= 1000, '中额', '小额')) AS AMT_LEVEL FROM T_ORDER;改造成达梦版本,直接拍平成搜索 CASE,嵌套没了,可读性反而更好:
SELECT ORDER_ID, CASE WHEN AMT >= 10000 THEN '大额' WHEN AMT >= 1000 THEN '中额' ELSE '小额' END AS AMT_LEVEL FROM T_ORDER;这里有个是否要加AMT IS NULL分支的决策点。原 MySQL 语句没考虑空值,空金额会掉进ELSE变成"小额"。改造时我补了一个空值分支,放在最前面显式判断。理由很实际:报表的下游会用这个标签做分组统计,空值混进"小额"会污染数据。补这一行成本极低,但要跟业务方确认清楚语义,别自己拍脑袋改。
4.2 存储过程分支:从 IF 到 ELSIF 的改写
MySQL 的存储过程分支写法,注意它不需要END IF之外的额外收尾,参数类型和变量声明语法也不一样:
-- MySQL 版本 CREATE PROCEDURE P_SET_LEVEL(IN P_AMT DECIMAL(12,2)) BEGIN DECLARE V_LEVEL VARCHAR(20); IF P_AMT >= 10000 THEN SET V_LEVEL = 'A'; ELSEIF P_AMT >= 1000 THEN SET V_LEVEL = 'B'; ELSE SET V_LEVEL = 'C'; END IF; SELECT V_LEVEL; END;迁到达梦要改的地方比想象中多:参数方向要显式写IN,变量声明搬到AS和BEGIN之间,赋值从SET改成:=,ELSEIF要改成ELSIF(少一个 E),最后补上斜杠结束:
CREATE OR REPLACE PROCEDURE P_SET_LEVEL(P_AMT IN DECIMAL(12,2)) AS V_LEVEL VARCHAR(20); BEGIN IF P_AMT >= 10000 THEN V_LEVEL := 'A'; ELSIF P_AMT >= 1000 THEN V_LEVEL := 'B'; ELSE V_LEVEL := 'C'; END IF; DBMS_OUTPUT.PUT_LINE('等级:' || V_LEVEL); END; /这批改动里,ELSEIF到ELSIF的拼写变化是我统计下来出错率最高的,因为编辑器不会提示,只有建过程的时候才报错。我的处理办法是在迁移前先做一轮全局替换,把ELSEIF统一替换成ELSIF,能省掉大量返工。
4.3 行转列与自定义排序:CASE 的两个高频用法
行转列在 MySQL 里基本只能靠CASE WHEN加GROUP BY,达梦也支持这套写法:
SELECT MONTH_NO, SUM(CASE WHEN CHANNEL = '线上' THEN AMT ELSE 0 END) AS ONLINE_AMT, SUM(CASE WHEN CHANNEL = '线下' THEN AMT ELSE 0 END) AS OFFLINE_AMT FROM T_SALE GROUP BY MONTH_NO;但达梦多了个选择,它原生支持PIVOT,语句更短,也更容易维护:
SELECT * FROM ( SELECT MONTH_NO, CHANNEL, AMT FROM T_SALE ) P PIVOT ( SUM(AMT) FOR CHANNEL IN ('线上' AS ONLINE_AMT, '线下' AS OFFLINE_AMT) );两种写法我都在用。CASE版本的优势是兼容性好、执行计划更可控;PIVOT版本的优势是可读性强,列多的时候不容易写错。选哪个取决于后续要不要继续在这段结果上加工,如果要,我倾向CASE版本嵌到子查询里,灵活度更高。
自定义排序也是CASE的高频场景。MySQL 里很多人用ORDER BY FIELD(STATUS, 'RUNNING', 'WAITING', 'DONE'),达梦没有FIELD函数,得改成:
SELECT * FROM T_TASK ORDER BY CASE STATUS WHEN 'RUNNING' THEN 1 WHEN 'WAITING' THEN 2 WHEN 'DONE' THEN 3 ELSE 9 END, CREATE_TIME DESC;注意ELSE 9这一句不能少。不给ELSE的话,未匹配到的状态会落到 NULL,而 NULL 在排序里通常排在最前或者最后(取决于数据库和排序方向),结果就是你精心设计的顺序被打乱。给个明确的大数字兜底,语义最清晰。
4.4 批量更新:SET 子句里的 CASE 写法
这个场景迁移时容易被漏掉,但业务里特别常见——按条件批量刷字段值:
UPDATE T_ORDER SET LEVEL_CODE = CASE WHEN AMT >= 10000 THEN 'A' WHEN AMT >= 1000 THEN 'B' ELSE 'C' END, UPDATE_TIME = NOW() WHERE STATUS = 'PAID';达梦完全支持这种写法,NOW()也有对应函数。要注意的是WHERE条件必须给足,否则全表刷数据,锁表时间会很长。我在测试环境试过一次没加WHERE,几十万行的表刷了将近一分钟,期间整张表基本处于不可写状态。生产环境做这类批量更新,我的习惯是分批执行,每批几千行,跑完一批提交一次,减少锁持有时间。
另外提醒一句,UPDATE里的CASE每次只对本行求值一次,这跟子查询关联更新的性能特征完全不同,前者通常更快,能用CASE就别用关联子查询。
5. 踩坑记录与排查速查
语法看完只是知道了路怎么走,真正的坑都在路上。把改造过程中撞到的典型问题整理成速查表,方便对号入座。
5.1 报错信息对照表
| 报错现象 | 真实原因 | 处理办法 |
|---|---|---|
| 提示函数名无效 | 在普通 SQL 里用了IF() | 改写成CASE WHEN |
| 提示数据类型不一致 | CASE各分支返回类型不同 | 统一类型或显式CAST |
| 过程创建失败,指向末尾 | 缺END IF;或语句缺分号 | 逐层检查分支收尾 |
| 客户端执行过程体直接报语法错 | 没用/结束或写成单行 | 换 disql 用斜杠结束 |
CASE结果全是 NULL | 条件写成= NULL | 改成IS NULL |
| 提示标识符无效,但表确实存在 | 大小写敏感,未加引号被转大写 | 统一大写或加双引号 |
UPDATE卡住不返回 | 缺少WHERE条件全表刷 | 补条件,分批提交 |
CASE报类型推导失败 | 所有分支都是 NULL | 加CAST(NULL AS 类型) |
这张表里,大小写那一条值得多说两句。达梦在初始化时可以选择是否大小写敏感,如果选了敏感模式,你建表时用双引号写的"t_order"和t_order会被当成两个不同的对象。迁移脚本如果是从 MySQL 直接搬过来的,表名基本都不会加引号,运气不好就会撞上这个。我的经验是跟 DBA 确认清楚实例的初始化参数,然后统一口径:全部用大写、不加引号,最省心。
5.2 性能和索引上的取舍
CASE WHEN写在 SELECT 列表里,对索引没什么影响,因为它是结果列。但写在WHERE里就要小心了:
-- 这种写法基本用不上索引 WHERE CASE WHEN STATUS = 'PAID' THEN 1 ELSE 0 END = 1 -- 应该直接写成 WHERE STATUS = 'PAID'道理很简单,索引是对列建的,一旦把列包进表达式,优化器就没法用索引去做范围扫描或者等值匹配了。我在一个查询耗时 3 秒的报表 SQL 里把CASE从WHERE挪到 SELECT 列表,耗时降到 200 毫秒以内,差别就是这么大。
如果业务逻辑确实需要根据条件走不同的过滤,可以考虑用UNION ALL拆成两条语句,每条都能走索引,比一个大CASE强。另一种情况是CASE出现在ORDER BY里做自定义排序,这种也走不了索引,但如果排序的数据量不大(几千行以内),实际影响可以忽略,没必要为了这点性能把 SQL 写成天书。
查看执行计划的方式,达梦可以用EXPLAIN前缀,也可以在执行工具里点开执行计划视图,重点看有没有出现全表扫描以及扫描的行数估算。
5.3 几条我自己常用的替代函数
除了CASE WHEN,达梦还有一批条件相关的函数值得放进工具箱。NVL(x, y)是最常用的空值兜底,两个参数,语义清晰;COALESCE(a, b, c, ...)支持多参数,按顺序取第一个非空值,做多级兜底特别顺手。
DECODE是 Oracle 风格的函数,写法紧凑,适合简单映射:
SELECT DECODE(STATUS, 'PAID', '已支付', 'DONE', '已完成', '其他') FROM T_ORDER;但DECODE有一个必须记住的特性:它认为两个 NULL 是相等的。这跟CASE WHEN的行为正好相反。所以如果映射逻辑里涉及空值,用DECODE可能得到和CASE不一样的结果。我一般不混用,全篇统一用CASE WHEN,只在维护老代码时按原样保留DECODE。
NULLIF(a, b)用来在两者相等时返回空值,做除法前判断分母会不会为零时很好用:AMT / NULLIF(CNT, 0),比写一整个CASE简洁。
IFNULL(x, y)这个名字达梦在不少版本里也认,行为和NVL一样,但它只吃两个参数。我的建议是统一用NVL或COALESCE,别依赖IFNULL,因为它是 MySQL 习惯的遗留物,换个版本或者换个实例配置就可能不认,属于给自己埋雷。
最后分享一个我在迁移项目里养成的小习惯:每改完一批 SQL,我会挑几条典型语句,把改造前在 MySQL 里的执行结果和改造后在达梦里的执行结果做逐列比对,包括空值分布和类型。这个动作看着笨,但它拦下过好几次"能跑但结果不对"的问题,尤其是CASE分支顺序写反、空值判断遗漏这类肉眼很难发现的情况。迁移这件事,跑通只是及格线,结果一致才算完成。