范围查询截断后面列原因
问题现象
联合索引中,一旦某列使用了范围查询(如 >, <, BETWEEN, LIKE '张%'),后面的列就无法使用索引了。
-- 索引:idx_a_b_c(a, b, c)
-- 范围查询截断了后面的列
SELECT * FROM t WHERE a = 1 AND b > 10 AND c = 3;
-- a: ✅ 索引(等值)
-- b: ⚠️ 索引(范围)
-- c: ❌ 索引失效,回表后过滤
根本原因:B+树的有序性被破坏
sequenceDiagram
participant Q as 查询条件
participant B as B+树索引
Q->>B: 查找 a=1 的位置
B-->>Q: ✅ 找到 a=1 的起始页
Note over B,Q: 等值匹配,可以精确定位
Q->>B: 在 a=1 范围内查找 b>10
B-->>Q: ✅ 定位到 b=10 之后的位置
Note over B,Q: 范围匹配,找到起始点
Q->>B: 在 b>10 范围内查找 c=3
B-->>Q: ❌ 无法精确定位
Note over B,Q: b>10 范围内<br/>c 的值不保证有序!
索引内部的有序性说明
联合索引 (a, b, c) 的有序性
-- 索引中的实际存储顺序
-- ↓是按 (a, b, c) 排序的结果
graph LR
A["a=1, b=1, c=9"] --> B["a=1, b=1, c=8"]
B --> C["a=1, b=2, c=7"]
C --> D["a=1, b=2, c=5"]
D --> E["a=1, b=3, c=1"]
E --> F["a=1, b=3, c=12"]
F --> G["a=1, b=4, c=2"]
G --> H["a=1, b=4, c=6"]
H --> I["a=1, b=5, c=3"]
I --> J["a=1, b=5, c=10"]
重要观察:
- 全局有序:a 列全局有序 ✅
- a相同时:b 列在 a=1 的范围内有序 ✅
- a、b都相同时:c 列在 (a=1, b=某个值) 的范围内有序 ✅
- 但:c 列在 b>3 的范围内不是有序的!
在 b>3 的范围(b=4, b=5)中,c 的值为:2, 6, 3, 10
这些值不是递增或递减的,无法二分查找!
具体示例
-- 创建表和索引
CREATE TABLE employee (
dept_id INT,
salary INT,
name VARCHAR(50),
INDEX idx_dept_salary_name(dept_id, salary, name)
) ENGINE=InnoDB;
-- 插入示例数据
INSERT INTO employee VALUES
(1, 8000, '张三'),
(1, 12000, '李四'),
(1, 15000, '王五'),
(2, 9000, '赵六'),
(2, 11000, '钱七'),
(2, 13000, '孙八');
-- 查询:部门1中工资>10000的员工姓名
SELECT * FROM employee
WHERE dept_id = 1 AND salary > 10000 AND name = '王五';
执行计划分析
EXPLAIN SELECT * FROM employee
WHERE dept_id = 1 AND salary > 10000 AND name = '王五';
| id | select_type | table | type | key | rows | Extra |
|---|---|---|---|---|---|---|
| 1 | SIMPLE | employee | range | idx_d_s_n | 2 | Using index condition |
type = range(范围扫描),name 列索引失效。
执行过程:
1. 通过 dept_id = 1 定位到部门1的范围 ✅
2. 在部门1范围内通过 salary > 10000 定位到起始位置 ✅
3. 扫描 salary > 10000 的所有数据,逐行检查 name = '王五' ❌
如何绕过这个限制
方案一:调整列顺序
-- 把范围查询的列放在最后
-- ❌ 当前索引:idx(dept_id, salary, name)
-- 范围列 salary 在中间,截断了 name
-- ✅ 改造索引:把范围放最后
CREATE INDEX idx_dept_name_salary(dept_id, name, salary);
-- 等值列 name 在中间,不会被截断
-- 现在的查询
SELECT * FROM employee
WHERE dept_id = 1 AND salary > 10000 AND name = '王五';
-- dept_id: ✅ 等值匹配
-- name: ✅ 等值匹配(在中间也没问题)
-- salary: ⚠️ 范围匹配(放最后不会截断任何列)
方案二:拆成两个查询
-- 先用覆盖索引筛选
SELECT id FROM employee
WHERE dept_id = 1 AND salary > 10000;
-- 再按需回表
SELECT * FROM employee
WHERE dept_id = 1 AND name = '王五' AND salary > 10000;
方案三:使用 IN 代替范围
-- ❌ 范围查询(截断后面的列)
SELECT * FROM t WHERE a = 1 AND b > 10 AND c = 3;
-- ✅ IN 列表(等值查询,不会截断)
SELECT * FROM t WHERE a = 1 AND b IN (11, 12, 13, 15, 20) AND c = 3;
-- IN 实际上被优化器转换为多个等值条件
-- b 没有被当作范围,c 还能用索引
常见误解澄清
-- ❌ 误解1:只要是范围查询就截断
SELECT * FROM t WHERE a > 5 AND b = 10;
-- a > 5 是范围,a 后面的 b=10 也用不了索引
-- 因为 a>5 范围内 b 无序
-- ✅ 误解2:等值+等值+范围=可以
SELECT * FROM t WHERE a = 1 AND b = 2 AND c > 5;
-- a=1等值 ✅, b=2等值 ✅, c>5范围 ✅(c 后面没列了)
-- 这样没问题!因为等值+等值确定了精确的子范围
-- ✅ 误解3:LIKE 前缀匹配不是范围
SELECT * FROM t WHERE a = 1 AND b LIKE '张%' AND c = 3;
-- LIKE '张%' 是范围(相当于 b >= '张' AND b < '值')
-- c 会被截断,索引用不到
面试要点
- 根本原因:范围查询后的列在 B+树中不再有序,无法二分查找
- 截断规律:从第一个范围查询列开始,后续所有列索引失效
- 避开方法(三选一):
1. 把范围列放索引最后
2. 用 IN 替代范围
3. 拆分查询
一句话总结:范围查询截断是因为 B+树的有序性在某列变成范围后被破坏了——后面的列是"乱序"的,没法用索引。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END


暂无评论内容