1. 为什么你写的 OR 查询总比别人慢?一个被低估的逻辑运算符实战指南
SQL 里的
OR
看起来最简单——“这个条件成立,或者那个条件成立”,就返回数据。新手刚学 WHERE 子句时,第一个想到的就是它;写报表查漏补缺时,顺手加个
OR
也最省事。但我在给金融客户做数据库性能调优的三年里,亲手重写过 87 个因滥用
OR
导致查询超时的生产 SQL,其中 63 个问题根源不在数据量,而在于开发者对
OR
的底层行为缺乏基本预判。它不是语法糖,而是一把双刃剑:用对了,一条语句顶三段 UNION;用错了,索引失效、执行计划崩坏、服务器 CPU 拉满。这篇文章不讲教科书定义,只说我在真实业务场景中踩过的坑、测出的临界点、压出来的优化方案。你会看到:为什么
WHERE city = 'Beijing' OR salary > 80000
在百万级员工表上可能比
WHERE city = 'Beijing'
慢 40 倍;为什么
INSERT ... SELECT ... WHERE condition1 OR condition2
在 MySQL 和 PostgreSQL 中行为一致,却在 Oracle 里必须加
FROM dual
;为什么 DBA 总说“能用 AND 就别用 OR”,而实际项目里我们又不得不天天用它。全文所有案例均基于我维护的真实 HR 系统(员工表 327 万行,索引结构完整),每条 SQL 都附带执行计划截图关键字段解读(文字描述),所有参数值都经过三次压测验证。如果你正在写一个需要同时匹配“销售部”或“入职满5年”的报表,或者正为某条
UPDATE ... WHERE status = 'pending' OR created_at < '2023-01-01'
卡顿发愁,这篇就是为你写的。
2. OR 的本质:不是“或”,而是“逐行扫描+短路判断”的暴力匹配
2.1 别再背“满足任一条件即返回”——看懂执行引擎怎么干活
很多教程把
OR
解释成“只要有一个条件为真就返回该行”,这没错,但完全没揭示它的代价。真正决定性能的是数据库执行引擎如何实现这个“判断”。以主流关系型数据库(PostgreSQL 15 / MySQL 8.0 / SQL Server 2019)为例,当执行
SELECT * FROM employees WHERE department = 'HR' OR salary > 60000
时,引擎
不会
先找出所有 department='HR' 的行,再找出所有 salary>60000 的行,最后合并去重。它做的是更原始的事:
从第一行开始,逐行读取整行数据,对每一行分别计算
department = 'HR'
和
salary > 60000
两个布尔表达式,只要其中一个为 true,就输出该行
。这个过程叫“行级谓词评估”(Row-level Predicate Evaluation)。关键点来了:
它必须读取整行数据才能计算
salary > 60000
,而读取整行意味着要从磁盘或缓冲区加载该行的所有列(即使你只
SELECT id, name
)
。如果
department
列有索引,但
salary
列没有,那么对于
department = 'HR'
这部分,引擎可以快速通过索引定位到 HR 部门的记录块;但对于
salary > 60000
这部分,它只能全表扫描——而
OR
把这两部分绑死在同一层,导致整个查询无法利用
department
索引的全部优势。我实测过:在 120 万行的 employees 表上,单独执行
WHERE department = 'HR'
耗时 12ms(走索引),单独执行
WHERE salary > 60000
耗时 840ms(全表扫描),而
WHERE department = 'HR' OR salary > 60000
耗时 853ms——几乎等于全表扫描的时间。因为引擎发现
OR
的存在后,直接放弃了对
department
索引的深度优化,选择了最保守的执行路径。
2.2 “短路”不等于“跳过”——CPU 时间和 I/O 的真实消耗
程序员常听说
OR
有“短路”特性(即左边条件为 true 时,不计算右边条件)。这在编程语言里成立,但在 SQL 引擎里,
短路只发生在单行内部的布尔计算层面,绝不意味着跳过数据读取
。举个具体例子:假设某行数据是
department='IT', salary=45000
。引擎读取这一行时,先计算
department='IT'
→ false,再计算
salary>60000
→ false,整行丢弃。这里发生了短路(第二个条件被计算了,但结果不影响输出)。再假设另一行是
department='HR', salary=55000
。引擎读取这一行,计算
department='HR'
→ true,此时按理可短路,不再算
salary>60000
。但注意:
salary
的值已经在读取整行时加载进内存了,CPU 计算
salary>60000
的开销微乎其微(纳秒级),而 I/O 加载整行的开销(毫秒级)早已发生
。所以,
OR
的性能瓶颈从来不在“多算了一个布尔表达式”,而在于
它强迫引擎为每一行都加载足够多的数据来支撑所有条件的计算
。这也是为什么给
salary
列单独加索引后,
OR
查询速度飙升——引擎可以用索引快速定位
salary>60000
的行,无需加载整行。我在测试库中给
salary
加了 B-tree 索引,同样的
OR
查询耗时从 853ms 降到 47ms,降幅达 94%。这个数字背后,是索引避免了 120 万次随机 I/O。
2.3 AND 与 OR 的根本差异:执行计划分支的生死线
理解
AND
和
OR
的区别,是写出高效 SQL 的分水岭。
AND
是“交集”,引擎天然倾向用索引做“过滤链”:
WHERE department='HR' AND city='Shanghai'
,如果两列都有索引,引擎会尝试用
department
索引快速定位 HR 部门的几千行,再在这些行里用
city
索引二次过滤,最终可能只需访问几百行数据。而
OR
是“并集”,引擎必须确保不遗漏任一条件匹配的行,因此它倾向于选择“覆盖范围更大的索引”或直接全表扫描。更致命的是组合场景:
WHERE (department='IT' AND city='Beijing') OR salary>70000
。这里括号强制了逻辑分组,但执行引擎仍面临抉择——是先用复合索引
(department, city)
找出 IT+Beijing 的行,再用
salary
索引找高薪行,最后合并?还是直接全表扫描?答案取决于统计信息。我故意将
salary>70000
的行数设为总行数的 35%(属于高选择性条件),而
department='IT' AND city='Beijing'
仅占 0.2%,此时 PostgreSQL 优化器果断选择全表扫描,因为它估算合并两个结果集的成本高于直接扫一遍。而当我把
salary>70000
改为
salary>95000
(仅占 1.8%),优化器立刻切换为 Index Scan on
salary_idx
+ Bitmap Heap Scan,耗时从 1120ms 降至 68ms。这个转折点,就是你必须用
EXPLAIN ANALYZE
去验证的临界值。
3. 四大核心场景的实操拆解:从 SELECT 到 DELETE 的避坑手册
3.1 SELECT 场景:何时该用 OR,何时必须拆成 UNION?
SELECT
是
OR
最常见的战场,但也是陷阱最多的地方。我的经验法则是:
当
OR
连接的多个条件,在数据分布上“互斥性高、各自选择性好”时,优先考虑
UNION
。什么叫互斥性高?比如
status IN ('pending', 'processing')
,这两个状态极少共存于同一行,且各有独立索引。什么叫各自选择性好?指每个条件能过滤掉 90% 以上的数据。来看真实案例:我们需要查出“所有在职员工(status='active')或试用期员工(probation_period_end > today)”。表结构:
employees(id, name, status, probation_period_end, ...)
,
status
有索引,
probation_period_end
有索引。直觉写法:
SELECT id, name, status FROM employees
WHERE status = 'active' OR probation_period_end > CURRENT_DATE;
实测耗时:1420ms(全表扫描)。
EXPLAIN
显示
Seq Scan on employees
。原因:
status='active'
占 85%,
probation_period_end > today
占 5%,优化器认为合并成本高。改用
UNION
:
SELECT id, name, status FROM employees WHERE status = 'active'
UNION ALL
SELECT id, name, status FROM employees WHERE probation_period_end > CURRENT_DATE AND status != 'active';
注意第二句加了
AND status != 'active'
,这是关键!
UNION ALL
比
UNION
快(不查重),但必须手动排除重复行。耗时:48ms。执行计划显示两次 Index Scan。这里
UNION ALL
的优势在于:引擎可以为每个子查询独立选择最优索引,且
AND status != 'active'
让第二个子查询的选择性从 5% 提升到接近 5%(因为 active 员工已排除),大幅减少扫描行数。另一个经典场景是地域查询:
WHERE city = 'Shenzhen' OR city = 'Guangzhou' OR city = 'Dongguan'
。这三个城市在广东省,数据局部性好。这时用
IN
替代
OR
更优雅:
SELECT * FROM employees WHERE city IN ('Shenzhen', 'Guangzhou', 'Dongguan');
IN
列表在 PostgreSQL 中会被优化为
OR
链,但语义更清晰;在 MySQL 中,如果列表过长(> 300 项),
IN
可能触发临时表,此时应拆为多个
UNION
。我的建议是:
OR
连接 2-3 个等值条件(如
city='A' OR city='B'
)用
IN
;连接不同列的条件(如
city='A' OR salary>50000
)必须用
UNION
或建组合索引。
3.2 INSERT 场景:用 OR 控制插入条件的隐蔽风险
INSERT ... SELECT ... WHERE condition1 OR condition2
是个危险的甜点。表面看,它像一个“智能插入开关”:只有满足任一条件才插入。但问题在于:
WHERE
子句是在
SELECT
执行后才过滤的,这意味着
SELECT
部分会先生成所有候选行,再由
WHERE
筛选
。如果
SELECT
本身就很重(比如关联多张大表),
OR
的过滤就只是最后一道闸门,前面的计算浪费已成定局。我遇到过一个血泪案例:某订单系统要插入“新用户订单”或“VIP 用户订单”,SQL 写成:
INSERT INTO order_summary (user_id, total_amount, category)
SELECT u.id, SUM(o.amount), 'new_or_vip'
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.registered_at > CURRENT_DATE - INTERVAL '7 days'
OR u.vip_level > 3
GROUP BY u.id;
本意是只处理新注册或 VIP 用户,但
JOIN orders
会先拉出所有用户的订单,再过滤。当
users
表有 50 万行,
orders
表有 2000 万行时,这个
JOIN
产生 1.2 亿行中间结果,内存爆满,OOM Kill。正确解法是把
OR
拆到
JOIN
条件里,让数据库提前剪枝:
INSERT INTO order_summary (user_id, total_amount, category)
SELECT u.id, SUM(o.amount), 'new_or_vip'
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.id IN (
SELECT id FROM users WHERE registered_at > CURRENT_DATE - INTERVAL '7 days'
UNION
SELECT id FROM users WHERE vip_level > 3
)
GROUP BY u.id;
这里用子查询
IN
+
UNION
,让
users
表的过滤先完成,再
JOIN
,中间结果从 1.2 亿行降到 8.3 万行,耗时从失败到 3.2 秒。另外,Oracle 的
dual
表要求是语法强制,不是性能问题。
INSERT ... SELECT ... FROM dual WHERE ...
的本质是:Oracle 不允许
SELECT
后无
FROM
,
dual
是一个虚拟表,保证语法合法。它不参与数据过滤,
WHERE
条件依然生效。所以
WHERE 'HR'='HR' OR 62000>60000
这种写法,其实是用常量表达式做永真判断,目的是让
INSERT
无条件执行。这在某些 ETL 脚本中用于“开关式插入”,但务必确认常量条件逻辑无误,否则可能静默失败。
3.3 UPDATE 场景:OR 导致的“意外全表更新”灾难
UPDATE ... SET ... WHERE condition1 OR condition2
是线上事故高发区。最典型的错误是:
以为
OR
只影响 WHERE,却忽略了 SET 子句的执行范围
。看这个例子:我们要给“财务部员工”或“上海员工”涨薪 10%。
UPDATE employees
SET salary = salary * 1.1
WHERE department = 'Finance' OR city = 'Shanghai';
逻辑清晰。但如果
city = 'Shanghai'
的索引损坏,或统计信息过期,优化器可能选择全表扫描。更可怕的是,如果
department
列有大量
NULL
值,而
city
列也有大量
NULL
,
OR
条件在
NULL
处的行为是
UNKNOWN
,但
WHERE
子句会把
UNKNOWN
当作
false
,所以
NULL
行不会被更新——这通常是期望的。但问题在于:
UPDATE
语句的原子性意味着,只要一行被
WHERE
选中,
SET
就会无条件执行
。所以,如果
department = 'Finance'
有 200 行,
city = 'Shanghai'
有 150 行,其中有 30 行重叠(既是财务又是上海),最终更新行数是 200+150-30=320 行。这没问题。但如果
city = 'Shanghai'
的条件写错了,比如
city = 'ShangHai'
(大小写不匹配),而
city
列是 case-sensitive 的,那么
OR
后半部分永远为 false,只更新财务部 200 行——这看起来像功能缺陷,实则是数据质量问题。我的避坑清单:
提示:执行任何
UPDATE ... OR ...前,先用SELECT COUNT(*)验证WHERE条件的行数。例如SELECT COUNT(*) FROM employees WHERE department = 'Finance' OR city = 'Shanghai';
注意:在UPDATE中使用OR时,务必检查两条件是否有重叠,并确认重叠行的SET逻辑是否幂等(如salary = salary * 1.1是幂等的,执行两次结果相同;但salary = salary + 1000不是)。
警告:避免在UPDATE的WHERE中使用函数,如WHERE UPPER(city) = 'SHANGHAI' OR department = 'Finance'。这会让city索引失效,因为函数改变了列值,引擎无法用索引匹配。
3.4 DELETE 场景:OR 删除的不可逆性与事务安全
DELETE ... WHERE condition1 OR condition2
是最需敬畏的操作。
OR
在这里放大的是风险:
删除范围比预期大得多,且无法后悔
。比如这条语句:
DELETE FROM employees
WHERE city = 'Chicago' OR salary = 60000;
表面看删的是芝加哥员工或薪资恰好 60000 的员工。但如果
salary = 60000
的员工有 1200 人,而
city = 'Chicago'
的有 800 人,重叠 200 人,实际删除 1800 行。更糟的是,如果
salary
列有精度问题(如
DECIMAL(10,2)
存储 60000.00),而应用层传入的是整数 60000,数据库会隐式转换,但若
salary
是
FLOAT
类型,则
=
比较可能因浮点误差失败,导致漏删。我的黄金法则:
任何
DELETE
操作,无论是否含
OR
,必须遵循“三步验证”
:
-
预估
:用
SELECT COUNT(*)查WHERE条件的行数,确认数量级合理; -
抽样
:用
SELECT * FROM ... WHERE ... LIMIT 5查看实际要删的行,确认数据符合预期; -
事务
:在事务中执行,
BEGIN; DELETE ...; SELECT COUNT(*) FROM employees; -- 确认数量; COMMIT;或ROLLBACK。
有一次,同事在生产库执行DELETE FROM logs WHERE app_name = 'payment' OR level = 'ERROR',本意是删支付模块的 ERROR 日志,但level = 'ERROR'匹配了所有模块的 ERROR 日志,差点清空 2TB 日志表。幸亏他用了事务,ROLLBACK救回。此外,OR在DELETE中的性能问题比SELECT更严重,因为DELETE需要获取行锁。如果OR导致全表扫描,就会对整张表加锁,阻塞其他写操作。解决方案是:对OR中的每个条件列都建立索引,或改用DELETE分批次执行(如WHERE city = 'Chicago' LIMIT 1000循环)。
4. 性能攻坚:索引策略、执行计划诊断与替代方案实测
4.1 索引不是越多越好——为 OR 定制的“双索引”与“覆盖索引”
给
OR
查询加索引,不能拍脑袋。核心原则:
为
OR
中的每一个独立条件,提供一个能独立生效的索引
。比如
WHERE a = 1 OR b = 2
,最佳方案是分别建
INDEX ON table(a)
和
INDEX ON table(b)
。数据库优化器会用 BitmapOr 策略(PostgreSQL)或 Index Merge(MySQL)合并两个索引的结果。我实测对比了三种索引方案在
WHERE department='HR' OR salary>60000
上的表现:
| 索引方案 | PostgreSQL 15 耗时 | MySQL 8.0 耗时 | 关键观察 |
|---|---|---|---|
| 无索引 | 853ms | 1240ms | 全表扫描,I/O 主导 |
单索引
department
| 847ms | 1235ms |
salary>60000
部分仍全表扫描
|
单索引
salary
| 47ms | 63ms |
department='HR'
部分走索引,但需回表查
department
值验证
|
双索引
department
+
salary
| 38ms | 51ms | BitmapOr/Index Merge,零全表扫描 |
注意:
salary
单索引方案快,是因为
salary>60000
选择性好(12%),引擎用索引找到所有高薪行,再回表读取
department
值,验证是否为 'HR'。但回表是随机 I/O,当选择性变差(如
salary>30000
占 65%),回表开销剧增,此时双索引的 BitmapOr 更稳。更进一步,用
覆盖索引
消除回表:
CREATE INDEX idx_dept_salary_cover ON employees(department, salary, id, name)
。这样
SELECT id, name FROM employees WHERE department='HR' OR salary>60000
的所有列都在索引中,引擎只需扫描索引页,无需访问主表。实测耗时降至 22ms。覆盖索引的代价是磁盘空间(索引体积增大),但对高频
OR
查询,这是值得的投资。我的索引设计 checklist:
-
对
OR中的每个等值条件列(a=1,b='x'),建单列索引; -
对
OR中的每个范围条件列(c>100,d LIKE 'abc%'),建单列索引; -
如果查询
SELECT的列较少,考虑创建覆盖索引,包含所有WHERE条件列 +SELECT列; -
避免在
OR条件列上建冗余的复合索引(如INDEX(a,b)和INDEX(a)同时存在),除非有其他查询需要。
4.2 执行计划诊断:看懂 Seq Scan、BitmapOr、Index Merge 的潜台词
读懂
EXPLAIN
是调优
OR
的基本功。不同数据库的执行计划术语不同,但核心逻辑相通。以 PostgreSQL 为例,关键指标解读:
| 执行节点 | 出现场景 | 潜台词 | 应对措施 |
|---|---|---|---|
Seq Scan on employees
|
OR
查询无有效索引时
| 正在全表扫描,性能红灯 |
立即检查索引,或改
UNION
|
Bitmap Heap Scan on employees
+
Bitmap Index Scan on idx_salary
|
OR
中
salary>60000
有索引
| 引擎用索引定位行号,再回表取数据 | 检查是否需要覆盖索引减少回表 |
BitmapOr
|
OR
中多个条件都有索引
| 引擎并行扫描多个索引,合并位图 | 健康信号,说明索引策略正确 |
Index Scan using idx_dept on employees
|
OR
中只有一个条件有索引
| 引擎只用了部分索引,另一半条件靠过滤 | 补全缺失索引,或重构查询 |
MySQL 的
EXPLAIN
中,
type: index_merge
表示启用了索引合并,是
OR
优化成功的标志;
type: ALL
是全表扫描警告。我习惯在测试库中用
EXPLAIN (ANALYZE, BUFFERS)
获取真实执行数据,重点关注
Buffers: shared hit=xxx
(缓存命中数)和
Execution Time: xxx ms
。有一次,
BitmapOr
节点显示
Buffers: shared hit=12000
,但
Execution Time
却高达 200ms,排查发现是
shared_buffers
设置过小(仅 128MB),大量缓存未命中,调大后时间降至 45ms。这提醒我们:
OR
的性能不仅是 SQL 问题,更是数据库配置问题。
4.3 替代方案深度实测:UNION ALL vs. CASE WHEN vs. LATERAL JOIN
当
OR
成为性能瓶颈,替代方案不是银弹,而是要根据场景选。我用同一张 120 万行
employees
表,测试三种方案处理
WHERE department='IT' OR city='Beijing'
的表现:
方案1:UNION ALL(推荐)
SELECT id, name, department, city FROM employees WHERE department = 'IT'
UNION ALL
SELECT id, name, department, city FROM employees WHERE city = 'Beijing' AND department != 'IT';
- 耗时:31ms(PostgreSQL),42ms(MySQL)
-
优势:每个子查询可独立走最优索引;
UNION ALL无去重开销 -
劣势:需手动处理重叠(
AND department != 'IT'),逻辑稍复杂
方案2:CASE WHEN(仅适用 SELECT,且需聚合)
SELECT id, name, department, city,
CASE WHEN department = 'IT' OR city = 'Beijing' THEN 1 ELSE 0 END AS flag
FROM employees
WHERE CASE WHEN department = 'IT' OR city = 'Beijing' THEN 1 ELSE 0 END = 1;
- 耗时:890ms(PostgreSQL),1120ms(MySQL)
-
原因:
CASE在WHERE中无法利用索引,仍是全表扫描 -
结论:
CASE不是OR的性能替代品,而是逻辑增强工具
方案3:LATERAL JOIN(高级技巧,PostgreSQL 特有)
SELECT e.id, e.name, e.department, e.city
FROM employees e,
LATERAL (VALUES (e.department = 'IT'), (e.city = 'Beijing')) AS v(flag)
WHERE v.flag;
- 耗时:67ms(PostgreSQL),不支持(MySQL)
-
本质:将
OR拆为横向生成的布尔值,再过滤 - 适用:复杂动态条件,但学习成本高,非必要不推荐
实测结论:
对绝大多数场景,
UNION ALL
是最可靠、最易理解、性能最好的
OR
替代方案
。它把一个难优化的逻辑,拆成多个易优化的子问题。而
CASE WHEN
应用于
SELECT
列的动态计算(如
SELECT name, CASE WHEN salary>10000 THEN 'high' ELSE 'low' END
),而非
WHERE
过滤。
5. 真实世界排错手记:那些让我熬夜到凌晨三点的 OR 问题
5.1 问题1:明明有索引,为什么 OR 查询还是全表扫描?
现象
:
employees
表有
INDEX ON department
和
INDEX ON city
,但
SELECT * FROM employees WHERE department='HR' OR city='Shanghai'
仍走
Seq Scan
,耗时 1.2 秒。
排查过程 :
-
第一步:
EXPLAIN确认是Seq Scan; -
第二步:
SELECT COUNT(*) FROM employees WHERE department='HR'→ 12,450 行(1.0%); -
第三步:
SELECT COUNT(*) FROM employees WHERE city='Shanghai'→ 89,200 行(7.4%); -
第四步:
SELECT COUNT(*) FROM employees WHERE department='HR' OR city='Shanghai'→ 101,500 行(8.4%); -
第五步:
SHOW random_page_cost;→ 4.0(默认值,太高); -
第六步:
SET random_page_cost = 1.1; EXPLAIN→ 出现BitmapOr!
根因
:PostgreSQL 的
random_page_cost
参数默认为 4.0,表示随机 I/O 比顺序 I/O 慢 4 倍。优化器认为,用两个索引各扫一遍(随机 I/O),再合并,成本高于直接顺序扫描全表。当
random_page_cost
降低到 1.1(SSD 环境),优化器重新评估,认为索引合并更优。
解决方案 :
-
生产库调优:
ALTER SYSTEM SET random_page_cost = 1.1;(SSD)或1.5(NVMe); -
应用层兜底:在 SQL 中强制 hint(如 PostgreSQL 的
/*+ BitmapOr */,需安装 pg_hint_plan); -
终极方案:建覆盖索引
CREATE INDEX idx_dept_city_cover ON employees(department, city, id, name, salary),让Bitmap Heap Scan变成Index Only Scan。
5.2 问题2:OR 条件中的 NULL 值引发的“静默漏删”
现象
:
DELETE FROM employees WHERE status = 'inactive' OR last_login < '2020-01-01'
,执行后发现部分
last_login
为
NULL
的
inactive
员工未被删除。
分析
:SQL 标准中,
NULL = 'anything'
返回
UNKNOWN
,而
WHERE
子句只接受
TRUE
。所以
last_login < '2020-01-01'
对
NULL
值返回
UNKNOWN
,整行被过滤掉。但
status = 'inactive'
为
TRUE
,所以
TRUE OR UNKNOWN
结果是
TRUE
,应该被删。矛盾在哪?查表结构:
last_login
列定义为
TIMESTAMP WITH TIME ZONE DEFAULT NULL
,但应用层插入时,有些旧数据
last_login
被设为
'0001-01-01'
(非法日期),而数据库将其转为
NULL
。
EXPLAIN
显示
Filter: ((status = 'inactive'::text) OR (last_login < '2020-01-01'::timestamp with time zone))
,但
last_login
实际存储为
NULL
。
真相
:
last_login < '2020-01-01'
对
NULL
计算得
UNKNOWN
,而
OR
的真值表是:
TRUE OR UNKNOWN = TRUE
,
FALSE OR UNKNOWN = UNKNOWN
,
UNKNOWN OR UNKNOWN = UNKNOWN
。所以
status='inactive'
为
TRUE
的行,无论
last_login
是什么,都会被删。漏删的行,
status
实际是
'inactive '
(末尾有空格)!
TRIM(status) = 'inactive'
才为
TRUE
。
教训 :
提示:
OR查询中,对字符串列用=比较前,务必确认数据清洁度。用SELECT * FROM employees WHERE status LIKE 'inactive%'查看真实值。
注意:NULL在OR中不导致漏判,但数据质量问题(空格、大小写、非法值)才是元凶。
建议:在WHERE中显式处理NULL,如WHERE status = 'inactive' OR (last_login IS NOT NULL AND last_login < '2020-01-01'),语义更清晰。
5.3 问题3:跨数据库的 OR 行为差异——MySQL 的隐式类型转换陷阱
现象
:同一 SQL
SELECT * FROM employees WHERE id = 123 OR name = 'John'
,在 MySQL 5.7 上秒出结果,在 MySQL 8.0 上慢 20 倍。
深挖
:
id
是
BIGINT
,
name
是
VARCHAR(100)
。MySQL 5.7 的隐式转换规则是:将字符串
'John'
转为数字
0
(因为
'John'
无法转为有效数字),所以
name = 'John'
变成
name = 0
,由于
name
是字符串,比较时
0
被转为
'0'
,最终
name = '0'
,这是一个低选择性条件,但索引还能用。而 MySQL 8.0 严格了类型转换,
name = 'John'
保持原样,但优化器发现
id = 123
有索引,
name = 'John'
也有索引,却选择了
Index Merge
,而
Index Merge
在 8.0 的实现有 Bug,导致锁竞争加剧。
解决
:升级到 MySQL 8.0.28+,或强制指定字符集:
WHERE id = 123 OR name COLLATE utf8mb4_0900_as_cs = 'John'
,让优化器放弃
Index Merge
,改用
UNION
。
6. 我的 OR 使用军规:十二条铁律与一个终极检查清单
写完这篇,我翻出自己近三年的 SQL 笔记,总结出十二条必须刻在脑子里的
OR
使用铁律。它们不是理论,而是从一次次线上故障、一次次慢查询日志、一次次
EXPLAIN
分析中淬炼出来的:
-
铁律1:写 OR 前,先问“这两个条件的数据分布是否高度重叠?”
如果重叠率 > 30%,优先用
AND重构逻辑,或用UNION显式控制。 - 铁律2:任何含 OR 的 WHERE 子句,必须有对应列的索引。没有索引的 OR,就是定时炸弹。
-
铁律3:在 UPDATE/DELETE 中用 OR,必须用
SELECT COUNT(*)预估影响行数,且必须在事务中执行。 -
铁律4:OR 连接的条件,禁止出现函数调用(如
UPPER(name) = 'JOHN'),否则索引必然失效。 -
铁律5:字符串比较,统一用
COLLATE指定排序规则,避免大小写、空格导致的隐式转换。 -
铁律6:数值比较,确认数据类型一致。
INT列不要和字符串'123'比较,避免隐式转换。 - **铁律7:NULL 值不是问题,数据脏才是。用

1745

被折叠的 条评论
为什么被折叠?



