title: “达梦数据库慢SQL排查与优化实战:从定位到解决的全流程”
tags:

  • 达梦数据库
  • DM8
  • 慢SQL
  • SQL优化
  • 数据库性能
    categories:
  • 数据库
    description: “达梦数据库慢SQL怎么查?本文从动态视图实时监控、慢SQL日志配置、EXPLAIN执行计划分析、到具体优化手段,手把手教你完整的排查与优化流程,适合达梦DBA和开发人员实战参考。”

导读:做过达梦数据库运维的同学应该都遇到过——系统突然变慢,应用频繁超时,最后定位下来就是几条 SQL 在搞鬼。这篇文章把我日常排查达梦慢 SQL 的完整流程整理出来,从"怎么发现慢 SQL"到"怎么优化慢 SQL",尽量覆盖实际工作中的常见场景。


一、先说我的排查思路

我碰到达梦性能问题,一般按这个顺序来:

  1. 实时监控:用动态视图看看当前有没有正在执行的慢 SQL
  2. 日志分析:开启慢 SQL 日志,把历史慢 SQL 记录下来
  3. 执行计划分析:拿到慢 SQL 后用 EXPLAIN 看执行计划,找到瓶颈
  4. 针对性优化:根据执行计划的结果,加索引、改写 SQL、调参数

下面按这个顺序一个个讲。

⚠️ 本文基于 DM8 版本,DM7 大部分操作也类似,但个别参数可能有差异。


二、方法一:动态视图实时排查

这是最快的方式,不需要额外配置,连上数据库就能查。

2.1 看看当前有多少活动会话

SELECT COUNT(*) FROM V$SESSIONS WHERE STATE='ACTIVE';

如果这个数字突然飙升,说明系统压力变大,大概率有慢 SQL 在捣乱。

2.2 查正在执行的慢 SQL

这个查询我经常用,找出已经执行超过 2 秒的 SQL:

SELECT * FROM (
    SELECT
        SESS_ID,
        SQL_TEXT,
        DATEDIFF(SS, LAST_RECV_TIME, SYSDATE) Y_EXETIME,
        SF_GET_SESSION_SQL(SESS_ID) FULLSQL,
        CLNT_IP
    FROM V$SESSIONS
    WHERE STATE='ACTIVE'
) WHERE Y_EXETIME >= 2;

返回字段说明:

字段 含义
SESS_ID 会话 ID,杀会话时要用
SQL_TEXT 正在执行的 SQL 片段
FULLSQL 完整的 SQL 语句
Y_EXETIME 已执行时间(秒)
CLNT_IP 客户端 IP

💡 如果某个 SQL 卡死了需要紧急处理,可以用 SP_CLOSE_SESSION(SESS_ID) 杀掉会话。生产环境慎用,确认没有重要事务再操作。

2.3 SQL_TEXT 显示问号(?)看不到实际参数怎么办

这是达梦 DBA 最头疼的问题之一。因为应用端大多用 PreparedStatement 绑定变量的方式执行 SQL,V$SESSIONS 里的 SQL_TEXTSF_GET_SESSION_SQL() 返回的都是预编译 SQL,参数全是 ?,比如:

SELECT * FROM EMPLOYEE WHERE DEPT_ID = ? AND SALARY > ?

你看不到实际传了什么值,排查问题时就很抓瞎。我总结了几种解决办法:

方法一:开启 SQL 跟踪日志(最靠谱)

达梦的 dmsql 日志文件会记录完整的 SQL 语句和参数信息。开启后日志里能看到带实际参数值的 SQL,而不是占位符。具体配置方法见本文第三章"开启慢 SQL 日志"部分,关键是配置好 sqllog.ini 中的 ITEMS 参数(设为 0 表示记录所有列)。

日志开启后,在 dmsql 日志文件中搜索对应的 SESS 或 TRXID 即可找到完整 SQL:

grep "SESS: 0x7f8a2c001234" /home/dmdba/dmdbms/log/dmsql_DMSERVER_*.log

⚠️ 注意:开启 SQL 日志对性能有影响,排查完记得关掉。

方法二:用 dbms_lob.substr 获取完整 SQL

有时候 SF_GET_SESSION_SQL 返回的 CLOB 被截断了,用 dbms_lob.substr 可以获取完整内容:

