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

 更新时间:2026年09月02日 09:40:59   作者:迷枫712  
本文给大家介绍SQL优化记录:OR 条件改写后利用现有联合索引,本文结合实例代码给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧

把 OR 条件拆成 UNION ALL,核心不是“改写法”,而是让‌每个分支都能单独命中现有的联合索引‌。改写本身不会自动利用索引,真正起作用的是拆分后每个子查询的 WHERE 条件是否满足最左前缀原则。‌‌

改写前先确认三件事

  • 每个分支单独 EXPLAIN‌:确认 type 是 ref 或 range,key 显示命中了你期望的联合索引,而不是 ALL
  • 分支互斥才用 UNION ALL‌:如果两个条件可能命中同一行(比如 status='paid' OR customer_id=123),用 UNION ALL 会产生重复数据,需要业务确认是否允许。
  • 公共条件要复制到每个子查询‌:比如 deleted=0、时间范围这类公共过滤条件,必须写进每个 SELECT 的 WHERE 里,不能只放外层。‌‌

如何利用现有联合索引

假设现有联合索引是 (htid, raid, date),原查询是:

SELECT htid, raid, SUM(amount)
FROM configuration
WHERE (htid='x1' AND raid='y1')
   OR (htid='x2' AND raid='y2')
   OR (htid='x3' AND raid='y3')
  AND date BETWEEN '2025-01-01' AND '2025-12-31'
GROUP BY htid, raid;

改写后每个分支都完整保留联合索引的前缀列和范围条件:‌‌

SELECT htid, raid, SUM(amount) FROM configuration
WHERE htid='x1' AND raid='y1' AND date BETWEEN '2025-01-01' AND '2025-12-31'
GROUP BY htid, raid
UNION ALL
SELECT htid, raid, SUM(amount) FROM configuration
WHERE htid='x2' AND raid='y2' AND date BETWEEN '2025-01-01' AND '2025-12-31'
GROUP BY htid, raid
UNION ALL
SELECT htid, raid, SUM(amount) FROM configuration
WHERE htid='x3' AND raid='y3' AND date BETWEEN '2025-01-01' AND '2025-12-31'
GROUP BY htid, raid;

什么时候不该拆

  • 同一字段的多个等值 OR‌:id=1 OR id=2 OR id=3 直接改 IN (1,2,3),更简洁且稳定走索引。
  • 分支本身无法走索引‌:比如 name LIKE '%abc' 或 DATE(created_at)='2026-01-01',拆了也只是多次全表扫描。
  • OR 落在同一联合索引的最左前缀上‌:比如 (a=1 AND b=2) OR (a=1 AND b=3),索引是 (a,b),MySQL 可能直接走 range 扫描,比 UNION 更优。‌‌

还有个更治本的思路

如果查询字段不多,考虑用‌覆盖索引‌。把 SELECT 需要的列也加进联合索引,让每个分支直接走索引树、不回表,比折腾 UNION ALL 提升更明显。 比如上面例子中,建 (htid, raid, date, amount) 就能覆盖 WHERE 和 SUM(amount),彻底避免回表。‌‌

⚠️ ‌最关键的验证‌:改完必须对每个子查询单独跑 EXPLAIN,确认 type 和 key 都符合预期。没验证就上线,等于把性能问题从优化器手里移交给你自己。‌‌

1. 问题现象

现场发现一条查询执行时间较长,SQL 本身并不复杂,最终仅返回 2 行,但实际执行耗时超过 1 分钟。

原 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 返回:

2 rows

执行耗时:

已用时间: 00:01:02.466

2. 查看执行计划

原执行计划关键部分如下:

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 行:

#CSCN2: [11939, 12540543->12554838, 2965]

最终只返回 2 行,但底层累计扫描约 1255 万行。

原 SQL 中实际上只有两组条件,并且:

PRES_NO      完全相同
PRES_SUB_NO  完全相同
EXEC_TIME    不同

