SQL 优化记录:OR 条件改写后利用现有联合索引

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 改写,使现有索引真正被有效利用。

相关推荐
漂着的圆木16 小时前
Agent 功能参与度:Copilot 怎么算
sql·数据分析·agent·githubcopilot·度量
旺仔不是程序员19 小时前
LIMIT 1:PostgreSQL 只取一行的高效查询姿势
数据库·后端·sql
知识的搬运工旺仔2 天前
唯一索引与 NULL 值:PostgreSQL 主键约束与 NULLS NOT DISTINCT
数据库·后端·sql·postgresql
想念是会呼吸的鱼2 天前
【ClickHouse 常用 SQL 语句整理】
sql·clickhouse
想念是会呼吸的鱼2 天前
MySQL 常用语法整理
sql·mysql
SelectDB技术团队2 天前
StarRocks 迁移至 Apache Doris 完整指南:三步完成结构、数据与业务平滑切换
数据库·人工智能·sql·clickhouse·apache doris·selectdb·湖仓架构升级
imDwAaY2 天前
MySQL MVCC 详解:原理、版本链、Read View 与可见性判断
数据库·sql·mysql
bbq粉刷匠2 天前
触发器(下):事务与锁、binlog 一致性与替代方案
sql