SELECT
    DATEDIFF(SS, LAST_RECV_TIME, SYSDATE) SS,
    DBMS_LOB.SUBSTR(SF_GET_SESSION_SQL(SESS_ID)) FULLSQL,
    SQL_ID,
    CURR_SCH,
    SESS_ID,
    TRX_ID,
    CLNT_IP
FROM V$SESSIONS
WHERE STATE = 'ACTIVE'
ORDER BY 1 DESC;

注意:这个方法能拿到完整的 SQL 模板,但参数仍然是 ?,因为绑定变量的值在协议层,动态视图里拿不到。

方法三:通过 V$SQL_HISTORY 查执行记录

SELECT
    SQL_TEXT,
    EXEC_TIME,
    START_TIME,
    SEQNO,
    SESS
FROM V$SQL_HISTORY
WHERE SQL_TEXT LIKE '%EMPLOYEE%'
ORDER BY EXEC_TIME DESC;

V$SQL_HISTORY 中的 SQL_TEXT 同样是预编译的,但可以结合执行时间、客户端 IP 等信息缩小范围,然后去应用日志中根据时间点反查具体参数。

方法四:结合应用日志定位

这是最务实的方法。先用动态视图定位到慢 SQL 的 SQL_TEXT 模板和执行时间,然后去应用的业务日志里根据时间窗口和 SQL 模式反查传入的参数值。

方法五:开启审计功能(终极手段)

如果以上方法都搞不定,可以开启达梦的审计功能,它会记录完整的 SQL 操作信息:

SP_SET_ENABLE_AUDIT(1);

审计记录会写入 V$AUDITRECORDS 视图,包含更详细的执行信息。不过审计对性能影响更大,一般只在万不得已的时候用。

我的建议: 日常排查用方法二 + 方法三快速定位 SQL 模板和执行时间,然后去应用日志反查参数。如果应用日志不够详细,再临时开 SQL 跟踪日志(方法一)抓完整信息。

2.4 查看历史 SQL 执行记录

达梦提供了 V$SQL_HISTORY 视图,可以查看历史执行过的 SQL:

SELECT
    SQL_TEXT,
    EXEC_TIME,
    START_TIME,
    FINISH_TIME
FROM V$SQL_HISTORY
ORDER BY EXEC_TIME DESC
LIMIT 20;

这个视图在排查"昨天那个时段到底哪条 SQL 慢"时特别有用。

2.4 排查锁和阻塞

有时候 SQL 慢不是因为本身执行效率低,而是被别的会话阻塞了。

查锁:

SELECT O.NAME, L.*
FROM V$LOCK L, SYSOBJECTS O
WHERE L.TABLE_ID = O.ID AND BLOCKED = 1;

查阻塞详情(能看到谁阻塞了谁):

SELECT
    DS.SESS_ID "被阻塞的会话ID",
    DS.SQL_TEXT "被阻塞的SQL",
    DS.TRX_ID "被阻塞的事务ID",
    (CASE L.LTYPE
        WHEN 'OBJECT' THEN '对象锁'
        WHEN 'TID' THEN '事务锁'
     END CASE) "被阻塞的锁类型",
    DS.CREATE_TIME "开始阻塞时间",
    SS.SESS_ID "占用锁的会话ID",
    SS.SQL_TEXT "占用锁的SQL",
    SS.CLNT_IP "占用锁的IP",
    L.TID "占用锁的事务ID"
FROM V$SESSIONS DS
INNER JOIN V$LOCK L ON DS.TRX_ID = L.TRX_ID
LEFT JOIN V$SESSIONS SS ON SS.TRX_ID = L.TID
WHERE L.BLOCKED = 1;

找到阻塞源后,如果确认可以杀,执行:

SP_CLOSE_SESSION(占用锁的会话ID);

三、方法二:开启慢 SQL 日志

动态视图只能看当前的,如果要做系统性的慢 SQL 排查,还是得开慢 SQL 日志。

3.1 开启慢 SQL 日志

在 disql 中依次执行以下语句:

-- 设置记录的 SQL 类型(2:3:22:25:28 是常用组合)
SF_SET_SYSTEM_PARA_VALUE('SQL_TRACE_MASK', '2:3:22:25:28', 0, 1);