但执行计划没有根据这三个条件进行精准索引查找,而是将两组 OR 条件转换成:

CONST VALUE LIST
        ↓
HASH RIGHT SEMI JOIN
        ↓
CSCN2

导致底层进行了大范围扫描。

3. 结合 ET 确认主要耗时节点

继续查看 ET:

行号  OP     TIME(US)     PERCENT
7     CSCN2  61732078     98.93%
6     HRS2     666636      1.07%

其中:

CSCN2 = 61,732,078 us
占总执行时间 98.93%

可以基本确定,本次 SQL 的主要耗时集中在底层扫描。

原 Statistics:

logical reads  = 42832
physical reads = 108078
io wait time   = 36586 ms
exec time      = 62465 ms

本次执行中 I/O 等待时间约 36.6 秒,同时底层扫描量又非常大,因此优先考虑减少扫描范围。

4. 初步尝试联合索引

原 SQL 的过滤条件涉及:

PRES_NO
PRES_SUB_NO
EXEC_TIME

最开始考虑建立:

CREATE INDEX IDX_TEST_PRES_APPLY_POOL_01
ON ZOEPRES.PRES_APPLY_RECORDS_POOL
(PRES_NO, PRES_SUB_NO, EXEC_TIME);

执行时报错:

-3236: 此列列表已索引

说明这组三列实际上已经存在对应索引,因此问题并不是“缺少索引”。

也就是说,当前更值得关注的是:

已经存在合适索引,但原 SQL 的写法没有让优化器使用该索引进行精准范围扫描。

5. 分析 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 条件中:

t.pres_no     = 96462280
t.pres_sub_no = 1

完全相同。

真正发生变化的只有:

t.exec_time

因此可以将公共条件提取出来,将两个时间条件改为 IN。

6. 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 行。

关键执行计划:

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)
              ]

执行方式发生了明显变化。

原来:

CONST VALUE LIST
        ↓
HASH RIGHT SEMI JOIN
        ↓
CSCN2
        ↓
扫描约 1255 万行

改写后:

CONST VALUE LIST
        ↓
NEST LOOP INDEX JOIN2
        ↓
SSEK2
        ↓
利用现有联合索引精准查找
        ↓
返回 2 行

此时优化器已经能够使用:

JPC_PRES_APPLY_RECORDS_POOL_20260402

进行索引范围定位。

8. 优化前后对比

指标优化前优化后
返回行数22
执行时间62465 ms2.107 ms
logical reads4283282
physical reads1080780
io wait time36586 ms1 ms
主要访问方式CSCN2SSEK2
底层访问情况扫描约 1255 万行精准命中 2 行

逻辑读:

42832 → 82

下降约:

99.81%

执行时间由约 62 秒下降到毫秒级。

需要注意,前后物理读受缓存状态影响较大,因此最终判断优化效果时,主要结合:

执行计划变化
逻辑读变化
实际扫描量变化

进行确认,而不是只看单次执行时间。

9. 总结

本次 SQL 本身已有可以利用的联合索引,因此问题不在于缺少索引,而在于 SQL 条件的写法。

原 SQL 使用两组 OR 条件:

(PRES_NO + PRES_SUB_NO + EXEC_TIME)
OR
(PRES_NO + PRES_SUB_NO + EXEC_TIME)

由于前两个条件完全相同,仅 EXEC_TIME 不同,优化器将其转换为常量表与 HASH SEMI JOIN,最终对底层数据进行了大范围扫描。

将公共条件提取,并把时间条件改写为:

EXEC_TIME IN (...)

以后,优化器能够直接利用现有联合索引进行 SSEK 精准检索,底层扫描量和逻辑读均明显下降。

本次优化的关键点不是新增索引,而是:

通过等价 SQL 改写,使现有索引真正被有效利用。

到此这篇关于SQL 优化记录之OR 条件改写后利用现有联合索引的文章就介绍到这了,更多相关sql联合索引内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

最新评论