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_RECORD在NOT EXISTS判断中需要同时根据EVENT_NO和PATIENT_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 的实际访问条件匹配。