MySQL索引优化:让查询快如闪电
以下内容主要来自 MySQL 官方文档中关于 索引优化、ORDER BY 与 GROUP BY 的执行方式 的部分。这些内容涉及数据库查询优化器如何利用索引来避免排序(filesort
)和减少临时表的使用,从而提升查询性能。
下面我将用通俗易懂的方式为你系统地解释这些核心概念,并帮助你理解文档中的每一个要点。
一、核心思想:让索引代替排序
在 MySQL 中,如果一个查询包含 ORDER BY
或 GROUP BY
,数据库通常需要对结果进行排序或分组。如果没有合适的索引,MySQL 就必须:
- 先查出所有匹配的数据行;
- 把它们放入内存或磁盘上的临时表;
- 再进行排序或分组 —— 这个过程叫做 filesort(文件排序),非常耗资源。
但如果有一个合适的 有序索引(如 BTREE 索引),并且查询结构能“顺着”这个索引走,那么就可以:
✅ 直接按顺序读取数据,跳过排序步骤!
这就是整个文档的核心目标:通过合理设计索引和 SQL 写法,让 MySQL 利用索引的“天然顺序”,避免额外排序和临时表。
二、什么时候可以用索引避免 ORDER BY
排序?
✅ 情况1:ORDER BY 字段是索引的最左前缀,且方向一致
-- 假设索引是 (key_part1, key_part2)
SELECT * FROM t1 ORDER BY key_part1, key_part2;
✔️ 可以利用索引顺序,无需排序。
因为 BTREE 索引本身就是先按
key_part1
排,再按key_part2
排,所以遍历索引就是有序的。
✅ 情况2:WHERE 条件固定了前面字段,ORDER BY 后面字段
-- 索引仍是 (key_part1, key_part2)
SELECT * FROM t1
WHERE key_part1 = 'const'
ORDER BY key_part2;
✔️ 能用索引!
为什么?
因为 key_part1 = const
相当于锁定了索引树的一个子集(比如只看 key_part1='A'
的那一块),这一块内部已经按照 key_part1, key_part2
排好序了,而 key_part1
是常量,所以剩下的自然就是按 key_part2
排好的。
👉 相当于在一个“二级目录”里,内容已经是排好序的。
✅ 情况3:多个字段 DESC 排序,但索引也是 DESC 或可反向扫描
SELECT * FROM t1 ORDER BY key_part1 DESC, key_part2 DESC;
✔️ 如果索引是 (key_part1 ASC, key_part2 ASC)
,也能用!
→ 因为 MySQL 支持 反向扫描索引(backward index scan)。
更进一步:
SELECT * FROM t1 ORDER BY key_part1 DESC, key_part2 ASC;
✔️ 也可以用索引,只要索引定义支持混合方向(MySQL 8.0+ 支持 降序索引):
CREATE INDEX idx ON t1(key_part1 DESC, key_part2 ASC);
📌 关键点:索引的方向要和 ORDER BY 匹配,或者可以通过反向扫描模拟出来。
✅ 情况4:范围查询 + ORDER BY 主键本身
SELECT * FROM t1 WHERE key_part1 > 10 ORDER BY key_part1 ASC;
✔️ 可以用索引 (key_part1, ...)
,因为:
- 先找到
>10
的第一个位置, - 然后顺着索引往后读,每一条都满足条件且自动有序。
同理:
SELECT * FROM t1 WHERE key_part1 < 10 ORDER BY key_part1 DESC;
✔️ 也可以用索引,只是从右往左扫描。
✅ 情况5:复合条件 + ORDER BY 后续字段
SELECT * FROM t1
WHERE key_part1 = 'c1' AND key_part2 > 'c2'
ORDER BY key_part2;
✔️ 依然可以利用索引 (key_part1, key_part2)
!
原因还是:key_part1
固定 → 锁定一个范围;在这个范围内,key_part2
是有序的。
❌ 哪些情况不能用索引排序?(即必须 filesort)
以下是文档列出的典型 无法使用索引排序 的场景:
场景 | 示例 | 原因 |
---|---|---|
使用不同索引的列排序 | ORDER BY key1, key2 (key1 和 key2 属于不同索引) | 索引之间无顺序关系 |
非连续使用复合索引 | WHERE key2=const ORDER BY key1_part1, key1_part3 | 跳过了中间列,破坏了最左前缀原则 |
排序列不在同一个索引中 | WHERE key2=const ORDER BY key1 | 扫描的是 key2 索引,但排序要用 key1,不匹配 |
对表达式排序 | ORDER BY ABS(key) , ORDER BY -key | 表达式改变了原始值,索引存的是原值 |
多表 JOIN,ORDER BY 不在驱动表上 | JOIN 多张表,ORDER BY 的列不在第一个被扫描的非 const 表上 | 数据是拼接出来的,顺序混乱 |
ORDER BY 和 GROUP BY 不一致 | GROUP BY a; ORDER BY b | 分组和排序字段不同 |
三、GROUP BY 如何利用索引?
类似 ORDER BY
,GROUP BY
也需要对数据分组。传统做法是:
- 查出所有行 → 放进临时表 → 哈希分组 or 排序分组。
但如果索引设计得好,可以直接利用索引的有序性来“跳着读”,实现高效分组。
有两种主要方式:
1. Loose Index Scan(松散索引扫描)
💡 思想:每个组只读一条记录即可确定该组的存在,不需要遍历所有行。
适用条件:
- 查询只涉及一张表;
GROUP BY
的列构成索引的 最左前缀;- 没有聚合函数依赖于未参与分组的列;
- 最重要:索引是有序的(BTREE)。
🌰 例子:
-- 索引: (a, b, c)
SELECT a, b, COUNT(*) FROM t1 GROUP BY a, b;
✔️ 可以使用 Loose Index Scan:
- 直接沿着索引走,遇到新的
(a,b)
组合就开启新组; - 不需要加载所有行到内存。
⚠️ 不适用的情况:
GROUP BY b, c; -- 不是最左前缀
GROUP BY a, d; -- d 不在索引中
注意:Loose Index Scan 在 MySQL 5.7+ 中受限较多,8.0 引入了 Skip Scan 后有所改善。
2. Tight Index Scan(紧密索引扫描)
💡 思想:先做一个范围扫描(range scan),取出所有符合条件的行,然后对这些行做排序或哈希分组。
虽然不如 Loose 快,但比全表扫描+临时表好。
适用于:
- 有 WHERE 条件限制;
- GROUP BY 列仍是索引前缀;
- 但条件导致无法“跳跃”访问。
四、Descending Indexes(降序索引)—— MySQL 8.0 新特性
以前的索引都是升序(ASC)存储,即使写 ORDER BY col DESC
,也只能通过 反向扫描 实现。
但在某些复杂排序中,比如:
ORDER BY col1 DESC, col2 ASC
旧版本可能无法有效利用索引。
从 MySQL 8.0 开始,支持创建 真正的降序索引:
CREATE INDEX idx ON t1(col1 DESC, col2 ASC);
这样就能完美匹配混合排序需求,极大提高这类查询的性能。
五、总结:如何写出能用索引优化的 ORDER BY / GROUP BY?
技巧 | 说明 |
---|---|
✅ 使用最左前缀 | ORDER BY / GROUP BY 的列应构成索引的最左开始部分 |
✅ 让 WHERE 固定前导列 | 如 col1 = const ,可以让后续列直接利用索引顺序 |
✅ 注意排序方向 | 尽量让 ORDER BY 方向与索引一致,或使用降序索引 |
✅ 单一索引覆盖 | 所有排序/分组字段应在同一个 BTREE 索引中 |
✅ 避免表达式 | ORDER BY -col 、ABS(col) 等会破坏索引使用 |
✅ 合理设计复合索引 | 根据常用查询模式建立 (a,b) 、(a,b,c) 类型索引 |
✅ 升级到 MySQL 8.0+ | 支持降序索引、Skip Scan,大幅提升灵活性 |
六、实战建议
假设你有一张订单表:
CREATE TABLE orders (user_id INT,create_time DATETIME,amount DECIMAL(10,2),status TINYINT,INDEX idx_user_time (user_id, create_time)
);
以下查询都可以 避免排序:
-- ✔️ 固定 user_id,按时间排序
SELECT * FROM orders
WHERE user_id = 123
ORDER BY create_time DESC;-- ✔️ 按 user_id 分组,统计最早下单时间
SELECT user_id, MIN(create_time)
FROM orders
GROUP BY user_id;-- ✔️ 范围查询 + 正序输出
SELECT * FROM orders
WHERE create_time > '2024-01-01'
ORDER BY create_time;
但这个不行:
-- ❌ create_time 不是最左前缀
SELECT * FROM orders
WHERE create_time > '2024-01-01'
ORDER BY user_id;
需要加额外索引才能优化。
如果你愿意,我可以根据你的具体表结构和查询语句,帮你分析是否能用索引优化排序或分组。
希望这份解读能帮你彻底理解那几段技术文档 😊