1. 核心原因:索引的数据结构特性
1.1 B+Tree 索引的扫描方式
MySQL 的 InnoDB 引擎使用 **B+Tree**索引,这种索引的特点是:
-
有序存储:索引键值按顺序存储
-
范围查询高效:可以快速定位到范围的起点和终点
-
最左前缀匹配:必须从索引的最左列开始匹配
1.2 OR 操作的逻辑特性
WHERE condition1 OR condition2意味着:
-
满足 condition1 或 condition2 的记录都要返回
-
相当于两个结果集的并集
2. 具体场景分析
2.1 场景一:所有 OR 字段都有索引
-- 假设 name 和 age 字段都有独立索引
SELECT * FROM users WHERE name = 'Alice' OR age = 25;
执行计划:
-
使用
name索引查找name = 'Alice'的记录 -
使用
age索引查找age = 25的记录 -
对两个结果集进行索引合并(Index Merge)
-
合并后去重,回表获取完整数据
结论:可以使用索引,性能较好
2.2 场景二:部分 OR 字段有索引
-- 假设 name 有索引,但 age 没有索引
SELECT * FROM users WHERE name = 'Alice' OR age = 25;
执行计划:
-
使用
name索引查找name = 'Alice'的记录 -
对于
age = 25的条件,由于 age 没有索引,必须:-
全表扫描 所有记录
-
逐行检查
age = 25的条件
-
-
合并两个结果集
问题:
一旦需要全表扫描,使用索引的优势就丧失了
优化器通常会选择直接全表扫描,因为:
索引查找 + 全表扫描的成本 > 直接全表扫描
避免多次随机 I/O 操作
3. 深入理解:为什么不能部分使用索引
3.1 数据访问的物理特性
-- 假设有 1000 万条数据
-- name='Alice' 有 100 条(有索引)
-- age=25 有 10 万条(无索引)
SELECT * FROM users WHERE name = 'Alice' OR age = 25;
如果尝试部分使用索引:
-
通过 name 索引找到 100 条记录(快速)
-
需要检查剩下的 999.99 万条记录是否符合 age=25
-
检查过程需要:
-
读取所有数据页
-
逐行判断 age 条件
-
-
实际上比直接全表扫描更慢
3.2 优化器的决策逻辑
MySQL 优化器会计算各种执行计划的成本:
-
全表扫描成本:读取所有数据页的成本
-
索引合并成本:多个索引查找 + 合并 + 回表的成本
-
部分索引成本:索引查找 + 部分全表扫描的成本
通常结果 :当有字段无索引时,部分索引的成本最高 ,优化器会选择全表扫描。
4. 索引合并(Index Merge)的局限性
4.1 支持的索引合并类型
MySQL 支持三种索引合并:
-
Index Merge Intersection:多个索引的交集(AND)
-
Index Merge Union:多个索引的并集(OR)
-
Index Merge Sort-Union:排序后的并集
4.2 索引合并的前提条件
-- 只有 name 和 age 都有索引时,才能使用 Index Merge Union
EXPLAIN SELECT * FROM users
WHERE name = 'Alice' OR age = 25;
-- 执行计划会显示:
-- type: index_merge
-- Extra: Using union(idx_name, idx_age); Using where
必要条件:
-
每个 OR 条件都必须有可用的索引
-
索引必须是单列索引或复合索引的最左前缀
-
查询条件相对简单
5. 实际验证示例
5.1 创建测试表
CREATE TABLE user_test (
id INT PRIMARY KEY,
name VARCHAR(100),
age INT,
email VARCHAR(100),
INDEX idx_name (name),
INDEX idx_age (age)
);
-- 插入测试数据
INSERT INTO user_test VALUES
(1, 'Alice', 25, 'alice@test.com'),
(2, 'Bob', 30, 'bob@test.com'),
(3, 'Charlie', 25, 'charlie@test.com');
5.2 测试不同场景
场景 A:两个字段都有索引
EXPLAIN SELECT * FROM user_test
WHERE name = 'Alice' OR age = 25;
-- 结果:使用 index_merge
-- key: idx_name,idx_age
-- type: index_merge
场景 B:只有 name 有索引
-- 删除 age 索引
DROP INDEX idx_age ON user_test;
EXPLAIN SELECT * FROM user_test
WHERE name = 'Alice' OR age = 25;
-- 结果:全表扫描
-- type: ALL
-- key: NULL
6. 解决方案和最佳实践
6.1 解决方案
方案 1:为所有 OR 字段创建索引
-- 确保所有 OR 条件的字段都有索引
CREATE INDEX idx_name ON users(name);
CREATE INDEX idx_age ON users(age);
方案 2:使用 UNION 重写查询
-- 原始查询
SELECT * FROM users WHERE name = 'Alice' OR age = 25;
-- 重写为 UNION
SELECT * FROM users WHERE name = 'Alice'
UNION
SELECT * FROM users WHERE age = 25;
方案 3:使用覆盖索引
-- 如果只需要索引列,可以避免回表
SELECT name, age FROM users
WHERE name = 'Alice' OR age = 25;
6.2 最佳实践
-
避免在 WHERE 子句中随意使用 OR
-
分析查询模式,为频繁查询的字段创建索引
-
使用 EXPLAIN 分析执行计划
-
考虑使用 UNION 替代 OR
-
对于复杂查询,考虑使用全文索引或其他解决方案
7. 总结
MySQL 中 OR 条件要求所有字段都有索引的根本原因是:
-
B+Tree 索引的特性:只能高效处理索引字段的查询
-
OR 操作的逻辑:需要获取多个条件的结果集并集
-
性能考虑:部分字段无索引时,优化器会选择全表扫描
-
索引合并的限制:要求所有参与合并的索引都必须存在