MySQL迁移达梦:IF与CASE WHEN条件判断改写指南 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 tCASE WHEN a b THEN 1 ELSE 0 END达梦不返回布尔型列多值映射CASE / ELT / FIELDDECODE或CASEDECODE 认为两个空值相等行转列CASE WHENGROUP 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分支顺序写反、空值判断遗漏这类肉眼很难发现的情况。迁移这件事跑通只是及格线结果一致才算完成。