-- 开启异步刷盘(对性能影响更小)
SF_SET_SYSTEM_PARA_VALUE('SVR_LOG_ASYNC_FLUSH', 1, 0, 1);

-- 设置最小执行时间阈值(单位毫秒,200 表示只记录执行超过 200ms 的 SQL)
SF_SET_SYSTEM_PARA_VALUE('SVR_LOG_MIN_EXEC_TIME', 200, 0, 1);

-- 正式开启慢日志
SP_SET_PARA_VALUE(1, 'SVR_LOG', 1);

3.2 确认配置是否生效

SELECT * FROM V$DM_INI WHERE para_name = 'SVR_LOG';
SELECT * FROM V$DM_INI WHERE para_name = 'SQL_TRACE_MASK';
SELECT * FROM V$DM_INI WHERE para_name = 'SVR_LOG_MIN_EXEC_TIME';
SELECT * FROM V$DM_INI WHERE para_name = 'SVR_LOG_ASYNC_FLUSH';

确认 SVR_LOG 的值为 1 就说明已经开启。

3.3 查看慢日志文件

慢 SQL 日志文件默认在达梦安装目录的 log 子目录下,文件名格式为:

dmsql_实例名_日期_时间.log

例如:

ls -ltr /home/dmdba/dmdbms/log/dmsql*.log

直接用 grep 过滤慢 SQL:

grep -n "select" /home/dmdba/dmdbms/log/dmsql_DMSERVER_20260520_140043.log

3.4 用 DMLOG 工具分析

达梦官方提供了一个日志分析工具 DMLOG,可以把慢 SQL 日志导入后生成可视化的统计报表,包括:

  • SQL 执行时间排行
  • SQL 执行次数统计
  • 时间分布趋势图

如果你的慢 SQL 日志量比较大,手动 grep 效率太低,建议用这个工具。

3.5 用完记得关

慢 SQL 日志对系统性能有影响,排查完之后一定要关掉:

SP_SET_PARA_VALUE(1, 'SVR_LOG', 0);

⚠️ 生产环境不建议长期开启慢 SQL 日志。排查问题时临时开,排查完马上关。

3.6 sqllog.ini 进阶配置

如果你对日志的粒度有更高要求,可以编辑 sqllog.ini 文件(位于数据库实例目录下):

BUF_TOTAL_SIZE = 10240
BUF_SIZE       = 1024
BUF_KEEP_CNT  = 6

[SLOG_ALL]
FILE_PATH      = ../log
PART_STOR      = 0
SWITCH_MODE    = 2
SWITCH_LIMIT   = 128
ASYNC_FLUSH    = 1
FILE_NUM       = 5
ITEMS          = 0
SQL_TRACE_MASK = 1
MIN_EXEC_TIME  = 0
USER_MODE      = 0

几个关键参数解释:

参数 含义 建议值
SWITCH_MODE 日志切换模式,2 表示按大小切换 2
SWITCH_LIMIT 日志文件大小上限(MB) 128
ASYNC_FLUSH 异步刷盘,1 开启 1
FILE_NUM 保留的日志文件数量 5
MIN_EXEC_TIME 最小执行时间阈值(ms) 按需设置

四、拿到慢 SQL 后怎么看执行计划

找到慢 SQL 之后,下一步就是分析执行计划,看它到底慢在哪。

4.1 EXPLAIN 命令

最简单的方式,在 SQL 前面加上 EXPLAIN

EXPLAIN SELECT * FROM EMPLOYEE WHERE DEPT_ID = 10 AND SALARY > 5000;

输出类似这样:

1 #NSET2: [1, 100, 156]
2   #PRJT2: [1, 100, 156]; exp_num(8), is_atom(FALSE)
3     #BLKUP2: [1, 100, 156]; IDX_EMP_DEPT(EMPLOYEE)
4       #SSEK2: [1, 100, 156]; scan_type(ASC), IDX_EMP_DEPT(EMPLOYEE),
            scan_range[(10,min),(10,max)]

4.2 看懂执行计划中的操作符

这是我整理的达梦常见操作符速查表:

