SQL 优化排查记录:补充联合索引,避免大范围扫描

1. 问题现象

现场发现一条由多个 UNION ALL 组成的查询执行较慢。

SQL 中大部分分支执行都比较轻,真正耗时较高的是其中"取消入科"相关分支。该分支除患者和科室条件外,还包含:

sql 复制代码
费用汇总 = 0

以及:

sql 复制代码
NOT EXISTS

形式的医嘱校验逻辑。

其中重点关注的 SQL 片段如下:

sql 复制代码
AND NOT EXISTS (
    SELECT 1
      FROM ZOEPRES.PRES_INP_PRES_RECORD pres
     WHERE pres.PRES_STATUS_CODE NOT IN ('9', '7')
       AND a.PATIENT_ID = pres.PATIENT_ID
       AND pres.ITEM_CODE NOT IN (
           SELECT dc.CONTROL_CODE
             FROM ZOEDICT.DIC_CLINIC_CONTROL_FLOW_CONFIG dc
            WHERE dc.CONTROL_MODE = '0002'
       )
       AND a.EVENT_NO = pres.EVENT_NO
)

原 SQL 执行情况:

text 复制代码
logical reads  = 206731
physical reads = 42384
io wait time   = 5177 ms
exec time      = 12631 ms

实际执行时间约:

text 复制代码
12.6 秒

2. 第一步:查看执行计划

原执行计划非常长,包含多个 UNION ALL 分支。

这种情况下不适合从第一行开始逐个节点看,而是应该先配合 ET 找真正的热点节点。

原执行计划中,PRES_INP_PRES_RECORD 出现了两个明显的大范围访问节点:

text 复制代码
#PARALLEL
  #CSCN2: INDEX33560750(PRES_INP_PRES_RECORD)

累计处理数据量约:

text 复制代码
4,811,732 行

而且类似扫描出现了两次。

也就是说,同一张大表被重复进行了大范围索引扫描。


3. 第二步:通过 ET 确认热点

原 ET 中最重的两个节点均来自:

text 复制代码
PRES_INP_PRES_RECORD

关键数据:

text 复制代码
第一个 CSCN2:
TIME    ≈ 8.72 秒
PERCENT ≈ 69.73%

第二个 CSCN2:
TIME    ≈ 2.86 秒
PERCENT ≈ 22.89%

两者合计:

text 复制代码
69.73% + 22.89% = 92.62%

也就是说:

整条 SQL 超过 90% 的执行时间,都消耗在 PRES_INP_PRES_RECORD 的大范围扫描上。

因此后续优化无需先处理其他几十个节点,优先解决这两个扫描即可。


4. 第三步:分析 SQL 为什么没有精准访问

对应的关联条件是:

sql 复制代码
a.EVENT_NO   = pres.EVENT_NO
a.PATIENT_ID = pres.PATIENT_ID

这两个条件是同时出现的。

继续检查 PRES_INP_PRES_RECORD 的现有索引,发现:

text 复制代码
ID_PRES_INP_PRES_RECORD_EVENT
(EVENT_NO)

ID_PRES_INP_PRES_RECORD_PATIENT
(PATIENT_ID)

也就是说:

text 复制代码
EVENT_NO 有单列索引
PATIENT_ID 有单列索引

但没有直接匹配当前关联条件的:

text 复制代码
(EVENT_NO, PATIENT_ID)

联合索引。

表上虽然还有较长的组合索引,例如:

text 复制代码
BEGIN_EXEC_TIME
STOP_PRES_TIME
PATIENT_ID
EVENT_NO
ITEM_NAME
PRES_STATUS_CODE

但由于本 SQL 并没有使用前导列:

text 复制代码
BEGIN_EXEC_TIME
STOP_PRES_TIME

因此该索引并不能很好地用于:

text 复制代码
EVENT_NO + PATIENT_ID

这两个条件的精准定位。


5. 第四步:查看字段选择性

继续统计表数据:

text 复制代码
总行数:
4,812,038

不同 EVENT_NO:
34,144

不同 PATIENT_ID:
24,517

大致可以看出:

text 复制代码
单独按 EVENT_NO 查询,平均仍可能对应较多记录
单独按 PATIENT_ID 查询,也可能对应较多记录

而本 SQL 实际上同时具备:

text 复制代码
EVENT_NO
PATIENT_ID

两个条件。

因此更合理的访问方式应该是:

text 复制代码
先根据 EVENT_NO + PATIENT_ID
直接定位到更小的数据范围

而不是对 480 多万行数据做大范围扫描。


