导读:生产环境的一条 SQL,只返回 16 行记录,却要跑 56 秒——问题不在数据量,而在一个叫 HAGR2(HASH 分组聚集) 的操作符上。本文还原一个真实生产案例的完整优化过程:如何用 trace 定位到慢操作符、如何读懂执行计划里的分组聚集、以及如何用「EXISTS 半连接 + 标量子查询」等价改写,把这条 SQL 从 56 秒打到毫秒级。全程附可复现的 SQL 与实测数据。
在太平费控系统现场,有一条 SQL 执行后只返回 16 行记录,却耗时 56 秒。这种「结果集极小但耗时巨大」的反差,几乎可以直接断定:瓶颈不在返回数据的多少,而在中间过程的重复计算。
这类问题的排查入口不是猜,而是把 SQL 拆开看执行码。达梦提供了 trace 机制,可以精确统计每条 SQL 内部每个操作符的耗时占比。
先确认监控相关参数,并在会话级开启执行监控:
-- 查看 monitor 相关参数
SELECT * FROM v$dm_ini WHERE PARA_NAME LIKE '%MONITOR%';
-- 会话级开启 SQL 执行监控
CALL SF_SET_SESSION_PARA_VALUE('MONITOR_SQL_EXEC', 1);
CALL SF_SET_SESSION_PARA_VALUE('ENABLE_MONITOR_DMSQL', 1);
在管理工具中对原 SQL 做 trace,查看执行码中每个操作符的耗时。结果非常清晰:
| 操作符 | 名称 | 耗时 |
|---|---|---|
| HAGR2 | HASH 分组聚集 | 约 53 秒 |
| 其余(连接、投影等) | — | 约 3 秒 |
结论:这条 SQL 慢就慢在 HAGR2 操作符上,它单独吃掉了 53 秒。HAGR 即 HASH 分组聚集(HASH GROUP BY),是执行
GROUP BY时的典型算子。
沿着执行计划里 71 行对应的 HAGR2 节点,回到 SQL 文本去看具体逻辑。慢的部分是这样的结构:
-- 内联视图:先对 w 表按 document_number 分组,再取每个分组的 max(instance_id)
SELECT h.id, h.apply_number, wwi.instance_id
FROM h_apply h
INNER JOIN (
SELECT MAX(w.instance_id) AS instance_id, w.document_number
FROM wfl_workflow_instance w
WHERE w.status IN ('1', '10')
GROUP BY w.document_number -- ← 这里触发 HAGR2
) wwi
ON h.apply_number = wwi.document_number;
问题的本质在于执行语义的差别:
GROUP BY + MAX:先把 wfl_workflow_instance 表全量扫描、全量分组聚合,算出每个 document_number 的 max(instance_id),物化出一张中间结果,再与 h_apply 做连接。相当于「先把整个账本算完,再按需取用」。EXISTS + 标量子查询:不预聚合,而是对 h_apply 的每一行,按需去 wfl_workflow_instance 里探测,命中即止。当 wfl_workflow_instance 表很大、而 h_apply 驱动行相对有限时,前者的全量聚合成本会被无限放大——这正是 53 秒的来源。
改写思路分两步,把「聚合」拆成「存在性判断」和「按需取值」:
SELECT h.id,
-- 标量子查询:按需取每个单据号的最大 instance_id
(SELECT MAX(w.instance_id)
FROM wfl_workflow_instance w
WHERE w.status IN ('1', '10')
AND h.apply_number = w.document_number
) AS instance_id
FROM h_apply h
WHERE EXISTS ( -- 半连接:只判断"该单据是否存在符合条件的实例"
SELECT 1
FROM wfl_workflow_instance w
WHERE w.document_number = h.apply_number
AND w.status IN ('1', '10')
);
改写后重新执行,执行时长直接降到毫秒级,wfl_workflow_instance 表走 document_number 上的二级索引。
#NSET2: [89989462, ...]
#PRJT2
#HAGR2: [89989462, ...]; grp_num(4), sfun_num(1) -- 全量 HASH 分组聚合
#HASH2 INNER JOIN KEY(W.DOCUMENT_NUMBER = H.APPLY_NUMBER)
#CSCN2: [631, 5000000, ...] w 表全表扫描 500 万行 ← 慢点①
#CSCN2: [237, 2000000, ...] h 表全表扫描 200 万行 ← 慢点②
三个慢点一目了然:
w 表 CSCN2 全表扫描 500 万行;h 表 CSCN2 全表扫描 200 万行;#NSET2: [1194, 5000, ...]
#PIPE2
#PRJT2
#HASH2 INNER JOIN
#NEST LOOP INDEX JOIN2 -- h 表驱动,w 表逐行索引探测
#CSCN2: [237, 2000000, ...] h 表
#BLKUP2 / #SSEK2: 索引探测
#SPL2 / #AAGR2 -- 标量子查询逐行取 MAX
#HASH RIGHT SEMI JOIN2
#BLKUP2 / #SSEK2: w 走 document_number 二级索引
改写后:
w 表不再全表扫,而是通过 document_number 索引 SSEK2 索引范围探测;EXISTS 半连接命中即止,无需对全部实例做聚合;max(instance_id) 改为标量子查询逐行聚合,避免一次性物化海量中间结果;为了固化这个优化手法,在测试环境搭建了同型场景(wjh-test-db3,端口 5636):
| 对象 | 规模 |
|---|---|
wfl_workflow_instance |
500 万行,document_number 建二级索引 |
h_apply |
200 万行 |
三种 SQL 形态的实测耗时:
| 形态 | SQL 结构 | 执行时间 | 相对提速 |
|---|---|---|---|
| A(原版) | 内联视图 GROUP BY 聚合 + JOIN | 15.90 s | 1× |
| B(改写1) | EXISTS 半连接 | 865 ms | ≈ 18× |
| C(改写2) | EXISTS + 标量子查询取 MAX | 739 ms | ≈ 21× |
三者结果集一致(1,200,000 行),改写前后语义等价 ✓。生产案例原 56 秒 → 毫秒级,量级与本模拟一致(秒级 → 毫秒级)。
| 校验点 | 说明 |
|---|---|
| 过滤条件一致 | 原 WHERE status IN ('1','10') 与改写后的过滤条件完全相同 |
| 存在性判断一致 | INNER JOIN 本身有「匹配才保留」的语义,EXISTS 半连接完全承接 |
| 取最大值一致 | 原 GROUP BY document_number 取 MAX(instance_id),改写后用标量子查询逐行取 MAX,结果一致 |
| 走对索引 | wfl_workflow_instance.document_number 上有二级索引,改写后才能走 SSEK2 探测 |
补充:如果业务只需要「存在性判断」、不需要携带
max(instance_id),用 B 形态即可;若需携带聚合值,用 C 形态(EXISTS + 标量子查询)。
GROUP BY + MAX 与 EXISTS + 标量子查询 是两种完全不同的执行哲学:前者「先全量算再匹配」,后者「按需探测」。驱动行相对有限、被驱动表很大时,后者优势呈数量级。CSCN2(全表扫)、HAGR2(哈希分组聚集)、SSEK2(二级索引扫描)、SEMI JOIN(半连接)这几个操作符,把慢的机理讲得清清楚楚。文章
阅读量
获赞