操作符 含义 慢的典型原因
CSCN2 全表扫描 没有合适的索引
SSEK2 二级索引扫描 正常,关注回表代价
CSEK2 聚集索引扫描 范围太大
BLKUP2 回表操作 索引覆盖不够
NEST LOOP 嵌套循环连接 驱动表数据量太大
HASH2 INNER JOIN 哈希连接 一般是合理选择
HAGR2 哈希聚合 分组字段没有索引
SORT 排序 排序字段没有索引
PRJT2 投影 通常不慢
SLCT 选择过滤 过滤条件没走索引

4.3 管理工具查看执行计划

如果你用的是达梦自带的管理工具(DM Manager),可以选中 SQL 语句后按 F9 直接查看图形化的执行计划,比纯文本直观很多。

4.4 用 AUTOTRACE 看实际执行统计

在 disql 中可以开启 AUTOTRACE:

SET AUTOTRACE ON;
SELECT * FROM EMPLOYEE WHERE DEPT_ID = 10 AND SALARY > 5000;

这样会同时显示 SQL 的执行结果和执行计划。如果只想看计划不看结果:

SET AUTOTRACE TRACE;

用完关闭:

SET AUTOTRACE OFF;

4.5 用 ET 看每个操作符的实际耗时

EXPLAIN 显示的是预估计划,如果你想看每个操作符实际执行了多久,用 ET:

-- 先确保开启了监控参数
SP_SET_PARA_VALUE(1, 'ENABLE_MONITOR', 1);
SP_SET_PARA_VALUE(1, 'MONITOR_SQL_EXEC', 1);

-- 执行 SQL(注意执行号)
SELECT * FROM EMPLOYEE WHERE DEPT_ID = 10 AND SALARY > 5000;

-- 用执行号查看各操作符耗时
ET 140007513786848;

ET 会显示每个操作符的实际执行时间、处理行数等信息,这对定位瓶颈非常关键。


五、慢 SQL 优化的实战手段

看完执行计划,基本就知道问题在哪了。下面是我常用的几种优化手段。

5.1 加索引——最直接的优化

场景: 执行计划里出现了全表扫描(CSCN2),而且 WHERE 条件中有明确的过滤字段。

CREATE INDEX IDX_EMP_DEPT_SALARY ON EMPLOYEE(DEPT_ID, SALARY);

几个加索引的经验:

  • 把区分度高的字段放前面(比如 DEPT_ID 比 STATUS 区分度高)
  • 如果查询只涉及索引字段,数据库会走索引覆盖,不需要回表(BLKUP2)
  • 不要盲目加索引,索引太多会影响 INSERT/UPDATE 性能
  • 一个表的索引数量我一般控制在 5 个以内

5.2 更新统计信息

场景: 执行计划看起来很离谱,比如预估行数跟实际差了好几个数量级。

达梦的 CBO(基于代价的优化器)依赖统计信息来选择执行计划。如果统计信息过期,优化器就可能做出错误的判断。

-- 收集指定表的统计信息
DBMS_STATS.GATHER_TABLE_STATS('SYSDBA', 'EMPLOYEE');

-- 收集指定索引的统计信息
DBMS_STATS.GATHER_INDEX_STATS('SYSDBA', 'IDX_EMP_DEPT');

-- 收集整个模式的统计信息
DBMS_STATS.GATHER_SCHEMA_STATS('SYSDBA');

💡 我的建议是,在大量数据导入或大批量删除之后,手动收集一次统计信息。日常可以设置定时任务定期收集。

5.3 用 Hint 强制指定执行计划

有时候优化器的选择确实不对,你可以用 Hint 手动引导:

SELECT /*+ INDEX(EMPLOYEE IDX_EMP_DEPT_SALARY) */ *
FROM EMPLOYEE
WHERE DEPT_ID = 10;

SELECT /*+ USE_HASH(E, D) */ *
FROM EMPLOYEE E
JOIN DEPARTMENT D ON E.DEPT_ID = D.ID;

SELECT /*+ NO_INDEX(EMPLOYEE) */ *
FROM EMPLOYEE
WHERE DEPT_ID = 10;

常用的 Hint:

