1. 问题现象
现场发现一条查询执行时间较长,SQL 本身并不复杂,最终仅返回 2 行,但实际执行耗时超过 1 分钟。
原 SQL:
sql
SELECT *
FROM zoepres.pres_apply_records_pool t
WHERE 1 = 1
AND (
(t.pres_no = 96462280
AND t.pres_sub_no = 1
AND t.exec_time = '2026-08-27 10:00:00.000000')
OR
(t.pres_no = 96462280
AND t.pres_sub_no = 1
AND t.exec_time = '2026-08-27 20:00:00.000000')
);
原 SQL 返回:
text
2 rows
执行耗时:
text
已用时间: 00:01:02.466
2. 查看执行计划
原执行计划关键部分如下:
text
1 #NSET2: [11939, 627741->2, 2965]
2 #PRJT2: [11939, 627741->2, 2965]
3 #PARALLEL: [11939, 627741->2, 2965]; scan_type(FULL)
4 #HASH RIGHT SEMI JOIN2: [11939, 627741->2, 2965];
key_num(3)
KEY(DMTEMPVIEW_891440050.colname=T.PRES_NO
AND DMTEMPVIEW_891440050.colname=T.PRES_SUB_NO
AND DMTEMPVIEW_891440050.colname=T.EXEC_TIME)
5 #CONST VALUE LIST: [1, 2->24, 73]; row_num(2), col_num(3)
6 #CSCN2: [11939, 12540543->12554838, 2965];
INDEX33560548(PRES_APPLY_RECORDS_POOL); btr_scan(1)
这里最明显的是第 6 行:
text
#CSCN2: [11939, 12540543->12554838, 2965]
最终只返回 2 行,但底层累计扫描约 1255 万行。
原 SQL 中实际上只有两组条件,并且:
text
PRES_NO 完全相同
PRES_SUB_NO 完全相同
EXEC_TIME 不同
但执行计划没有根据这三个条件进行精准索引查找,而是将两组 OR 条件转换成:
text
CONST VALUE LIST
↓
HASH RIGHT SEMI JOIN
↓
CSCN2
导致底层进行了大范围扫描。
3. 结合 ET 确认主要耗时节点
继续查看 ET:
text
行号 OP TIME(US) PERCENT
7 CSCN2 61732078 98.93%
6 HRS2 666636 1.07%
其中:
text
CSCN2 = 61,732,078 us
占总执行时间 98.93%
可以基本确定,本次 SQL 的主要耗时集中在底层扫描。
原 Statistics:
text
logical reads = 42832
physical reads = 108078
io wait time = 36586 ms
exec time = 62465 ms
本次执行中 I/O 等待时间约 36.6 秒,同时底层扫描量又非常大,因此优先考虑减少扫描范围。
4. 初步尝试联合索引
原 SQL 的过滤条件涉及:
text
PRES_NO
PRES_SUB_NO
EXEC_TIME
最开始考虑建立:
sql
CREATE INDEX IDX_TEST_PRES_APPLY_POOL_01
ON ZOEPRES.PRES_APPLY_RECORDS_POOL
(PRES_NO, PRES_SUB_NO, EXEC_TIME);
执行时报错:
text
-3236: 此列列表已索引
说明这组三列实际上已经存在对应索引,因此问题并不是"缺少索引"。
也就是说,当前更值得关注的是:
已经存在合适索引,但原 SQL 的写法没有让优化器使用该索引进行精准范围扫描。
5. 分析 SQL 结构
原条件:
sql
AND (
(t.pres_no = 96462280
AND t.pres_sub_no = 1
AND t.exec_time = '2026-08-27 10:00:00.000000')
OR
(t.pres_no = 96462280
AND t.pres_sub_no = 1
AND t.exec_time = '2026-08-27 20:00:00.000000')
)
两组 OR 条件中:
text
t.pres_no = 96462280
t.pres_sub_no = 1
完全相同。
真正发生变化的只有:
text
t.exec_time
因此可以将公共条件提取出来,将两个时间条件改为 IN。
6. SQL 改写
改写后:
sql
SELECT *
FROM zoepres.pres_apply_records_pool t
WHERE t.pres_no = 96462280
AND t.pres_sub_no = 1
AND t.exec_time IN (
'2026-08-27 10:00:00.000000',
'2026-08-27 20:00:00.000000'
);
改写只调整了过滤条件表达方式,没有增加索引,也没有修改业务逻辑。
7. 优化后执行计划
改写后仍返回 2 行。
关键执行计划:
text
1 #NSET2: [1, 1->2, 2965]
2 #PRJT2: [1, 1->2, 2965]
3 #NEST LOOP INDEX JOIN2: [1, 1->2, 2965]
4 #CONST VALUE LIST: [1, 2->2, 13]; row_num(2), col_num(1)
5 #PARALLEL: [1, 1->2, 2965]; scan_type(FULL)
6 #BLKUP2: [1, 1->2, 2965];
JPC_PRES_APPLY_RECORDS_POOL_20260402(PRES_APPLY_RECORDS_POOL)
7 #SSEK2: [1, 1->2, 2965];
JPC_PRES_APPLY_RECORDS_POOL_20260402(PRES_APPLY_RECORDS_POOL)
scan_range[
(exp_cast(96462280),exp_cast(1),DMTEMPVIEW_891597895.colname),
(exp_cast(96462280),exp_cast(1),DMTEMPVIEW_891597895.colname)
]
执行方式发生了明显变化。
原来:
text
CONST VALUE LIST
↓
HASH RIGHT SEMI JOIN
↓
CSCN2
↓
扫描约 1255 万行
改写后:
text
CONST VALUE LIST
↓
NEST LOOP INDEX JOIN2
↓
SSEK2
↓
利用现有联合索引精准查找
↓
返回 2 行
此时优化器已经能够使用:
text
JPC_PRES_APPLY_RECORDS_POOL_20260402
进行索引范围定位。
8. 优化前后对比
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 返回行数 | 2 | 2 |
| 执行时间 | 62465 ms | 2.107 ms |
| logical reads | 42832 | 82 |
| physical reads | 108078 | 0 |
| io wait time | 36586 ms | 1 ms |
| 主要访问方式 | CSCN2 | SSEK2 |
| 底层访问情况 | 扫描约 1255 万行 | 精准命中 2 行 |
逻辑读:
text
42832 → 82
下降约:
text
99.81%
执行时间由约 62 秒下降到毫秒级。
需要注意,前后物理读受缓存状态影响较大,因此最终判断优化效果时,主要结合:
text
执行计划变化
逻辑读变化
实际扫描量变化
进行确认,而不是只看单次执行时间。
9. 总结
本次 SQL 本身已有可以利用的联合索引,因此问题不在于缺少索引,而在于 SQL 条件的写法。
原 SQL 使用两组 OR 条件:
text
(PRES_NO + PRES_SUB_NO + EXEC_TIME)
OR
(PRES_NO + PRES_SUB_NO + EXEC_TIME)
由于前两个条件完全相同,仅 EXEC_TIME 不同,优化器将其转换为常量表与 HASH SEMI JOIN,最终对底层数据进行了大范围扫描。
将公共条件提取,并把时间条件改写为:
sql
EXEC_TIME IN (...)
以后,优化器能够直接利用现有联合索引进行 SSEK 精准检索,底层扫描量和逻辑读均明显下降。
本次优化的关键点不是新增索引,而是:
通过等价 SQL 改写,使现有索引真正被有效利用。