把 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. 优化前后对比
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 返回行数 | 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 行 |
逻辑读:
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联合索引内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论