Hint 作用
/*+ INDEX(表名 索引名) */ 强制使用指定索引
/*+ NO_INDEX(表名) */ 禁止使用索引
/*+ USE_HASH(表1, 表2) */ 强制使用哈希连接
/*+ USE_NL(表1, 表2) */ 强制使用嵌套循环连接
/*+ USE_MERGE(表1, 表2) */ 强制使用合并连接

5.4 SQL 改写优化

有些 SQL 写法本身就有效率问题,改写一下效果立竿见影。

**1. 避免 SELECT ***

❌ 低效:

SELECT * FROM EMPLOYEE WHERE DEPT_ID = 10;

✅ 改为只查需要的列:

SELECT ID, NAME, SALARY FROM EMPLOYEE WHERE DEPT_ID = 10;

只查需要的列可以减少数据传输量,还可能走索引覆盖。

2. 用 EXISTS 替代 IN(大数据量子查询时)

❌ 当子查询结果集很大时低效:

SELECT * FROM DEPARTMENT D
WHERE D.ID IN (SELECT DEPT_ID FROM EMPLOYEE WHERE SALARY > 10000);

✅ 改为 EXISTS:

SELECT * FROM DEPARTMENT D
WHERE EXISTS (
    SELECT 1 FROM EMPLOYEE E
    WHERE E.DEPT_ID = D.ID AND E.SALARY > 10000
);

3. 避免在索引列上做函数运算

❌ 索引失效:

SELECT * FROM EMPLOYEE WHERE SUBSTR(NAME, 1, 3) = '张三';

✅ 改写:

SELECT * FROM EMPLOYEE WHERE NAME LIKE '张三%';

4. 避免隐式类型转换

CREATE INDEX IDX_EMP_CODE ON EMPLOYEE(CODE);

❌ CODE 是 VARCHAR 类型,但传了数字,隐式转换导致索引失效:

SELECT * FROM EMPLOYEE WHERE CODE = 10086;

✅ 类型一致:

SELECT * FROM EMPLOYEE WHERE CODE = '10086';

5. 大 IN 列表改为临时表 JOIN

❌ IN 里几百个值,执行计划会出现 CONST VALUE LIST,效率很差:

SELECT * FROM EMPLOYEE WHERE DEPT_ID IN (
    'D001', 'D002', 'D003', ... /* 几百个值 */
);

✅ 用临时表:

CREATE TEMPORARY TABLE TMP_DEPT_IDS (DEPT_ID VARCHAR(20));
INSERT INTO TMP_DEPT_IDS VALUES ('D001');
INSERT INTO TMP_DEPT_IDS VALUES ('D002');
-- 批量插入

SELECT E.* FROM EMPLOYEE E
INNER JOIN TMP_DEPT_IDS T ON E.DEPT_ID = T.DEPT_ID;

5.5 调整数据库参数

如果 SQL 本身没问题,但系统整体还是慢,可能要调整数据库参数。几个常见的:

-- 增大缓冲区(设置为物理内存的 60%-70%)
SP_SET_PARA_VALUE(2, 'BUFFER', 8192);

-- 增大排序区(大量排序场景)
SP_SET_PARA_VALUE(2, 'SORT_BUF_SIZE', 128);

-- 增大共享内存池
SP_SET_PARA_VALUE(2, 'MEMORY_POOL', 1024);

-- 减少缓冲区并发冲突
SP_SET_PARA_VALUE(2, 'BUFFER_POOLS', 19);
参数 含义 调优建议
BUFFER 系统缓冲区大小(MB) 数据量小于内存就设为数据量大小,否则设为内存的 2/3
BUFFER_POOLS 缓冲区分区数 高并发时调大,减少冲突
MEMORY_POOL 共享内存池(MB) 高并发时调大
SORT_BUF_SIZE 排序区大小(MB) 大量排序时调大
RECYCLE 回收缓冲区(MB) 大量临时表、WITH 子句时调大

⚠️ 参数修改后部分需要重启数据库才能生效。修改前做好备份,在测试环境验证后再上生产。


六、一个完整的排查优化案例

假设某天系统告警,查询超时。我来走一遍完整流程。

第一步:实时查看

