SQL 优化排查记录:补充索引,利用 OR 展开消除大表重复扫描

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 秒

从整体数据可以先得到两个结论:

  1. SQL 的逻辑读非常高,达到 2000 多万;
  2. 物理读和 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 展开,将大范围扫描转换为多个精准索引访问。

相关推荐
九皇叔叔1 天前
《MySQL 体系架构详解:Server 层、SQL 执行流程与 InnoDB、MyISAM、MEMORY 存储引擎》
sql·mysql·架构
这个DBA有点耶2 天前
SQL:2023新特性详解:JSON_TABLE、GREATEST/LEAST、QUALIFY 到底怎么用?
数据库·sql·mysql
小小龙学IT2 天前
Qt 数据库编程(Qt SQL 模块)深度解析
数据库·sql·qt
疯狂打码的少年2 天前
【数据库技术】SQL概述与数据定义(DDL:CREATE/DROP/ALTER)
数据库·笔记·sql·oracle
Blockchina2 天前
Codex安全盲区:我用4组本地测试复盘SQLi、XSS、越权与路径穿越
sql·安全·xss
山峰哥3 天前
数据库工程与查询优化案例深度复盘‌
数据库·sql·oracle·编辑器·深度优先·宽度优先
茶栀(*´I`*)3 天前
数据库零基础入门指南:从基本概念到 SQL 实战
数据库·sql
zcn1263 天前
exists子查询改写思考
数据库·sql·sql优化改写
黄俊懿3 天前
【架构师从入门到进阶】第五章:DNS&CDN&网关优化思路——第八节:网关-注入攻击与预防
sql·网络安全·架构·系统架构·架构师·架构设计