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
查历史 SQLSELECT * 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

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