1. 问题现象
现场发现一条 SQL 执行时间较长,原 SQL 执行时间约 10 分 38 秒。
原 SQL 本身结构较复杂,包含多层关联、聚合、排序、多个子查询以及若干 OR 条件。执行计划节点很多,如果逐节点分析,比较容易失去重点。
原 SQL Statistics:
text
logical reads = 20,259,177
physical reads = 67,656
io wait time = 15,252 ms
exec time = 638,314 ms
实际执行时间:
text
约 638 秒
约 10 分 38 秒
从整体数据可以先得到两个结论:
- SQL 的逻辑读非常高,达到 2000 多万;
- 物理读和 I/O wait 也不低,但和 638 秒总耗时相比,I/O 并不是唯一原因。
因此需要继续通过 ET 定位真正的高耗时节点。
2. 第一步:先看 ET,不直接分析整份执行计划
由于执行计划节点较多,优先通过 ET 看:
text
哪个节点最耗时
哪个节点被反复进入
哪个节点占了大部分执行时间
原 ET 中最明显的两个热点节点为:
text
SEQ 162
OP = CSCN2
TIME ≈ 309,593,645 us
PERCENT ≈ 24.34%
SEQ 168
OP = CSCN2
TIME ≈ 302,683,950 us
PERCENT ≈ 23.79%
两个节点分别耗时:
text
约 309 秒
约 302 秒
两者合计接近整条 SQL 一半的执行时间。
另外,上层还有一个:
text
PRJT2
TIME ≈ 635 秒
PERCENT ≈ 49.95%
这个属于父节点的累计时间,不应该再和下面两个 CSCN2 简单相加。
因此真正需要处理的,是下面两个:
text
MOB_PRESCRIBE_EXECUTE_RECORD
CSCN2
大范围扫描节点。
3. 第二步:定位热点 SQL 条件
执行计划中,两个热点节点都来自:
text
MOB_PRESCRIBE_EXECUTE_RECORD
对应条件主要是:
sql
RE.EXECUTE_SEQ_NO = ?
OR RE.PHYSIC_BARCODE = ?
其中一组还附带:
sql
RE.ABNORMAL_CONTENT IS NOT NULL
AND RE.START_EXECUTE_OPERATOR IS NULL
另一组附带:
sql
RE.START_EXECUTE_OPERATOR IS NOT NULL
真正决定访问路径的核心条件仍然是:
sql
RE.EXECUTE_SEQ_NO = ?
OR RE.PHYSIC_BARCODE = ?
原计划中这两组条件都没有走精准索引定位,而是:
text
#CSCN2:
INDEX33562711(MOB_PRESCRIBE_EXECUTE_RECORD)
btr_scan(1)
累计处理的数据量非常大。
计划中可以看到:
text
MOB_PRESCRIBE_EXECUTE_RECORD
基础数据约 594 万行
而节点累计处理量达到数亿级,说明这个大范围扫描被重复触发了很多次。
4. 第三步:为什么 OR 条件会成为问题
SQL 条件:
sql
RE.EXECUTE_SEQ_NO = ?
OR RE.PHYSIC_BARCODE = ?
如果两个字段都存在合适索引,优化器有机会将 OR 条件展开为两个分支:
text
EXECUTE_SEQ_NO = ?
↓
走 EXECUTE_SEQ_NO 索引
PHYSIC_BARCODE = ?
↓
走 PHYSIC_BARCODE 索引
然后再将两个结果合并。
但是如果只有一侧有索引:
text
EXECUTE_SEQ_NO 有索引
PHYSIC_BARCODE 无索引
优化器就很难同时对 OR 两侧都进行精准定位。
最终可能选择:
text
大范围扫描
+
再判断 OR 条件
这就会使原本很简单的等值查询变成高代价扫描。
5. 第四步:检查 MOB_PRESCRIBE_EXECUTE_RECORD 现有索引
检查现有索引后发现:
text
IDX_MOB_PRESCRIBE_EXECUTE_RECORD_EVENT_NO
(EVENT_NO)
IDX_MOB_PRESCRIBE_EXECUTE_RECORD_PRES_NO
(PRES_NO)
INDEX33562712
(EXECUTE_SEQ_NO)
可以确认:
text
EXECUTE_SEQ_NO 已有单列索引
PHYSIC_BARCODE 没有索引
这与原执行计划的现象能够对应起来。
SQL 中 OR 两侧:
sql
EXECUTE_SEQ_NO = ?
OR PHYSIC_BARCODE = ?
只有第一侧具备索引条件。
因此下一步不需要直接改写 SQL,先补齐缺失索引做最简单验证。
6. 第五步:创建 PHYSIC_BARCODE 单列索引
新增测试索引:
sql
CREATE INDEX IDX_TEST_MOB_EXEC_PHY_BARCODE
ON 对应模式.MOB_PRESCRIBE_EXECUTE_RECORD
(
PHYSIC_BARCODE
);
本次只增加一个字段:
text
PHYSIC_BARCODE
不修改原 SQL。
这样可以清楚验证:
原 SQL 慢是否主要就是因为 OR 条件一侧缺失索引。
7. 优化后执行计划变化
新增 PHYSIC_BARCODE 索引后,原 SQL 不做任何改写,执行计划已经发生明显变化。
原来:
text
EXECUTE_SEQ_NO = ?
OR PHYSIC_BARCODE = ?
↓
CSCN2
MOB_PRESCRIBE_EXECUTE_RECORD
大范围扫描
优化后变为:
text
#UNION FOR OR2
也就是说,达梦优化器自动将 OR 条件进行了展开。
第一组:
text
#UNION FOR OR2
├─ EXECUTE_SEQ_NO 分支
│
│ #BLKUP2
│ INDEX33562712
│
│ #SSEK2
│ INDEX33562712(MOB_PRESCRIBE_EXECUTE_RECORD)
│ scan_range[var17,var17]
│
└─ PHYSIC_BARCODE 分支
#BLKUP2
IDX_TEST_MOB_EXEC_PHY_BARCODE
#SSEK2
IDX_TEST_MOB_EXEC_PHY_BARCODE(MOB_PRESCRIBE_EXECUTE_RECORD)
scan_range[var18,var18]
第二组同样变为:
text
#UNION FOR OR2
├─ EXECUTE_SEQ_NO
│ └─ SSEK2 INDEX33562712
│
└─ PHYSIC_BARCODE
└─ SSEK2 IDX_TEST_MOB_EXEC_PHY_BARCODE
也就是说:
原来一次 OR 判断对应的大范围扫描,被拆成了两个精准的等值索引访问。
8. 为什么只建一个索引就能有这么大提升
这次优化不是因为:
text
MOB_PRESCRIBE_EXECUTE_RECORD 完全没有索引
实际上:
text
EXECUTE_SEQ_NO
原本就有索引。
真正的问题是:
text
OR 条件有两边
一边有索引
一边没索引
原来的情况:
text
EXECUTE_SEQ_NO = ?
有索引
OR
PHYSIC_BARCODE = ?
无索引
导致优化器无法很好地将 OR 两边都转换成高效索引访问。
补充:
text
PHYSIC_BARCODE
索引以后:
text
EXECUTE_SEQ_NO = ?
↓
SSEK2
OR
PHYSIC_BARCODE = ?
↓
SSEK2
两边都可以走索引,优化器就能够自动执行:
text
UNION FOR OR2
从而避免对 594 万级大表进行大范围扫描。
9. 优化后执行情况
优化后整条 SQL:
text
logical reads = 436,433
physical reads = 0
io wait time = 0 ms
exec time = 2,506 ms
实际执行时间约:
text
2.5 秒
相比优化前:
text
638,314 ms
提升约:
text
638314 / 2506 ≈ 254.7 倍
逻辑读:
text
20,259,177
↓
436,433
下降约:
text
97.85%
物理读:
text
67,656
↓
0
I/O 等待:
text
15,252 ms
↓
0 ms
10. 优化前后对比
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 执行时间 | 638,314 ms | 2,506 ms |
| 实际耗时 | 约 10 分 38 秒 | 约 2.5 秒 |
| logical reads | 20,259,177 | 436,433 |
| physical reads | 67,656 | 0 |
| io wait time | 15,252 ms | 0 ms |
| 热点访问方式 | CSCN2 大范围扫描 | SSEK2 精准索引访问 |
| OR 处理方式 | 扫描后判断 | UNION FOR OR2 |
| 提升倍数 | - | 约 254.7 倍 |
11. 优化后 ET 再看热点
优化前最重的两个底层扫描节点:
text
CSCN2
约 309 秒
CSCN2
约 302 秒
优化后已经消失。
新的热点已经下降到几百毫秒级。
也就是说:
text
优化前:
热点节点是 300 秒级
优化后:
热点节点是 0.x 秒级
这说明:
原来的主要瓶颈已经被彻底消除。
12. 本次优化到底优化了什么
这次优化不能简单理解成:
text
建了一个索引,所以快了
更完整的逻辑是:
text
原 SQL 存在 OR 条件
↓
EXECUTE_SEQ_NO 有索引
PHYSIC_BARCODE 无索引
↓
OR 两侧访问能力不对称
↓
优化器无法同时对两侧做精准索引访问
↓
选择 CSCN2 大范围扫描
↓
大表约 594 万行
↓
节点被反复进入
↓
两个热点节点分别消耗约 309 秒、302 秒
补充索引以后:
text
EXECUTE_SEQ_NO 有索引
PHYSIC_BARCODE 也有索引
↓
OR 两侧都可以精准定位
↓
优化器自动 OR 展开
↓
UNION FOR OR2
↓
两个分支分别 SSEK2
↓
大范围 CSCN 消失
↓
638 秒 → 2.5 秒
13. 为什么没有直接手工改写 OR
理论上也可以手工将:
sql
EXECUTE_SEQ_NO = ?
OR PHYSIC_BARCODE = ?
拆成类似:
sql
SELECT ...
WHERE EXECUTE_SEQ_NO = ?
UNION
SELECT ...
WHERE PHYSIC_BARCODE = ?
但本次没有必要。
因为补充索引之后,达梦优化器已经自动生成:
text
UNION FOR OR2
说明优化器本身具备 OR 展开能力。
因此本次最小改动就是:
text
补充缺失索引
不需要改业务 SQL。
这类修改通常比直接大幅重写 SQL 更容易控制风险。
14. 本次排查思路总结
完整排查过程:
text
1. SQL 执行约 10 分 38 秒
↓
2. 执行计划节点很多
↓
3. 先看 ET 找真正热点
↓
4. 发现两个 CSCN2
分别约 309 秒、302 秒
↓
5. 两个热点都来自
MOB_PRESCRIBE_EXECUTE_RECORD
↓
6. 回看条件
↓
7. 发现:
EXECUTE_SEQ_NO = ?
OR
PHYSIC_BARCODE = ?
↓
8. 检查现有索引
↓
9. EXECUTE_SEQ_NO 有索引
PHYSIC_BARCODE 无索引
↓
10. 创建 PHYSIC_BARCODE 单列索引
↓
11. 原 SQL 不改写重新执行
↓
12. 优化器自动 UNION FOR OR2
↓
13. 两侧分别走 SSEK2
↓
14. 两个 300 秒级 CSCN 消失
↓
15. 638 秒 → 2.5 秒
15. 本次优化得到的经验
经验 1:OR 条件要检查两侧是否都有合适索引
例如:
sql
COL_A = ?
OR COL_B = ?
如果:
text
COL_A 有索引
COL_B 没索引
即使其中一侧条件很好,也可能因为另一侧缺失索引,导致整个 OR 条件无法高效执行。
因此看到 OR 时可以优先检查:
text
OR 左侧有没有索引
OR 右侧有没有索引
经验 2:执行计划复杂时,先看 ET 热点
本 SQL 有大量:
text
HASH JOIN
NEST LOOP
SSEK
BLKUP
SORT
HAGR
SPL
如果全部逐节点分析,工作量非常大。
但 ET 很快就能看到:
text
两个 CSCN2
单个超过 300 秒
这样排查方向立即收缩到一张表、两个条件。
经验 3:不要看到"有索引"就停止排查
原表已经存在:
text
EXECUTE_SEQ_NO
索引。
但 SQL 是:
sql
EXECUTE_SEQ_NO = ?
OR PHYSIC_BARCODE = ?
只满足一侧索引并不够。
需要继续看:
text
整个谓词是否都具备合适的访问路径
经验 4:OR 展开可以把一次大扫描变成多次精准索引访问
优化后计划:
text
UNION FOR OR2
可以理解成:
text
OR 条件
↓
拆成多个分支
↓
每个分支单独走最合适的索引
↓
再合并结果
本次就是:
text
EXECUTE_SEQ_NO
走 INDEX33562712
PHYSIC_BARCODE
走 IDX_TEST_MOB_EXEC_PHY_BARCODE
这比扫描整张大表再判断 OR 高效得多。
16. 最终结论
本次慢 SQL 的主要瓶颈是:
MOB_PRESCRIBE_EXECUTE_RECORD上存在EXECUTE_SEQ_NO = ? OR PHYSIC_BARCODE = ?条件,其中EXECUTE_SEQ_NO已有索引,而PHYSIC_BARCODE缺少索引,导致优化器无法对 OR 两侧同时进行精准索引访问,最终产生两个高耗时 CSCN2 大范围扫描节点。
通过新增:
sql
PHYSIC_BARCODE
单列索引后,优化器自动将 OR 条件展开为:
text
UNION FOR OR2
并分别使用:
text
EXECUTE_SEQ_NO 索引
PHYSIC_BARCODE 索引
进行 SSEK2 精准访问。
最终:
text
执行时间:
约 638 秒 → 约 2.5 秒
logical reads:
20,259,177 → 436,433
physical reads:
67,656 → 0
io wait:
15,252 ms → 0
整体性能提升约:
text
254.7 倍
本次优化的核心可以总结为:
OR 条件两侧都具备合适索引后,优化器才能充分利用 OR 展开,将大范围扫描转换为多个精准索引访问。