SELECT * FROM (
    SELECT SESS_ID, SQL_TEXT,
           DATEDIFF(SS, LAST_RECV_TIME, SYSDATE) Y_EXETIME,
           SF_GET_SESSION_SQL(SESS_ID) FULLSQL,
           CLNT_IP
    FROM V$SESSIONS WHERE STATE='ACTIVE'
) WHERE Y_EXETIME >= 5;

发现有一条 SQL 已经执行了 30 多秒:

SELECT * FROM ORDER_INFO WHERE CREATE_TIME >= '2026-01-01'
AND STATUS = 'PENDING' AND AMOUNT > 1000;

第二步:看执行计划

EXPLAIN SELECT * FROM ORDER_INFO
WHERE CREATE_TIME >= '2026-01-01'
AND STATUS = 'PENDING' AND AMOUNT > 1000;

发现执行计划是 CSCN2(全表扫描),ORDER_INFO 表有 500 万行数据。

第三步:检查索引

SELECT * FROM DBA_INDEXES WHERE TABLE_NAME = 'ORDER_INFO';

发现只有一个主键索引,没有业务字段的索引。

第四步:创建组合索引

CREATE INDEX IDX_ORDER_STATUS_TIME_AMT
ON ORDER_INFO(STATUS, CREATE_TIME, AMOUNT);

选择 STATUS 放第一列,因为它的区分度最高(PENDING 只占少量数据)。

第五步:更新统计信息

DBMS_STATS.GATHER_TABLE_STATS('SYSDBA', 'ORDER_INFO');

第六步:验证

再次执行 EXPLAIN,确认走了索引扫描 SSEK2。实际执行时间从 30 秒降到 50 毫秒。


七、速查表

常用排查 SQL 速查

场景 SQL
查活动会话数 SELECT COUNT(*) FROM V$SESSIONS WHERE STATE='ACTIVE';
查慢 SQL(>2秒) 查上面 2.2 节的完整 SQL
查历史 SQL SELECT * FROM V$SQL_HISTORY ORDER BY EXEC_TIME DESC;
查锁 SELECT O.NAME,L.* FROM V$LOCK L,SYSOBJECTS O WHERE L.TABLE_ID=O.ID AND BLOCKED=1;
查执行计划 EXPLAIN <你的SQL>;
收集统计信息 DBMS_STATS.GATHER_TABLE_STATS('模式名','表名');
杀会话 SP_CLOSE_SESSION(会话ID);
开慢日志 SP_SET_PARA_VALUE(1,'SVR_LOG',1);
关慢日志 SP_SET_PARA_VALUE(1,'SVR_LOG',0);

执行计划操作符速查

操作符 含义 出现时关注
CSCN2 全表扫描 是否缺少索引
SSEK2 二级索引扫描 正常,关注回表
BLKUP2 回表 考虑覆盖索引
NEST LOOP 嵌套循环 驱动表是否太大
HASH JOIN 哈希连接 大表连接首选
HAGR2 哈希聚合 分组字段有无索引
SORT 排序 排序字段有无索引

八、常见问题

Q:开启慢 SQL 日志对性能影响有多大?

影响是有的,尤其是高并发场景下。建议开启异步刷盘(SVR_LOG_ASYNC_FLUSH=1),并设置合理的最小执行时间阈值(比如 500ms),只记录真正慢的 SQL。排查完及时关闭。

Q:EXPLAIN 和实际执行时间差异很大怎么办?

EXPLAIN 是预估的执行计划,如果统计信息不准,预估可能和实际差很多。用 ET 执行号 查看每个操作符的实际耗时,或者用 SET AUTOTRACE ON 看实际的执行统计。如果差异很大,先收集统计信息。

Q:加了索引但还是走全表扫描?

几个可能的原因:

  1. 统计信息过期——收集一下统计信息
  2. WHERE 条件上有函数或隐式类型转换——检查 SQL 写法
  3. 数据量太小,优化器认为全表扫描更快——这是正常的
  4. 查询条件不满足索引最左前缀原则——调整索引列顺序

Q:怎么判断是该加索引还是改 SQL?

我的经验是:先看执行计划,如果是全表扫描且 WHERE 条件明确,优先加索引。如果已经走了索引但还是慢,那就要考虑改写 SQL(减少回表、优化 JOIN 方式等)。如果 SQL 本身没问题,考虑调整数据库参数。


参考链接

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