
从接手达梦数据库运维和开发工作的那段时间起我就一直想写一篇关于达梦 SQL 函数的实战总结。达梦数据库DM作为国产数据库里使用率很高的一款语法大方向兼容 Oracle但实际用起来并不是“完全复刻”函数部分尤其容易踩坑。很多人从 MySQL、SQL Server 或 Oracle 转到达梦时最容易在函数这里卡住因为同名函数可能有不同行为或者达梦有自己的函数体系比如 LISTAGG、WM_CONCAT、PIPE 等等。这篇内容不搞官方手册那一套我把日常开发、运维中真正用到的达梦 SQL 函数按场景分类整理出来附带实测经验和避坑点。适合谁看一种是刚接触达梦、正在做应用迁移的 Java 开发或 DBA另一种是已经用了一段时间但总在 SQL 函数上报错的人。文章里的每一个函数示例我都尽量贴近实际业务场景而不是拿 test 表跑 select 1。读完你至少能少翻十次手册遇到报错也能自己判断是函数写法问题还是数据类型问题。1. 达梦SQL函数初印象为什么它总被人说“像Oracle”1.1 兼容Oracle的大方向与本地化差异达梦数据库从设计之初就走了一条很务实的路线把 Oracle 的常用语法尽量兼容同时保留自己的实现方式。这意味着大部分 Oracle SQL 函数比如 NVL、DECODE、TO_DATE、ROWNUM、分析函数在达梦里直接跑基本没问题。这也解释了为什么很多从 Oracle 迁移过来的系统改造成本比从 MySQL 迁到达梦低不少。但如果你以为“Oracle 能跑的到达梦一定能跑”迟早会在某些特殊函数上栽跟头。比较典型的是字符串聚合函数Oracle 11g 用 WM_CONCAT12c 以后推荐 LISTAGG达梦这两个函数都支持但细节行为有差异再比如某些 Oracle 内置包和函数达梦有自己的实现方式比如 DBMS_RANDOM、UTL_FILE 这些包达梦也提供但函数签名和返回格式可能不完全一致。1.2 “函数行为不一致”的三种典型翻车现场根据我的实际经验达梦 SQL 函数造成的线上问题绝大多数集中在三类场景第一类是类型转换精度问题。比如 Oracle 里 TO_NUMBER(1,000) 在某些会话参数下可以成功达梦默认对千分位字符串的容错度不同直接执行可能报无效数字。第二类是空值处理差异NVL2、NULLIF、COALESCE 这些函数在不同数据库中的语义基本一致但涉及多个参数、嵌套使用时容易混乱尤其从 MySQL 迁移过来的人习惯 IFNULL达梦里没有 IFNULL要用 NVL 或 COALESCE。第三类是分析函数 ORDER BY 中字段类型不匹配比如在 RANK() 里对日期类型使用默认排序或者在 LAG 函数里对字符串字段指定数值偏移量这类报错信息往往不直观容易让人误判。这些坑不踩一次很难长记性。所以我后文每个函数都会带一句“和 Oracle/MySQL 的差异提醒”希望帮你把潜在问题提前排查掉。2. 最常用的内建函数分类实测与注意事项2.1 字符串函数常用但最容易“想当然”字符串处理是所有 SQL 开发者的日常达梦的字符串函数整体上让你很舒服因为它在很多命名上跟 Oracle 保持一致。比如 SUBSTR、INSTR、LENGTH、REPLACE、LPAD、RPAD、TRIM、LTRIM、RTRIM、ASCII、CHR这些函数直接照搬 Oracle 经验完全没问题。举例来说我要从员工编号 EMP_NO格式比如 EMP20240001中截取数字部分用 SUBSTRSELECT SUBSTR(EMP_NO, 4) AS DIGIT_PART FROM T_EMPLOYEE WHERE EMP_NO EMP20240001;如果还想知道数字部分是不是 8 位可以用 LENGTH 配合 INSTR 判断SELECT LENGTH(SUBSTR(EMP_NO, 4)) AS LEN_DIGIT FROM T_EMPLOYEE;这里有一个细节容易踩坑达梦的 SUBSTR 起始位置如果为 0效果跟 1 相同都是从头开始截取。Oracle 也是这个逻辑但 SQL Server 的 SUBSTRING 起始位置从 1 开始如果写 0 会返回空串。所以从 SQL Server 迁移到达梦的人遇到既有代码里 SUBSTRING(字段, 0, 3) 这种写法必须改成 SUBSTR(字段, 1, 3)否则结果会完全不对而且不报错排查起来非常隐蔽。INSTR 函数用于找子串位置常用于判断字段是否包含某个关键字。比如查所有部门名称里包含“技术”两个字的记录SELECT DEPT_NAME FROM T_DEPT WHERE INSTR(DEPT_NAME, 技术) 0;等价写法是 LIKE %技术%但 INSTR 的好处是还能同时返回位置信息在某些逻辑里更灵活。达梦的 INSTR 默认从第 1 个字符开始搜索第 4 个参数控制出现次数比如找第二个逗号的位置SELECT INSTR(A,B,C,D, ,, 1, 2) FROM DUAL;结果是 4因为第一个逗号在位置 2第二个逗号在位置 4。这个参数很多人用了很久才发现其实在解析 CSV 字符串时特别好用。拼接字符串方面达梦支持双竖线 || 作为连接符也支持 CONCAT 函数。比如拼完整地址SELECT PROVINCE || CITY || STREET AS FULL_ADDR FROM T_ADDRESS;需要注意 CONCAT 函数在达梦里只能传两个参数如果传多个会报参数个数错误。MySQL 的 CONCAT 支持多个参数迁移到达梦时要把 CONCAT(a, b, c) 改成 CONCAT(CONCAT(a, b), c) 或者直接用 a || b || c。从可读性来说我推荐一律使用 ||语义清晰也贴近 Oracle。LPAD 和 RPAD 在生成长度固定的流水号时非常实用。比如把订单号补足 12 位不足左侧补 0SELECT LPAD(ORDER_NO, 12, 0) AS PADDED_ORDER FROM T_ORDER;但如果字段长度已经超过 12 位LPAD 会从右侧开始截断这个行为和 Oracle 一致结果可能出乎意料。建议先确认字段内容长度再用 LPAD 做固定长度标准化。TRIM 系列相对简单默认去掉首尾空格也可以指定去掉特定字符SELECT TRIM(X FROM XXABCXX) FROM DUAL;返回 ABC。这里的重点是 FROM 关键字顺序看不清容易写反。2.2 日期时间函数格式化与计算的老大难问题日期函数是达梦里迁移改造工作量最大的一块。因为每个数据库的日期处理逻辑差别都很大哪怕同样是 Oracle 风格达梦还是有一些自己的行为。最常用的是 SYSDATE 和 NOW()。达梦里 SYSDATE 返回当前日期时间NOW() 也可以。如果只要日期部分可以用 CURDATE() 或者直接对 SYSDATE 做 TRUNC 处理SELECT SYSDATE, TRUNC(SYSDATE) AS TODAYS_DATE FROM DUAL;注意 TRUNC 支持对日期按精度截断比如按天、按月、按年SELECT TRUNC(SYSDATE, MM) AS MONTH_BEGIN, TRUNC(SYSDATE, YYYY) AS YEAR_BEGIN FROM DUAL;这对于按月分组的报表统计非常方便。日期转字符串用 TO_CHAR字符串转日期用 TO_DATE这两个函数的格式模型兼容 Oracle 大部分习惯。比如需要把日期转换为 2024-06-15 14:30:00 的格式SELECT TO_CHAR(SYSDATE, YYYY-MM-DD HH24:MI:SS) FROM DUAL;注意 HH24 表示 24 小时制不是简单写 HH。如果写 HH12在下午 2 点会显示 02。这个坑我在导出报表时踩过凌晨的数据看不出问题下午的数据就会“小时数对不上”客户会质疑数据准确性。字符串转日期更是高频操作尤其是从 Excel 导入或外部接口传入数据时SELECT TO_DATE(2024-06-15 14:30:00, YYYY-MM-DD HH24:MI:SS) FROM DUAL;如果字符串格式与格式模型不匹配达梦会直接报 ORA-01830 风格的错误但报错信息不一定直观有时只是提示“无效的日期”。这时候优先检查月份、日期部分是否写反比如 YYYY-DD-MM 和 YYYY-MM-DD 在月份和日期数字恰好合法时比如 12 月 5 日和 5 月 12 日不会报错但结果错极其危险。日期加减运算也是日常必备。达梦里日期可以直接加减数字默认按天计算SELECT SYSDATE 1 AS TOMORROW, SYSDATE - 7 AS WEEK_AGO FROM DUAL;要加小时、分钟可以除以 24 或 1440。比如加 3 小时SELECT SYSDATE 3 / 24 AS PLUS_3H FROM DUAL;要加月份、年份用 ADD_MONTHS。比如查询一年前的记录SELECT * FROM T_ORDER WHERE ORDER_DATE ADD_MONTHS(SYSDATE, -12);ADD_MONTHS 如果月底加一个月得到下个月月底比如 1 月 31 日加一个月得到 2 月 28 日或 29 日这点跟 Oracle 一致很多人第一次用会以为算错了其实是正常的“月末对齐”逻辑。月份差用 MONTHS_BETWEEN返回小数比如SELECT MONTHS_BETWEEN(TO_DATE(2024-06-15, YYYY-MM-DD), TO_DATE(2023-12-01, YYYY-MM-DD)) FROM DUAL;结果是 6.45...因为 12 月 1 日到 6 月 15 日不是整整 6 个月。如果只要整月数可以先 TRUNC 再做 MONTHS_BETWEEN或者配合 FLOOR。日期函数还有两个简便方法EXTRACT 取年、月、日、时、分、秒。比如按年分组统计SELECT EXTRACT(YEAR FROM ORDER_DATE) AS YR, COUNT(*) FROM T_ORDER GROUP BY EXTRACT(YEAR FROM ORDER_DATE);EXTRACT 在处理“按时间分区”的自动化任务时很好用比 TO_CHAR 再 TO_NUMBER 利落得多。最后一个容易忽略的是 LAST_DAY返回指定日期所在月的最后一天SELECT LAST_DAY(TO_DATE(2024-02-10, YYYY-MM-DD)) FROM DUAL;2024 年是闰年返回 2024-02-29非闰年返回 2024-02-28。做账周期、账单日计算都能用上。2.3 数值函数与类型转换别被精度和四舍五入坑到数值函数方面达梦也和 Oracle 走得很近。ROUND 四舍五入、TRUNC 直接截断、CEIL 向上取整、FLOOR 向下取整、MOD 取余、ABS 绝对值、POWER 幂运算等使用时按 Oracle 习惯即可。但真正容易出问题的是 ROUND 的第二个参数。ROUND(545.267, 2) 返回 545.27这个没问题但 ROUND(545.267, -1) 返回 550也就是四舍五入到十位。如果对负数位参数不敏感可能会在金额计算时得到和预期差距很大的结果。比如计算折扣价SELECT ROUND(1999 * 0.8, -2) AS DISCOUNT_PRICE FROM DUAL;返回 1600而不是 1599.2因为它是四舍五入到百位。这个场景风险很高建议金额计算一律指定非负小数位数。MOD 取余函数有个细节当除数为 0 时会报除零错误。业务里如果除数可能是 0先用 NULLIF 或 CASE 判断SELECT MOD(10, NULLIF(0, 0)) FROM DUAL;NULLIF(0, 0) 返回 NULLMOD 传入 NULL 返回 NULL不会报错。这个技巧在做分摊金额时很实用可以避免报表突然跑挂。TRUNC 对数值只做截断不四舍五入SELECT TRUNC(999.989, 2) FROM DUAL;返回 999.98不是 999.99。所有涉及金额、税率计算的场景一定要先确定业务需要“四舍五入”还是“截断”否则财务核对时会对不上账。CAST 和 TO_NUMBER 是类型转换的主力。CAST 适合标准 SQL 迁移比如把字符串转成整数SELECT CAST(123 AS INT) FROM DUAL;CAST 对日期时间的支持也比较标准。TO_NUMBER 则更灵活可以指定格式SELECT TO_NUMBER(1,234.56, 999,999.99) FROM DUAL;这里格式模型里的 9 代表可替换数字0 代表强制显示。如果字符串格式与模型不匹配会报错。从文件导入数据时带逗号千分位的数字必须用 TO_NUMBER 加格式模型转换直接用 CAST 会失败。2.4 判空与逻辑函数NVL、NVL2、COALESCE、DECODE达梦的判空函数对从 Oracle 迁移过来的开发者是最友好的部分因为语法几乎一致。NVL 用于空值替换SELECT NVL(PHONE, 无) FROM T_EMPLOYEE;NVL 的两个参数类型必须兼容。如果字段是日期类型替换值必须写成 TO_DATE 或直接用字符串日期否则隐式转换可能把日期变成奇怪的格式。比如SELECT NVL(END_DATE, TO_DATE(9999-12-31, YYYY-MM-DD)) FROM T_CONTRACT;如果直接写 9999-12-31达梦会尝试把字符串转换成日期逻辑上往往能跑通过但如果有索引就可能因为隐式转换导致索引失效。这点在达梦里和 Oracle 类似都值得注意。NVL2 是 NVL 的增强版三个参数分别表示“非空时返回值”和“空时返回值”SELECT NVL2(REMARK, 有备注, 无备注) FROM T_ORDER;COALESCE 支持多个参数返回第一个非空值SELECT COALESCE(HOME_PHONE, MOBILE, OFFICE_PHONE, 无联系方式) FROM T_EMPLOYEE;这是在多联系方式场景下最简单高效的写法比嵌套 NVL 清晰太多。DECODE 是等值判断的利器类似 CASE WHEN 的简写。比如把状态码 1、2、3 转为可读标签SELECT DECODE(STATUS, 1, 新建, 2, 处理中, 3, 完成, 未知) AS STATUS_TEXT FROM T_ORDER;从性能角度看DECODE 和 CASE WHEN 在达梦里差别不大选择哪种主要看可读性。如果逻辑复杂、涉及范围判断用 CASE WHEN 更好如果只是等值映射DECODE 更简洁。3. 分析函数窗口函数的实操拆解3.1 排名窗口ROW_NUMBER、RANK 与 DENSE_RANK 的差异分析函数是达梦的一大亮点因为 Oracle 的窗口函数语法在达梦里基本能直接复用。这对于做排名、同比环比、分组 TopN 报表来说太重要了。先说最常用的排名三兄弟。ROW_NUMBER 生成连续且不重复的行号适合做分页去重SELECT EMP_NO, SALARY, ROW_NUMBER() OVER (ORDER BY SALARY DESC) AS RN FROM T_EMPLOYEE;RANK 相同值排名相同但后续排名会跳号。比如两个并列第一下一个排名是 3SELECT EMP_NO, SALARY, RANK() OVER (ORDER BY SALARY DESC) AS RNK FROM T_EMPLOYEE;DENSE_RANK 相同值排名相同但后续排名不跳号。两个并列第一下一个排名是 2SELECT EMP_NO, SALARY, DENSE_RANK() OVER (ORDER BY SALARY DESC) AS DENSE_RNK FROM T_EMPLOYEE;业务场景里如果发奖要按名次人数来控制名额用 RANK如果只是展示排名且要求名次连续用 DENSE_RANK。分页去重、给行标号固定用 ROW_NUMBER。通过 PARTITION BY 可以按组排名。比如按部门分组后取每个部门工资最高的员工SELECT * FROM ( SELECT DEPT_ID, EMP_NO, SALARY, ROW_NUMBER() OVER (PARTITION BY DEPT_ID ORDER BY SALARY DESC) AS RN FROM T_EMPLOYEE ) WHERE RN 1;这个写法在日常取“每个分组最新一条记录”“每个分组最大值记录”时特别常用。关键点是内层查询把窗口函数算好外层再过滤 RN 1。如果直接在 WHERE 里写 ROW_NUMBER() 1达梦不支持必须嵌套子查询。很多人刚接触窗口函数时会犯这个错。3.2 聚合窗口SUM、AVG、COUNT 的累计与滚动计算窗口聚合函数算移动平均、累计销售额、占比等场景的利器。比如计算每个部门人数占全公司人数的比例SELECT DEPT_ID, EMP_NO, COUNT(*) OVER (PARTITION BY DEPT_ID) AS DEPT_CNT, COUNT(*) OVER () AS TOTAL_CNT, COUNT(*) OVER (PARTITION BY DEPT_ID) / COUNT(*) OVER () AS PCT FROM T_EMPLOYEE;注意 COUNT(*) OVER () 不加 PARTITION 时统计的是整个结果集行数。达梦对这个语法的支持很稳定可以放心用。经典的累计求和写法比如展示每月累计销售额SELECT MONTH, AMOUNT, SUM(AMOUNT) OVER (ORDER BY MONTH) AS CUM_AMOUNT FROM T_SALES ORDER BY MONTH;这里重点来了ORDER BY MONTH 在窗口内默认是从第一行到当前行所以等价于一个累计范围。如果不写 ORDER BY那么 SUM 作用于整个分组每一行看到的都是分组总和。这个区别想明白了窗口聚合就掌握了一半。移动平均比如最近 3 个月的平均销售额用 ROWS BETWEENSELECT MONTH, AMOUNT, AVG(AMOUNT) OVER (ORDER BY MONTH ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS MOV_AVG FROM T_SALES;ROWS BETWEEN 2 PRECEDING AND CURRENT ROW 表示取当前行及前两行共 3 行做平均。这种窗口帧frame语法在达梦里完全支持和 Oracle 一致。做库存周转、流量波动分析时很好用。还有 MAX、MIN 的窗口版本常用于计算“每条记录与分组最大值之间的差值”SELECT EMP_NO, SALARY, MAX(SALARY) OVER (PARTITION BY DEPT_ID) AS DEPT_MAX, MAX(SALARY) OVER (PARTITION BY DEPT_ID) - SALARY AS GAP FROM T_EMPLOYEE;3.3 位移与取值窗口LAG、LEAD、FIRST_VALUE、LAST_VALUELAG 和 LEAD 用于访问同一分组内前一行或后一行的值。比如计算环比差异SELECT MONTH, AMOUNT, LAG(AMOUNT, 1, 0) OVER (ORDER BY MONTH) AS PREV_AMOUNT, AMOUNT - LAG(AMOUNT, 1, 0) OVER (ORDER BY MONTH) AS DIFF FROM T_SALES;LAG(AMOUNT, 1, 0) 里 1 表示偏移 1 行0 表示没有前一行时默认返回 0。这个默认值参数很关键如果省略没有前一行时返回 NULL做减法时结果就是 NULL报表展示可能不好看但暴露的数据问题恰恰更真实。LEAD 用于向后取行比如要比较当前月份与下一个月SELECT MONTH, AMOUNT, LEAD(AMOUNT, 1) OVER (ORDER BY MONTH) AS NEXT_AMOUNT FROM T_SALES;FIRST_VALUE 和 LAST_VALUE 常用于分组内取首尾记录。比如每个部门第一个入职和最后一个入职的员工SELECT DEPT_ID, EMP_NO, HIRE_DATE, FIRST_VALUE(EMP_NO) OVER (PARTITION BY DEPT_ID ORDER BY HIRE_DATE) AS FIRST_EMP, LAST_VALUE(EMP_NO) OVER (PARTITION BY DEPT_ID ORDER BY HIRE_DATE) AS LAST_EMP FROM T_EMPLOYEE;这里有个坑LAST_VALUE 默认窗口范围是到当前行所以如果不显式指定 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWINGLAST_VALUE 的“最后一行”可能不是整个分组最后一行而是当前行导致结果不符合预期。建议用 LAST_VALUE 时显式写明窗口范围LAST_VALUE(EMP_NO) OVER ( PARTITION BY DEPT_ID ORDER BY HIRE_DATE ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS LAST_EMP这个细节在 Oracle 里同样存在但在达梦里更容易被忽略因为很多人迁移测试时只查了一两条数据数据量一上去就发现问题。3.4 分析函数的兼容性与排序关键字注意事项达梦对分析函数的支持总体很完整但有一点要特别提醒窗口函数里的 ORDER BY 字段如果存在重复值排名函数的结果可能不稳定。所以做排名、累计统计时最好在 ORDER BY 里加入一个唯一字段作为第二排序键。比如ROW_NUMBER() OVER (PARTITION BY DEPT_ID ORDER BY SALARY DESC, EMP_NO ASC)这样保证排序完全确定结果可复现。这在离线报表、数据对账场景中非常重要因为同样的 SQL 在不同时间跑出的行号必须一致。分析函数的别名不能在 WHERE 中使用只能在 ORDER BY 或外层查询里使用。这是标准 SQL 规则但频繁有人踩坑写 WHERE RN 1 报“无效列名”其实解决方案就是子查询套一层。我自己也犯过这个错当时排查了快十分钟才发现是 SQL 执行顺序的问题。4. 特殊函数与复杂场景实战4.1 行转列的两条路线LISTAGG 与 WM_CONCAT业务中经常需要把一组的多个值拼成一个字符串比如把某个部门所有员工姓名拼在一起。达梦支持两种写法LISTAGG 和 WM_CONCAT。LISTAGG 是标准推荐写法SELECT DEPT_ID, LISTAGG(EMP_NAME, ,) WITHIN GROUP (ORDER BY EMP_NO) AS EMP_NAMES FROM T_EMPLOYEE GROUP BY DEPT_ID;WITHIN GROUP (ORDER BY EMP_NO) 控制拼接顺序比如希望按入职时间排序拼。WM_CONCAT 是老写法达梦也支持但排序控制不如 LISTAGG 方便SELECT DEPT_ID, WM_CONCAT(EMP_NAME) AS EMP_NAMES FROM T_EMPLOYEE GROUP BY DEPT_ID;WM_CONCAT 的拼接顺序在 Oracle 中是不推荐的因为它没有标准 ORDER BY 控制达梦里同样是这个情况。所以全新开发或者数据排序敏感的场景建议直接使用 LISTAGG。LISTAGG 需要注意超长问题。如果拼接结果超过 VARCHAR 上限会报错。达梦里可以先用 MAX 判断长度或使用 CLOBSELECT DEPT_ID, LISTAGG(EMP_NAME, ,) WITHIN GROUP (ORDER BY EMP_NO) AS EMP_NAMES FROM T_EMPLOYEE GROUP BY DEPT_ID;如果报“结果字符串太长”可以改用 XMLAGG 或者截断也可以把目标字段 CAST 成 CLOB。但 CLOB 在分组字段、排序字段等方面限制较多建议业务上控制拼接行数或分批处理。4.2 PIVOT 与 UNPIVOT行列转换的高级玩法行列转换除了用聚合函数加 CASE WHEN 手工拼达梦还支持 Oracle 风格的 PIVOT 和 UNPIVOT。比如按月把销售额转成多列SELECT * FROM ( SELECT MONTH, AMOUNT FROM T_SALES ) PIVOT ( SUM(AMOUNT) FOR MONTH IN (1 AS M1, 2 AS M2, 3 AS M3) );这里 PIVOT 后面 SUM(AMOUNT) 是聚合方式FOR MONTH IN (1, 2, 3) 是转成列的枚举值。如果月份不确定可以用动态 SQL 拼。PIVOT 的麻烦点是必须穷举枚举值不能自动生成列这点和 Oracle 一致。UNPIVOT 把多列转成一列。比如把三列分数合并成一列SELECT * FROM ( SELECT EMP_NO, SCORE_1, SCORE_2, SCORE_3 FROM T_SCORE ) UNPIVOT ( SCORE FOR SUBJECT IN (SCORE_1 AS 语文, SCORE_2 AS 数学, SCORE_3 AS 英语) );UNPIVOT 常用于把宽表转成窄表便于后续统计分析。需要注意列的数据类型必须一致如果各不相同要先 CAST 统一。PIVOT 和 UNPIVOT 在实际运维中评率不高但一遇到就能省大量代码。如果业务报表团队大量使用 Excel 透视表这两个函数就是数据库端的透视表实现值得掌握。4.3 管道函数 PIPE 与自定义函数复杂转换的扩展手段达梦支持用 PL/SQL 写自定义函数和存储过程也支持定义管道函数用于按行流式返回结果集。管道函数 PIPE ROW 在 SQL 中的用法是从表函数角度解决复杂转换的典型手段。例如创建一个管道函数返回一张生成的数字表CREATE OR REPLACE FUNCTION FN_GEN_NUM(P_CNT INT) RETURN INT PIPELINED AS BEGIN FOR I IN 1..P_CNT LOOP PIPE ROW(I); END LOOP; RETURN; END;查询时配合 TABLE 关键字SELECT * FROM TABLE(FN_GEN_NUM(10));这个功能在做序列补齐、模拟行数时很实用。比如某天没产生订单但报表需要每天一行记录就要用管道函数生成日期序列再左关联订单表。自定义普通函数也很常用比如按身份证号计算年龄CREATE OR REPLACE FUNCTION FN_GET_AGE(P_ID_CARD VARCHAR2) RETURN INT AS V_BIRTH VARCHAR2(8); BEGIN V_BIRTH : SUBSTR(P_ID_CARD, 7, 8); RETURN FLOOR(MONTHS_BETWEEN(SYSDATE, TO_DATE(V_BIRTH, YYYYMMDD)) / 12); END;需要注意自定义函数如果用在 WHERE 条件或大表关联上可能引发性能问题因为函数没办法走常规索引。日常开发中尽量少在过滤条件里使用自定义函数或者改成虚拟列加函数索引的方式优化。达梦对函数索引的支持跟 Oracle 类似需要时可以用。4.4 递归查询与层级函数组织架构场景实战组织架构表查询是管理信息系统里绕不开的场景。达梦支持 START WITH ... CONNECT BY PRIOR 语法也支持 WITH RECURSIVE 递归 CTE。前者是 Oracle 风格后者是 SQL 标准风格。Oracle 风格查所有子部门SELECT DEPT_ID, DEPT_NAME, PARENT_ID FROM T_DEPT START WITH DEPT_ID D001 CONNECT BY PRIOR DEPT_ID PARENT_ID;这里 CONNECT BY PRIOR DEPT_ID PARENT_ID 表示“上一行的 DEPT_ID 等于当前行的 PARENT_ID”所以是从父行往子行递归。如果要从子部门往上找所有父级写成SELECT DEPT_ID, DEPT_NAME, PARENT_ID FROM T_DEPT START WITH DEPT_ID D100 CONNECT BY PRIOR PARENT_ID DEPT_ID;PRIOR 位置不同递归方向就相反。这个语法刚接触时需要反复理解但我个人经验是多画两棵树结构跑两次查询就清楚了。递归 CTE 在达梦里也同样支持WITH RECURSIVE DEPT_TREE(DEPT_ID, DEPT_NAME, PARENT_ID, LVL) AS ( SELECT DEPT_ID, DEPT_NAME, PARENT_ID, 1 FROM T_DEPT WHERE PARENT_ID IS NULL UNION ALL SELECT D.DEPT_ID, D.DEPT_NAME, D.PARENT_ID, T.LVL 1 FROM T_DEPT D JOIN DEPT_TREE T ON D.PARENT_ID T.DEPT_ID ) SELECT * FROM DEPT_TREE;递归 CTE 的好处是容易控制层级字段 LVL也能在递归过程中做更多处理。两者的性能没有绝对优劣关键是看是否有合适的索引。无论如何递归查询都建议限制层级深度避免循环引用导致死循环。达梦对 CONNECT BY 的循环检测比较友好但递归 CTE 如果数据本身存在环可能需要在递归体内加条件控制深度。5. 常见SQL函数报错排查与性能优化实录5.1 高频报错速查表与解决思路我把日常工作中遇到的达梦 SQL 函数报错整理成了一张速查表希望能帮你快速定位问题报错现象常见原因解决方案无效的列名在 WHERE 里用了窗口函数别名用子查询包裹外层过滤无效的关系运算符日期类型比较时格式隐式转换失败用 TO_DATE 统一格式数值溢出或除数为零MOD 除数为 0或 ROUND 位数参数过大用 NULLIF 防除零检查位数参数截断字符串或二进制数据VARCHAR 长度不足LISTAGG 超长扩大字段长度或使用 CLOB结果值太大TO_NUMBER 转换时格式与数据不匹配加上格式模型检查千分位缺失右括号函数嵌套时括号未配对尤其多层 DECODE使用编辑器括号高亮无效的日期TO_DATE 格式模型与实际字符串不一致用 DATETIME 类型时检查毫秒字段这张表看着简单但每条都能对应真实事故。比如有一次定时任务在凌晨报“无效的关系运算符”查了半天发现是 ORACLE 兼容模式下日期字符串和日期字段直接比较时出了问题最终改成 TO_DATE 显式转换才解决。5.2 函数使用与SQL性能优化心得函数写对了性能也可能成为瓶颈。我在达梦上做过一次报表优化把执行时间从 40 秒压到 2 秒主要动作跟函数相关分享几个原则。第一原则尽量避免在 WHERE 条件的字段上套函数。比如按年度查询SELECT * FROM T_ORDER WHERE EXTRACT(YEAR FROM ORDER_DATE) 2024;这样写功能没错但无法使用 ORDER_DATE 上的索引。改成范围查询SELECT * FROM T_ORDER WHERE ORDER_DATE TO_DATE(2024-01-01, YYYY-MM-DD) AND ORDER_DATE TO_DATE(2025-01-01, YYYY-MM-DD);范围查询能走索引性能立竿见影。这个优化思路对达梦、Oracle、MySQL 都适用。第二原则分析函数与大表关联时注意数据量。窗口函数会在内存或临时表空间里维护排序数据量巨大时有换出风险。如果确实要做全量排名可以先过滤到最小结果集再进行排名计算。第三原则LIKE 模糊查询中以通配符开头时无法走到索引SELECT * FROM T_EMPLOYEE WHERE EMP_NAME LIKE %张%;这是函数场景外的经典问题。如果有频繁前导模糊匹配需求可以结合应用搜索组件或注入函数索引但函数索引对 LIKE 的支持有限建议业务上尽量避免。第四原则避免大数据量下使用 DISTINCT 配合多个函数嵌套。DISTINCT 本身需要排序去重如果每行里又有 TO_CHAR、TO_DATE 等函数计算性能会成倍下降。可以先缩小数据范围再做去重。5.3 达梦SQL函数开发工具与调试建议达梦自带的 DM 管理工具DTS、DM Manager在 SQL 调试方面支持还可以但很多网上的热词搜索都围绕“DM 管理工具怎么用”说明大家还是容易在工具层面犯难。我的建议是日常写 SQL 用 DM 管理工具或者 DBeaver 连接达梦都行只要 JDBC 驱动配置正确。调试存储过程和自定义函数时DM 管理工具里可以打印变量打日志辅助排查。也可以在代码里用 DBMS_OUTPUT.PUT_LINE 输出中间结果跟 Oracle 的方式一致。测试函数是否可用建议单独开一个会话窗口避免长事务锁表影响其他请求。热词里提到的“dm 表锁住了怎么解锁”多数情况就是长事务未提交导致的排查方式可以查 V$LOCK 或 V$SESSIONS杀掉阻塞会话但前提是确认会话可以安全终止。关于 hikrcp 连接池配置因为达梦用 HikariCP 连接池的人非常多配置上要注意 URL 格式、驱动类名、验证语句。达梦的 JDBC URL 一般是 jdbc:dm://IP:PORT验证查询语句可以配置 SQL 查询 SELECT 1。有人习惯配 SELECT 1 FROM DUAL在达梦里也没问题但 SELECT 1 更轻量。6. 写在最后我踩过最深的两次坑最后分享两个我印象最深的踩坑记录希望后来者能绕开。第一次是做客户库存报表需求是“查询最近 30 天每天每个仓库的库存数量”我一开始用 LAG 取前一天库存做环比但结果总是跟财务手工算的对不上。排查了很久发现仓库某天没有进出库记录时流水表里根本没有这一行LAG 取到的“前一天”其实是“上一次有记录的那天”中间间隔可能不是 1 天导致环比算错。解决方案不是修函数而是先用日期维表把 30 天每一天补全再关联库存流水。这个案例提醒我窗口函数只对结果集“可见行”生效不会自动补行。第二次是迁移 Oracle 系统到达梦时一段使用了嵌套 DECODE 的复杂 SQL 在 Oracle 里运行正常到达梦后报错。细看发现达梦对 DECODE 参数类型的自动提升规则更严格比如参数里混合了 NUMBER 和 VARCHAR2Oracle 会隐式转换达梦某些模式下要求显式转换。最终我改成 CASE WHEN 消除了问题。从那以后我在适配达梦的项目里有一条不成文的规定多层嵌套的 DECODE 一律改写成 CASE WHEN宁可多写几行也不让隐式转换的变量留在代码里。达梦 SQL 函数体系整体成熟度已经相当高和 Oracle 的高度兼容也降低了迁移成本。但任何数据库都不是另一个数据库的复制品只有真正上手写、跑、调才能摸透它的脾气。希望这篇整理能让你在达梦 SQL 函数的路上少走几步弯路。