6. 第五步:建立联合索引测试

创建测试联合索引:

sql 复制代码
CREATE INDEX IDX_TEST_PRES_EVENT_PATIENT
ON ZOEPRES.PRES_INP_PRES_RECORD
(
    EVENT_NO,
    PATIENT_ID
);

这里只增加两个最核心的关联字段,没有继续加入:

text 复制代码
PRES_STATUS_CODE
ITEM_CODE

目的是先做一个简单、可控的验证:

只改变访问路径,观察能否把原来的大范围 CSCN 扫描变成根据 EVENT_NO、PATIENT_ID 的精准索引访问。


7. 优化后执行计划变化

增加联合索引后,原 SQL 不做任何改写,直接重新执行。

新计划中已经明确使用:

text 复制代码
IDX_TEST_PRES_EVENT_PATIENT

关键节点变为:

text 复制代码
#BLKUP2:
IDX_TEST_PRES_EVENT_PATIENT(PRES_INP_PRES_RECORD)

#SSEK2:
IDX_TEST_PRES_EVENT_PATIENT(PRES_INP_PRES_RECORD)

scan_range[
    (A.EVENT_NO,A.PATIENT_ID),
    (A.EVENT_NO,A.PATIENT_ID)
]

另一处相同逻辑也同样使用了新联合索引。

这说明访问路径已经发生实质变化。


8. 优化前后访问方式对比

优化前

text 复制代码
PRES_INP_PRES_RECORD
        ↓
CSCN2
        ↓
大范围扫描约 481 万行
        ↓
HASH / ANTI JOIN
        ↓
参与 NOT EXISTS 判断

而且类似的大范围扫描出现两次。

优化后

text 复制代码
外层记录
        ↓
EVENT_NO + PATIENT_ID
        ↓
IDX_TEST_PRES_EVENT_PATIENT
        ↓
SSEK2
        ↓
只访问与当前患者、就诊记录匹配的数据
        ↓
再判断状态、项目等条件

因此原来最重的两个 481 万级 CSCN2 节点被消除。


9. 优化后执行情况

优化后完整 SQL 执行情况:

text 复制代码
logical reads  = 1013906
physical reads = 0
io wait time   = 0 ms
exec time      = 2004 ms

执行时间由:

text 复制代码
12631 ms

下降到:

text 复制代码
2004 ms

约提升:

text 复制代码
12631 / 2004 ≈ 6.3 倍

10. 优化前后对比

指标 优化前 优化后
执行时间 12631 ms 2004 ms
physical reads 42384 0
io wait time 5177 ms 0 ms
logical reads 206731 1013906
主要访问方式 CSCN2 大范围扫描 SSEK2 联合索引定位
PRES_INP_PRES_RECORD 大范围扫描 约 481 万行,且出现两次 已消除

11. 为什么 logical reads 反而升高

这次有一个比较值得记录的现象:

text 复制代码
logical reads
206731 → 1013906

逻辑读没有下降,反而增加了。

但执行时间却明显下降:

text 复制代码
12.6 秒 → 2 秒

同时:

text 复制代码
physical reads
42384 → 0

io wait
5177 ms → 0

这说明优化后的执行方式虽然产生了更多逻辑层面的索引访问和回表操作,但避免了原来代价很高的大范围扫描和物理 I/O。

因此本次优化不能简单写成:

text 复制代码
逻辑读下降,所以性能提升

而应该写成:

执行计划由大范围 CSCN 扫描切换为按 EVENT_NO、PATIENT_ID 的 SSEK 精准定位,显著减少了物理读和 I/O 等待,从而使整体执行时间下降。


12. 为什么优化后还有进一步空间

优化后 ET 中,新的热点不再是原来的大范围扫描,而主要集中在:

text 复制代码
SSEK2
+
BLKUP2

也就是说:

text 复制代码
联合索引已经成功定位数据

但是找到索引记录后,还需要回表读取:

text 复制代码
PRES_STATUS_CODE
ITEM_CODE

等字段继续做过滤。

因此优化后的执行方式更接近:

text 复制代码
联合索引定位
        ↓
命中一批记录
        ↓
回表
        ↓
继续判断 PRES_STATUS_CODE、ITEM_CODE

所以逻辑读有所增加。

理论上还可以继续尝试覆盖更多过滤字段,例如:

text 复制代码
EVENT_NO
PATIENT_ID
PRES_STATUS_CODE
ITEM_CODE

来减少部分回表。

但本次已经实现:

text 复制代码
12.6 秒 → 2 秒

