MySQL `SELECT` 查询优化:原理 + 案例 + 实战总结
一、访问方法优化(Access Methods)
✅ 核心思想:
选择最高效的路径读取数据:能用索引就不用全表扫描,能跳过就别遍历。
🔹 案例1:索引查找 vs 全表扫描
-- 表结构
CREATE TABLE users (id INT PRIMARY KEY,name VARCHAR(50),age INT,city VARCHAR(30),INDEX idx_age (age)
);-- 查询:找出所有30岁的人
SELECT * FROM users WHERE age = 30;
❓ 执行方式?
- 有
idx_age
索引 → 使用 Index Range Scan,只扫描age=30
的部分。 - 无索引 → 全表扫描(
type: ALL
),性能差。
✅ 结论:为常用于查询条件的列建立索引。
🔹 案例2:Skip Scan(MySQL 8.0+ 新特性)
-- 复合索引
CREATE INDEX idx_name_age ON users(name, age);-- 查询:找年龄为25的所有人(不指定name)
SELECT * FROM users WHERE age = 25;
❓ 能用索引吗?
- 在 MySQL < 8.0.16:❌ 不能,因为跳过了前导列
name
。 - 在 MySQL ≥ 8.0.16:✅ 可以!优化器使用 Skip Scan:
- 遍历不同的
name
值; - 对每个
name
查找age=25
的记录。
- 遍历不同的
✅ 结论:升级到 MySQL 8.0+ 后,某些“看似无法用索引”的查询也能被优化。
二、JOIN 连接优化
✅ 核心思想:
小结果集驱动大表,优先使用索引连接(Index Nested Loop)
🔹 案例3:高效 JOIN 顺序
-- 表结构
CREATE TABLE orders (order_id INT PRIMARY KEY,user_id INT,amount DECIMAL(10,2),INDEX idx_user_id (user_id)
);
CREATE TABLE users (id INT PRIMARY KEY,name VARCHAR(50)
);-- 查询:获取每个用户的订单总额
SELECT u.name, SUM(o.amount)
FROM users u
JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;
❓ 执行计划?
- 先扫描
users
表(驱动表); - 对每条用户记录,在
orders
表中通过idx_user_id
快速查找其所有订单(Index Lookup); - 聚合计算。
✅ 优点:避免了全表扫描
orders
,减少了 I/O。
⚠️ 如果反过来(先扫
orders
),就需要临时表或排序来去重用户,效率更低。
🔹 案例4:常量表优化
SELECT *
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.id = 100;
❓ 优化器怎么做?
u.id = 100
是主键等值查询 →users
表是 常量表;- 先查出用户100的信息;
- 再用
o.user_id = 100
去orders
表查订单; - 整个过程极快。
✅ 结论:WHERE 中的主键/唯一索引等值查询会被优先处理。
三、ORDER BY 优化(避免 filesort)
✅ 核心思想:
让索引的物理顺序匹配 ORDER BY 的逻辑顺序,避免额外排序。
🔹 案例5:利用复合索引避免排序
-- 索引
CREATE INDEX idx_city_age ON users(city, age);-- 查询:某城市用户按年龄升序排列
SELECT name, age FROM users
WHERE city = 'Beijing'
ORDER BY age ASC;
❓ 是否需要 filesort?
- ✅ 不需要!
- 因为
city='Beijing'
锁定了索引的一个范围; - 在这个范围内,
age
已经有序。
✅ 结论:WHERE 固定前导列 + ORDER BY 后续列 = 可用索引排序。
🔹 案例6:混合排序方向(MySQL 8.0 降序索引)
-- 创建降序索引
CREATE INDEX idx_age_desc_name_asc ON users(age DESC, name ASC);-- 查询:按年龄倒序,同龄人按名字正序
SELECT name, age FROM users
ORDER BY age DESC, name ASC;
❓ 是否需要 filesort?
- ✅ 不需要!索引方向完全匹配。
- 如果没有这个索引,即使有
(age, name)
升序索引,也可能需要反向扫描或 filesort。
✅ 结论:MySQL 8.0 的 降序索引 极大提升了复杂排序的性能。
四、GROUP BY 优化(避免临时表 + filesort)
✅ 核心思想:
利用索引的有序性直接分组,而不是先把所有数据拉出来再分。
🔹 案例7:Loose Index Scan(松散索引扫描)
-- 索引
CREATE INDEX idx_city_age ON users(city, age);-- 查询:统计每个城市的最小年龄
SELECT city, MIN(age) FROM users
GROUP BY city;
❓ 如何执行?
- 优化器使用 Loose Index Scan:
- 沿着索引走,遇到新的
city
就开启新组; - 当前组的第一条记录的
age
就是最小值;
- 沿着索引走,遇到新的
- ✅ 无需加载所有行,无需临时表。
✅ 结论:适用于聚合函数如
MIN()
、MAX()
,且分组列是索引前缀。
🔹 案例8:Tight Index Scan(紧密索引扫描)
-- 查询:北京各年龄段人数
SELECT age, COUNT(*) FROM users
WHERE city = 'Beijing'
GROUP BY age;
❓ 执行方式?
- 使用
idx_city_age
索引; - 扫描
city='Beijing'
的所有记录; - 因为
age
在这部分中有序,可以直接分组计数; - ✅ 避免了显式排序。
✅ 结论:即使不能跳跃,只要有序,就能高效分组。
五、子查询优化
🔹 案例9:IN 子查询 → Semi-Join 优化
-- 查询:有订单的用户信息
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders);
❓ 优化器怎么做?
- 默认尝试 Semi-Join:
- 将子查询与外层合并,避免重复;
- 可能使用物化(Materialization)缓存子查询结果;
- 更高效,且可利用索引。
✅ 对比:如果写成
NOT IN
,可能退化为 Anti-Semi-Join,需注意 NULL 值陷阱。
六、COUNT(*) 优化
🔹 案例10:MyISAM vs InnoDB
SELECT COUNT(*) FROM users;
存储引擎 | 性能 |
---|---|
MyISAM | ⚡ 极快,直接从元数据读取行数 |
InnoDB | 🐢 较慢,需扫描聚簇索引(但会选最小索引) |
✅ 建议:对大表频繁 COUNT,可考虑用缓存或计数器表。
七、WHERE/HAVING 优化
🔹 案例11:表达式下推与 IS NULL 优化
SELECT * FROM users
WHERE age IS NULL;
- ✅ 可使用索引(如果
age
有索引); - 优化器将其视为一种“等值查询”;
- 访问类型可能是
ref
或range
。
❌ 对比:
WHERE age + 1 = 10
→ 无法使用索引,必须全表扫描。
🏁 终极总结:MySQL SELECT 优化 Checklist
类别 | 最佳实践 | 错误做法 |
---|---|---|
索引设计 | 建立复合索引 (a,b,c) 支持 WHERE a=... ORDER BY b | 只给每个列单独建索引 |
ORDER BY | 让排序列是索引最左前缀或后续列 | 对表达式排序 ORDER BY UPPER(name) |
GROUP BY | 分组列是索引前缀,用 MIN/MAX 利用 Loose Scan | GROUP BY b 但索引是 (a,b) 且未过滤 a |
JOIN | 小表驱动大表,ON 条件有索引 | 多表 JOIN 无索引,导致 BNL 和临时表 |
子查询 | 使用 EXISTS 替代 IN (尤其大数据量) | NOT IN 子查询包含 NULL 值 |
COUNT | 大表 COUNT 考虑缓存或近似值 | 直接 COUNT(*) 查千万级表 |
表达式 | 避免在列上使用函数:WHERE YEAR(date)=2024 → WHERE date BETWEEN ... | WHERE DATE(create_time) = '2024-01-01' |
工具 | 用 EXPLAIN 分析执行计划 | 写完 SQL 就跑,不管性能 |
版本 | 升级到 MySQL 8.0+ 享受 Skip Scan、降序索引等新特性 | 停留在 5.7,错过重要优化 |
🛠️ 实战建议:如何分析一条慢查询?
-
使用
EXPLAIN
查看执行计划EXPLAIN FORMAT=JSON SELECT ...
-
关注:
type
: 是否为ALL
(全表扫描)?key
: 是否用了索引?rows
: 扫描行数是否过多?Extra
: 是否有Using filesort
,Using temporary
?
-
优化策略:
- 添加合适的复合索引;
- 重写 SQL 避免函数操作;
- 拆分复杂查询;
- 考虑分区或缓存。
如果你提供一条具体的慢 SQL,我可以帮你一步步分析并给出优化方案。希望这份 “原理 + 案例 + 总结”三位一体 的指南能真正帮你掌握 MySQL 查询优化!