达梦数据库这几年在很多行业的系统里出现频率越来越高我最早接触它是因为某项目要求国产化改造当时第一反应是“又一个要重新学的Oracle变种”但实际用下来发现达梦对Oracle语法的兼容度做得相当到位很多习惯性写法直接搬过去就能跑。不过它毕竟不是Oracle查询语句里的一些细节、函数差异和隐藏坑还是值得单独整理一遍。这篇文章我按日常开发最常用的场景来梳理基础查询、去重、过滤排序、聚合分组、表连接、子查询、分页以及实际使用中最容易翻车的几个点。每段都会给具体的SQL示例和说明尽量做到拿来即用同时把“为什么这么写”讲清楚。1. 搭好查询环境连接工具与模式概念1.1 客户端选择和连接方式达梦数据库官方提供的管理工具叫“达梦数据库管理工具”图形界面做得中规中矩主要用于表结构查看、用户权限配置和执行查询。日常写SQL我习惯直接用命令行工具disql它的交互方式和Oracle的sqlplus很接近支持历史命令、脚本执行、结果格式化输出适合批量操作和排查问题。连接命令基本是这样disql SYSDBA/SYSDBAlocalhost:5236默认端口是5236用户名和密码按实际环境填SYSDBA是安装时自动创建的最高权限账号。连接成功后可以通过SELECT * FROM V$VERSION;查看版本信息。1.2 模式与用户的关系达梦里面有一个概念需要先弄清楚模式。在达梦中一个用户对应一个默认模式模式名通常和用户名相同。比如用户TEST登录后默认模式就是TEST查询自己的表可以直接写表名查询其他用户的表则要带模式名前缀像SELECT * FROM SCOTT.EMP;。这个机制和Oracle几乎一模一样好处是从Oracle迁移过来的代码基本不用改坏处是如果项目里用了多个业务用户写SQL时经常容易漏掉模式前缀导致报“表或视图不存在”。我的习惯是在所有跨用户查询里显式写模式名不做任何省略避免因为默认模式切换而踩坑。1.3 大小写敏感性达梦默认情况下标识符不区分大小写但存储时会被统一转为大写。这意味着你创建表时写create table user_info查询时写SELECT * FROM USER_INFO和SELECT * FROM user_info效果一样。真正容易出问题的是字符串内容比如查询条件里的WHERE name Tom和WHERE name tom是两回事这和表名规则完全不同。提示从其他数据库迁移过来时如果原库用驼峰命名且开启了大小写敏感设置达梦里的表名通常会变成带双引号的写法例如userInfo这种表名的查询必须加双引号且严格匹配大小写。这是迁移项目里最常见的坑之一。2. 基础SELECT查询核心语法与常用写法2.1 SELECT基本结构达梦的SELECT语法主干和标准SQL一致由SELECT、FROM、WHERE、GROUP BY、HAVING、ORDER BY几个子句构成执行逻辑顺序则是FROM→WHERE→GROUP BY→HAVING→SELECT→ORDER BY。理解这个顺序对排查慢查询很有帮助比如WHERE里可以用别名吗答案是不行因为WHERE执行时SELECT的别名还没生成。最基础的查询写法-- 查询单表全部字段 SELECT * FROM TEST.T_EMPLOYEE; -- 查询指定字段字段会按列出顺序返回 SELECT EMP_ID, EMP_NAME, DEPT_ID FROM TEST.T_EMPLOYEE; -- 查询时去重 SELECT DISTINCT DEPT_ID FROM TEST.T_EMPLOYEE;2.2 表达式、别名与虚拟表查询语句里可以包含各种表达式比如字段直接拼接、数值计算、函数处理。最常用的是用别名让输出更可读SELECT EMP_NAME AS 姓名, SALARY * 12 AS 年薪, 在职 || 员工 AS 状态 FROM TEST.T_EMPLOYEE;注意这里用了双竖线||作为字符串连接符这是Oracle和达梦共有的习惯写法和MySQL的CONCAT()不同。达梦和Oracle一样支持FROM DUAL这种无表查询形式用于执行与表无关的计算或取系统常量SELECT SYSDATE, 11 FROM DUAL; SELECT USER FROM DUAL;SYSDATE返回当前时间USER返回当前登录用户。这类写法在拼接动态SQL、生成临时数据、快速验证函数时非常好用。2.3 WITH子句的用法复杂查询建议先用WITH把中间结果抽出来再用主查询引用可读性会好很多而且达梦会把中间结果物化重复引用时能减少扫描次数。WITH EMP_DEPT AS ( SELECT D.DEPT_NAME, COUNT(*) CNT FROM TEST.T_EMPLOYEE E JOIN TEST.T_DEPT D ON E.DEPT_ID D.DEPT_ID GROUP BY D.DEPT_NAME ) SELECT * FROM EMP_DEPT WHERE CNT 10;这种写法在写报表统计、多步计算的时候非常实用比嵌套子查询清晰得多。3. 条件过滤与排序NULL语义和ROWNUM的坑3.1 WHERE条件的基础操作条件过滤最常用的是等值匹配、范围匹配和模糊匹配三类-- 等值匹配 SELECT * FROM TEST.T_EMPLOYEE WHERE DEPT_ID D001; -- 范围匹配 SELECT * FROM TEST.T_EMPLOYEE WHERE SALARY BETWEEN 5000 AND 15000; -- 模糊匹配%任意多个字符_单个字符 SELECT * FROM TEST.T_EMPLOYEE WHERE EMP_NAME LIKE 张%; SELECT * FROM TEST.T_EMPLOYEE WHERE EMP_NAME LIKE _明; -- IN列表匹配 SELECT * FROM TEST.T_EMPLOYEE WHERE DEPT_ID IN (D001,D002,D003);模糊匹配里的%和_是两种不同的通配符%张表示以“张”结尾%张%表示包含“张”。如果业务上查的就是百分号本身可以用ESCAPE关键字指定转义符比如LIKE %100%% ESCAPE %这一般在排查数据脏值时才用得上。3.2 NULL过滤的三种写法在SQL里NULL不是“空字符串”而是“不确定的值”。跟NULL做任何比较运算结果都是不确定的所以WHERE SALARY NULL永远查不到数据必须用IS NULL或IS NOT NULL-- 查没有部门的人员 SELECT * FROM TEST.T_EMPLOYEE WHERE DEPT_ID IS NULL; -- 查有部门的人员 SELECT * FROM TEST.T_EMPLOYEE WHERE DEPT_ID IS NOT NULL;还有一种批量替换NULL的写法用NVL函数SELECT EMP_NAME, NVL(DEPT_ID, 未分配) AS DEPT_ID FROM TEST.T_EMPLOYEE;NVL(a, b)的意思是如果a是NULL则返回b否则返回a本身。在报表导出这类场景里很常用能避免前端显示空值。提示写查询条件时不要对可空字段写!或不等于的排除条件NULL行会被自动排除。比如WHERE DEPT_ID ! D001不会返回DEPT_ID IS NULL的记录如果业务上需要排除指定部门同时保留未分配部门必须写成WHERE DEPT_ID ! D001 OR DEPT_ID IS NULL。3.3 ROWNUM的两个典型翻车场景达梦兼容Oracle的ROWNUM伪列用于限制返回行数但它有一个很隐蔽的坑ROWNUM是在结果集生成过程中逐行分配的只有当前行被返回时下一行才会得到编号。所以WHERE ROWNUM 5这种写法永远查不到数据WHERE ROWNUM BETWEEN 2 AND 5也不行。正确用法只有两种-- 取前10行 SELECT * FROM TEST.T_EMPLOYEE WHERE ROWNUM 10; -- 先排序再取前10 SELECT * FROM ( SELECT * FROM TEST.T_EMPLOYEE ORDER BY SALARY DESC ) WHERE ROWNUM 10;另外达梦还兼容Oracle的rownum写法之外也支持标准SQL的FETCH FIRST N ROWS ONLY子句这个在第8部分专门讲。排序本身用ORDER BY子句支持多列、升降序、NULL位置控制SELECT * FROM TEST.T_EMPLOYEE ORDER BY DEPT_ID ASC, SALARY DESC; -- NULLS FIRST / NULLS LAST 控制NULL排序位置 SELECT * FROM TEST.T_EMPLOYEE ORDER BY DEPT_ID ASC NULLS LAST;4. 去重查询DISTINCT与GROUP BY的适用边界4.1 DISTINCT去重去重是查询语句的高频需求。最直接的方式是SELECT DISTINCT它会对查询结果中所有返回字段的组合做去重-- 查所有部门ID重复的只留一个 SELECT DISTINCT DEPT_ID FROM TEST.T_EMPLOYEE; -- 多字段组合去重部门岗位都相同才视为重复 SELECT DISTINCT DEPT_ID, POSITION FROM TEST.T_EMPLOYEE;使用上要注意两点。第一DISTINCT作用于后面所有字段的组合不是只对第一个字段去重第二DISTINCT和ORDER BY一起用时排序列必须是SELECT列表里的字段否则会报错。4.2 GROUP BY去重与统计的结合当去重的同时还需要统计数量GROUP BY就是更自然的写法SELECT DEPT_ID, COUNT(*) AS EMP_CNT FROM TEST.T_EMPLOYEE GROUP BY DEPT_ID;这个查询的结果里每个部门只出现一行同时带出人数。如果只想去重而没有任何聚合计算DISTINCT性能更好因为它不需要走分组聚合的逻辑而一旦要算数量、平均值、总和这类指标就必须用GROUP BY。4.3 去重时的空值处理DISTINCT和GROUP BY对NULL的处理一致所有NULL值会被视为同一组。比如SELECT DISTINCT DEPT_ID所有DEPT_ID为NULL的行只返回一行。这在做数据清洗统计时要格外留意因为你可能以为“未分配部门”的明细还在但去重后它已经只剩一条了。另外还有一个高频需求是“按某个字段去重但取整行数据”比如“查每个部门工资最高的员工”。DISTINCT做不了这件事常见做法是窗口函数ROW_NUMBER()加外查询过滤SELECT * FROM ( SELECT E.*, ROW_NUMBER() OVER(PARTITION BY DEPT_ID ORDER BY SALARY DESC) RN FROM TEST.T_EMPLOYEE E ) T WHERE RN 1;ROW_NUMBER()按部门分组、按工资排序后编号外部过滤只保留每组第一行。这类需求在业务报表里出现频率很高建议直接记下这个模板。5. 聚合统计与分组求结果别让HAVING拖慢你的报表5.1 常用聚合函数达梦的聚合函数和标准数据库一致最常用的五个SELECT COUNT(*) AS 总人数, COUNT(DEPT_ID) AS 已分配部门人数, SUM(SALARY) AS 工资总额, AVG(SALARY) AS 平均工资, MAX(SALARY) AS 最高工资, MIN(SALARY) AS 最低工资 FROM TEST.T_EMPLOYEE;这里有一个细节值得展开COUNT(*)统计的是所有行数而COUNT(DEPT_ID)只统计该字段非NULL的行数。只要这个字段是NULL它就不会被计入这在核对数据时非常容易产生偏差。比如“查询已分配部门的员工数”和“查询总员工数”两个数字如果对不上一定要先想到是不是NULL在作怪。5.2 分组统计的写法聚合函数本身不带分组时所有行会作为一组返回一行结果。配合GROUP BY可以做维度统计SELECT DEPT_ID, COUNT(*) AS EMP_CNT, ROUND(AVG(SALARY), 2) AS AVG_SAL FROM TEST.T_EMPLOYEE GROUP BY DEPT_ID;GROUP BY执行顺序在WHERE之后所以如果想先过滤一部分数据再统计把条件写在WHERE里即可SELECT DEPT_ID, COUNT(*) AS EMP_CNT FROM TEST.T_EMPLOYEE WHERE STATUS 在职 GROUP BY DEPT_ID;5.3 HAVING的正确打开方式HAVING用于对分组后的结果做条件过滤跟随GROUP BY出现。它的关键区别在于WHERE过滤的是原始行而HAVING过滤的是聚合后的组。SELECT DEPT_ID, COUNT(*) AS EMP_CNT FROM TEST.T_EMPLOYEE GROUP BY DEPT_ID HAVING COUNT(*) 10;这里COUNT(*)不是SELECT里的别名而是HAVING子句里重新写了一遍聚合表达式。理论上有些数据库支持引用别名但我建议不要依赖这一点老老实实写完整表达式最稳妥。提示能用WHERE完成的条件一定不要放到HAVING里。比如“只统计在职员工”直接在WHERE写STATUS在职先过滤再分组数据量小时和HAVING区别不大但数据量大时先过滤能明显减少分组工作量报表性能差距就在这里体现的。5.4 ROLLUP与CUBE的场景化使用达梦也支持ROLLUP和CUBE扩展分组做统计报表时非常有用。比如按部门岗位统计人数同时还想看部门小计和全体总计SELECT DEPT_ID, POSITION, COUNT(*) AS EMP_CNT FROM TEST.T_EMPLOYEE GROUP BY ROLLUP(DEPT_ID, POSITION);结果集会额外生成小计行DEPT_ID有值而POSITION为NULL的行就是部门小计两列都为NULL的行就是总计。在做Excel式的报表数据源时这个功能能省很多应用层拼数据的代码。6. 表连接查询旧式写法与ANSI标准写法并存6.1 内连接的两种写法多表查询是日常开发离不开的达梦同时兼容两种连接语法一种是WHERE里写连接条件的旧式写法一种是JOIN ... ON的标准写法。-- 旧式写法Oracle风格 SELECT E.EMP_NAME, D.DEPT_NAME FROM TEST.T_EMPLOYEE E, TEST.T_DEPT D WHERE E.DEPT_ID D.DEPT_ID; -- ANSI标准写法 SELECT E.EMP_NAME, D.DEPT_NAME FROM TEST.T_EMPLOYEE E JOIN TEST.T_DEPT D ON E.DEPT_ID D.DEPT_ID;两种写法语义相同都是把两个表中满足连接条件的行匹配起来。我个人的建议是新写的SQL一律用JOIN ... ON因为连接条件和过滤条件分离可读性更强而且LEFT JOIN和RIGHT JOIN的表顺序关系一目了然。6.2 外连接的兼容写法Oracle旧式的外连接写法是用()标记补空侧的表达梦也支持-- Oracle旧式左连接右表补空 SELECT E.EMP_NAME, D.DEPT_NAME FROM TEST.T_EMPLOYEE E, TEST.T_DEPT D WHERE E.DEPT_ID D.DEPT_ID();这个写法非常容易读反。()放在哪一侧哪一侧就是“被补空”的表。更推荐用标准写法SELECT E.EMP_NAME, D.DEPT_NAME FROM TEST.T_EMPLOYEE E LEFT JOIN TEST.T_DEPT D ON E.DEPT_ID D.DEPT_ID;左连接的意思是左表全保留右表匹配不上就用NULL补齐。项目上有很多老代码是从Oracle直接迁移过来的阅读时如果看到()要能马上翻译成对应的LEFT JOIN语义。6.3 连接条件放到ON还是WHERE这是一个非常容易出错的地方。对于INNER JOIN连接条件写在ON和WHERE里结果一样但对于LEFT JOIN条件位置不同会导致完全不同的结果-- 用法A先把右表过滤再连接 SELECT E.EMP_NAME, D.DEPT_NAME FROM TEST.T_EMPLOYEE E LEFT JOIN TEST.T_DEPT D ON E.DEPT_ID D.DEPT_ID AND D.STATUS 启用; -- 用法B连接后再过滤右表 SELECT E.EMP_NAME, D.DEPT_NAME FROM TEST.T_EMPLOYEE E LEFT JOIN TEST.T_DEPT D ON E.DEPT_ID D.DEPT_ID WHERE D.STATUS 启用;用法A中右表不满足STATUS启用时仍然会保留左表记录DEPT_NAME显示为NULL用法B则会把不满足条件的整行都过滤掉结果等同于内连接。这个细节在改造老报表时经常引起数据对不上务必留意。6.4 多表连接的执行逻辑三张表以上的连接理论上按从左到右的顺序两两连接。达梦的优化器会自行调整连接顺序一般不需要人工干预但有一种情况需要特别注意——大表驱动问题。如果一张千万级的大表参与连接建议把它放在连接顺序的后面优先用过滤条件缩小其中一张表后再进行连接。这个也可以用ORDER BY、WHERE过滤、子查询等方式配合实现。7. 子查询与IN的报错现场从NOT IN返回空到列表膨胀7.1 子查询的三种基本形态子查询可以出现在WHERE、FROM、SELECT三个位置分别扮演过滤条件、虚拟表、计算值的角色-- WHERE子查询查询工资高于平均工资的员工 SELECT * FROM TEST.T_EMPLOYEE WHERE SALARY (SELECT AVG(SALARY) FROM TEST.T_EMPLOYEE); -- FROM子查询把聚合结果当表用 SELECT * FROM ( SELECT DEPT_ID, COUNT(*) AS CNT FROM TEST.T_EMPLOYEE GROUP BY DEPT_ID ) T WHERE CNT 10; -- SELECT子查询作为计算列输出 SELECT EMP_NAME, (SELECT DEPT_NAME FROM TEST.T_DEPT D WHERE D.DEPT_ID E.DEPT_ID) AS DEPT_NAME FROM TEST.T_EMPLOYEE E;第一种形态是标量子查询返回单值第二种是内联视图第三种是关联子查询每一行都要执行一次性能较差大表上慎重使用。7.2 IN查询的一个经典报错使用达梦时经常遇到IN查询报“结果集过大”或“超出上限”的错误尤其是在传入大量参数时-- 列表项过多时报错 SELECT * FROM TEST.T_EMPLOYEE WHERE DEPT_ID IN (D001,D002,...D999);达梦对IN列表项的数量有硬性限制不同版本限制不太一样但超过一定数量常见说法是单条语句中表达式个数限制就会触发报错。解决办法有三类一是把大列表拆成多段用OR连接二是先建一张临时表把列表数据插入再通过JOIN关联三是用 ANY()语法替代SELECT * FROM TEST.T_EMPLOYEE WHERE DEPT_ID ANY(D001,D002,D003);7.3 NOT IN的NULL陷阱如果说IN报错是好解决的那NOT IN的NULL陷阱就是最容易让人抓狂的。看这个查询-- 查没在D001、D002部门的员工 SELECT * FROM TEST.T_EMPLOYEE WHERE DEPT_ID NOT IN (D001,D002);只要T_EMPLOYEE表里存在DEPT_ID为NULL的行这个查询的结果就是空集。原因很简单NOT IN底层等价于! ALL()当列表里或待比较行里出现NULL整个比较结果变为不确定FALSE于是什么都不返回。这是SQL三值逻辑的经典产物不是达梦独有的bug。解决办法是显式排除NULL或者改用NOT EXISTSSELECT * FROM TEST.T_EMPLOYEE E WHERE NOT EXISTS ( SELECT 1 FROM TEST.T_DEPT D WHERE D.DEPT_ID IN (D001,D002) AND D.DEPT_ID E.DEPT_ID );我在项目里遇到过一次严重的数据对账问题两边报表查出来人数差了一大截最后排查到根因就是NOT IN遇到NULL导致大量记录被静默过滤。从此之后凡是涉及排除逻辑的查询我默认优先写NOT EXISTS可读性虽然差一点但至少结果可靠。7.4 EXISTS与IN的性能对比当子查询的结果集很大时IN通常会先把子查询结果全部求出再匹配而EXISTS是逐行判断是否存在很多人因此得出“EXISTS永远比IN快”的结论。实际上要看数据分布如果外层表大、子查询结果小IN往往更快如果外层表小、子查询结果大EXISTS更有优势。现代优化器一般会自动做等价改写不必太纠结但有一点是确定的——确保子查询涉及的关联字段有索引。8. 分页、字符串与日期处理的经验补充8.1 分页查询的三种写法达梦支持多种分页写法最简单的就是用FETCH ... OFFSET标准语法SELECT * FROM TEST.T_EMPLOYEE ORDER BY EMP_ID OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;这表示跳过前20行取接下来10行即第21到30条正好对应分页场景下“第一页每页10条第三页从21条开始”的需求。需要特别注意分页查询必须带ORDER BY否则数据库不保证每次返回的排列顺序一致翻页时会出现重复或遗漏。Oracle老项目迁移过来的代码里更多见的是ROWNUM两层嵌套写法SELECT * FROM ( SELECT T.*, ROWNUM RN FROM ( SELECT * FROM TEST.T_EMPLOYEE ORDER BY EMP_ID ) T WHERE ROWNUM 30 ) WHERE RN 20;两层ROWNUM的写法内层限制最大行号外层排除前序行。与FETCH FIRST相比这种写法在复杂排序时偶尔会带来额外成本但胜在兼容性好。如果项目是新写的直接用OFFSET ... FETCH就好代码更简洁。8.2 字符串处理的常见函数实际业务中字符串函数的使用频率可能比聚合函数还高。达梦里这几组函数要熟-- 字符串截取下标从1开始 SELECT SUBSTR(ABCDEF, 2, 3) FROM DUAL; -- 结果是 BCD -- 字符串拼接 SELECT ABC || DEF FROM DUAL; -- 结果是 ABCDEF -- 查找位置 SELECT INSTR(ABCDEF, CD) FROM DUAL; -- 结果是 3 -- 替换 SELECT REPLACE(ABCABC, BC, X) FROM DUAL; -- 结果是 AXAX -- 去空格 SELECT TRIM( ABC ) FROM DUAL;注意SUBSTR的下标从1开始这是一个非常容易与Java等编程语言的0起始下标混淆的地方。INSTR查不到时返回0这也可以用来做模糊匹配的进阶判断。8.3 日期格式化与计算日期查询最常用的是TO_CHAR把日期转成指定格式的字符串以及TO_DATE把字符串解析成日期-- 日期转字符串 SELECT TO_CHAR(SYSDATE, YYYY-MM-DD HH24:MI:SS) FROM DUAL; -- 字符串转日期 SELECT TO_DATE(2024-06-01, YYYY-MM-DD) FROM DUAL; -- 查询某一天的数据 SELECT * FROM TEST.T_EMPLOYEE WHERE CREATE_DATE TO_DATE(2024-06-01, YYYY-MM-DD) AND CREATE_DATE TO_DATE(2024-06-02, YYYY-MM-DD);日期范围查询建议用“大于等于当天零点小于次日零点”这种左闭右开写法而不是WHERE TO_CHAR(CREATE_DATE, YYYY-MM-DD) 2024-06-01后者在字段上套了函数索引会失效数据量大了会非常慢。8.4 CASE WHEN做逻辑分支最后补充一个非常有用的条件表达式CASE WHEN可以在查询结果里做逻辑分支相当于SQL里的if-elseSELECT EMP_NAME, SALARY, CASE WHEN SALARY 10000 THEN 高薪 WHEN SALARY 5000 THEN 中等 ELSE 待提升 END AS LEVEL_NAME FROM TEST.T_EMPLOYEE;CASE WHEN按顺序匹配命中第一个满足条件的分支后不再继续判断。它可以出现在SELECT列表、WHERE条件、ORDER BY子句里量身定做排序规则时尤其好用比如把“管理层”字段值为1的记录排在最前再按工资降序这样SQL里不需要应用层二次处理。达梦数据库日常查询用到的内容差不多就是这些。很多细节看似琐碎但在实际项目里NULL语义、ROWNUM边界、NOT IN陷阱、IN列表上限这些问题随便踩中一个都会让你排查很久。按这篇整理的套路去写至少能避开绝大多数坑。最后再分享一个我自己长期在用的习惯每个报表或接口的SQL写完先用小数据量验证结果集再放到生产环境跑涉及NULL判断的地方优先考虑显式处理而不是依赖默认行为——你永远不会知道哪条数据的某个字段在某个角落里悄悄变成了NULL。