并且主要慢点已经被解决,因此本次优化到此结束,不继续扩大索引。


13. 这次优化真正解决了什么

本次优化并不是因为:

text 复制代码
表没有索引

实际上原表已经分别存在:

text 复制代码
EVENT_NO 单列索引
PATIENT_ID 单列索引

真正的问题是:

SQL 的关联条件需要同时使用 EVENT_NO 和 PATIENT_ID,但现有索引只能分别按单列访问,无法高效满足组合条件。

因此优化重点不是"有没有索引",而是:

text 复制代码
索引结构是否和 SQL 实际访问条件匹配

14. 本次排查思路总结

本次排查过程可以总结为:

text 复制代码
1. SQL 执行约 12.6 秒
        ↓
2. 执行计划节点很多,不逐行分析
        ↓
3. 查看 ET
        ↓
4. 发现两个 PRES_INP_PRES_RECORD CSCN 节点
   合计占 92.62%
        ↓
5. 每次均大范围扫描约 481 万行
        ↓
6. 回看 SQL
        ↓
7. 发现实际关联条件为
   EVENT_NO + PATIENT_ID
        ↓
8. 查看现有索引
        ↓
9. 两个字段只有各自单列索引
   没有联合索引
        ↓
10. 新建
    (EVENT_NO, PATIENT_ID)
        ↓
11. 原 SQL 不改写重新执行
        ↓
12. CSCN → SSEK
        ↓
13. 物理读和 I/O wait 消失
        ↓
14. 12.6 秒 → 2 秒

15. 本次优化得到的经验

经验 1:单列索引存在,不代表组合条件就一定高效

原表已有:

text 复制代码
EVENT_NO
PATIENT_ID

两个单列索引。

但 SQL 的实际条件是:

sql 复制代码
pres.EVENT_NO   = a.EVENT_NO
AND
pres.PATIENT_ID = a.PATIENT_ID

这种情况下,联合索引通常比两个单列索引更符合访问需求。


经验 2:执行计划太长时,先通过 ET 找热点

本 SQL 执行计划非常长,包含大量:

text 复制代码
UNION ALL
NEST LOOP
HASH JOIN
SSEK
BLKUP

如果逐节点分析,很容易失去重点。

通过 ET 很快发现:

text 复制代码
两个 CSCN 节点
占总时间 92.62%

后续排查就可以围绕这两个节点展开。


经验 3:CSCN 走了索引,也不代表访问方式高效

原计划虽然显示:

text 复制代码
CSCN2
INDEX33560750(PRES_INP_PRES_RECORD)

但它本质上仍然是在做:

text 复制代码
大范围索引扫描

而不是根据当前关联条件精准查找。

优化后的:

text 复制代码
SSEK2
scan_range[(EVENT_NO,PATIENT_ID),(...)]

才是真正针对当前条件的索引定位。


经验 4:优化效果不能只看 logical reads

本次:

text 复制代码
logical reads 上升

但:

text 复制代码
physical reads 下降到 0
io wait 下降到 0
执行时间明显下降

所以评估优化时应该综合看:

text 复制代码
执行计划变化
ET 热点变化
logical reads
physical reads
io wait
exec time

不能只看其中一个指标。


16. 最终结论

本次慢 SQL 的主要性能瓶颈是:

PRES_INP_PRES_RECORDNOT EXISTS 判断中需要同时根据 EVENT_NOPATIENT_ID 进行关联,但原表仅存在两个字段的单列索引,导致优化器选择了两次约 481 万行的大范围 CSCN 扫描。

通过新增:

sql 复制代码
(EVENT_NO, PATIENT_ID)

联合索引后,执行计划变为:

text 复制代码
SSEK2

根据两个关联字段进行精准索引定位。

最终:

text 复制代码
执行时间:
12.6 秒 → 约 2 秒

physical reads:
42384 → 0

io wait:
5177 ms → 0

虽然逻辑读有所增加,但最主要的大范围扫描和物理 I/O 已被消除,整体性能获得明显提升。

本次优化的核心可以总结为:

索引不是越多越好,关键是索引列组合要和 SQL 的实际访问条件匹配。

相关推荐
迷枫7121 小时前
SQL 优化记录:OR 条件改写后利用现有联合索引
sql
拾光Ծ4 小时前
【MySQL】对表数据的操作:增删查改(CRUD)
android·数据库·sql·mysql
迷枫7125 小时前
SQL 优化排查记录:补充索引,利用 OR 展开消除大表重复扫描
sql
九皇叔叔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·编辑器·深度优先·宽度优先