适用版本:MySQL 8.0.x
一、为什么“会建索引”还不够
很多人学 MySQL 优化时,往往先学会两件事:
- 建索引
- 看
EXPLAIN
但真正进入项目后,很快就会发现:
- 建了索引,SQL 还是慢
EXPLAIN看起来没问题,线上依然卡- 慢查询并不总是由索引缺失引起
- 有些 SQL 慢在排序、回表、分页、JOIN、统计、返回结果过大
所以,查询优化不能只盯着某一个点,而要形成一套完整的方法。本文就从实际开发最常见的几个维度,整理一套可落地的 MySQL 查询优化实践。
二、先明确:查询为什么会慢
一条 SQL 慢,常见原因通常落在以下几类:
1. 扫描的数据太多
典型表现:
- 全表扫描
- 索引选择性差
- 范围过大
- 分页偏移量过深
2. 回表太多
虽然用了二级索引,但每命中一条记录都要去聚簇索引取整行,累计成本很高。
3. 排序或分组代价高
典型信号:
Using filesortUsing temporary
4. JOIN 方式不理想
例如:
- 驱动表结果过大
- 关联列没有索引
- 子查询写法导致执行路径变复杂
5. SQL 写法阻碍索引使用
比如:
- 对索引列做函数
- 隐式类型转换
LIKE '%xxx'OR使用不当
6. 返回结果本身就过大
即使路径合理,如果一次查几十万行数据,网络传输、应用层处理也会很慢。
三、查询优化的基本流程
我更建议把 SQL 优化当成一个固定流程,而不是“猜哪里慢”。
第一步:确认慢在哪里
常见工具:
- 慢查询日志(slow query log)
performance_schemasys库视图- 业务监控平台
第二步:查看执行计划
常见工具:
EXPLAINEXPLAIN ANALYZEEXPLAIN FORMAT=TREE
第三步:判断问题属于哪一类
例如:
- 没走索引
- 索引不合理
- 回表多
- 排序慢
- JOIN 慢
- 分页慢
- 写法不佳
第四步:针对性改写 SQL 或调整索引
这一步才是具体优化动作。
第五步:再次验证
验证手段包括:
- 对比执行计划
- 对比耗时
- 对比扫描行数
- 对比 CPU / I/O / 临时表 / 排序情况
这个闭环非常重要:
优化不是“改了代码”,而是“验证过变快了”。
四、定位慢查询的常用工具
1. 慢查询日志
慢查询日志适合定位高耗时 SQL。
常见配置思路:
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
你可以根据业务情况设置阈值,例如 1 秒、500ms 等。
适合排查:
- 经常慢的 SQL
- 高频慢查询
- 版本升级后的异常 SQL
2. performance_schema
performance_schema 能提供更细粒度的 SQL 执行统计。
例如配合汇总视图,可以看:
- 哪类 SQL 执行最频繁
- 平均耗时最高的是谁
- 总耗时最高的是谁
3. sys 库
MySQL 8.0 自带的 sys 库对 performance_schema 做了更友好的封装。
例如:
SELECT *
FROM sys.statement_analysis
ORDER BY avg_latency DESC
LIMIT 10;
这类视图非常适合日常巡检。
4. EXPLAIN ANALYZE
当你已经定位到某条 SQL,就可以进一步用它看真实执行成本:
EXPLAIN ANALYZE
SELECT * FROM orders WHERE customer_id = 1001;
五、实践一:让 WHERE 条件真正利用索引
这是最常见、也是收益最大的优化方向。
1. 避免对索引列做函数
不推荐:
SELECT *
FROM orders
WHERE DATE(created_at) = '2026-06-01';
更好:
SELECT *
FROM orders
WHERE created_at >= '2026-06-01 00:00:00'
AND created_at < '2026-06-02 00:00:00';
原因:
- 第一种写法破坏了索引列的原始有序性
- 第二种写法可以让范围查询更容易使用索引
2. 避免隐式类型转换
假设:
phone VARCHAR(20)
不推荐:
SELECT * FROM users WHERE phone = <MOBILE_1b1f18>;
更好:
SELECT * FROM users WHERE phone = '<MOBILE_215df0>';
3. 避免前导模糊匹配
不推荐:
SELECT * FROM article WHERE title LIKE '%MySQL';
更好:
SELECT * FROM article WHERE title LIKE 'MySQL%';
如果业务必须做全文检索,普通 B+Tree 索引未必合适,可能要考虑全文索引或搜索引擎。
4. 注意 OR 条件
例如:
SELECT *
FROM orders
WHERE customer_id = 1001 OR order_status = 1;
这类 SQL 是否高效,要看两侧条件是否都能较好使用索引。很多时候,拆成两条 SQL 再合并结果,反而更清晰也更可控。
六、实践二:用联合索引优化高频多条件查询
假设有表:
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
customer_id BIGINT NOT NULL,
order_status TINYINT NOT NULL,
created_at DATETIME NOT NULL,
amount DECIMAL(10,2) NOT NULL,
INDEX idx_customer_status_time(customer_id, order_status, created_at)
) ENGINE=InnoDB;
1. 高频查询场景
SELECT *
FROM orders
WHERE customer_id = 1001
AND order_status = 1
AND created_at >= '2026-06-01 00:00:00';
这个查询很适合联合索引:
customer_id:等值过滤order_status:等值过滤created_at:范围过滤
2. 为什么比单列索引更好
如果只建:
INDEX(customer_id)
INDEX(order_status)
INDEX(created_at)
优化器虽然有时会尝试合并,但通常不如一个设计合理的联合索引稳定高效。
3. 联合索引列顺序建议
常见经验:
- 高频等值过滤列放前面
- 范围列放后面
- 兼顾排序和分组需求
4. 不要盲目堆很多列
过长的联合索引会带来:
- 更大的索引体积
- 更高的维护成本
- 更复杂的优化器选择
所以要围绕核心 SQL 场景设计。
七、实践三:尽量使用覆盖索引,减少回表
1. 什么场景适合覆盖索引
最典型的是列表查询、后台筛选页、报表页。
例如:
SELECT customer_id, order_status, created_at
FROM orders
WHERE customer_id = 1001
AND order_status = 1;
如果索引是:
INDEX(customer_id, order_status, created_at)
那么这个查询就有机会直接从索引返回结果,而不需要回表。
2. 为什么覆盖索引常常收益明显
因为它减少了:
- 聚簇索引读取
- 随机 I/O
- 回表次数
3. 什么时候不适合为了覆盖而过度扩列
不要为了覆盖索引,把大量低频字段都塞进索引里。否则会造成:
- 索引膨胀
- 写入变慢
- 缓存命中率下降
优化的关键是:
优先覆盖高频、小结果集、价值高的查询。
八、实践四:优化排序和分组
1. ORDER BY 慢的常见原因
如果执行计划里出现:
Using filesort
说明 MySQL 不能直接利用索引顺序,需要额外排序。
例如:
SELECT *
FROM orders
WHERE customer_id = 1001
ORDER BY amount DESC;
如果没有匹配 amount 排序的索引,结果集又不小,就可能很慢。
2. 让索引兼顾过滤与排序
例如高频 SQL:
SELECT customer_id, order_status, created_at
FROM orders
WHERE customer_id = 1001 AND order_status = 1
ORDER BY created_at DESC;
那么索引:
INDEX(customer_id, order_status, created_at)
就很可能同时支持:
- 过滤
- 排序
- 覆盖索引
3. GROUP BY 的优化思路
如果执行计划出现:
Using temporaryUsing filesort
通常说明分组过程开销较高。
优化方向包括:
- 提前缩小扫描范围
- 使用更匹配分组列顺序的索引
- 避免对大范围原始数据直接分组
- 必要时先做汇总表或中间结果表
九、实践五:深分页优化
深分页是业务里很常见、也很容易被忽视的问题。
1. 问题示例
SELECT *
FROM orders
ORDER BY id
LIMIT 100000, 20;
这个 SQL 的问题在于:
- MySQL 需要先跳过前 100000 行
- 再取后 20 行
偏移量越深,代价越大。
2. 更好的做法:基于游标 / 上次位置翻页
例如:
SELECT *
FROM orders
WHERE id > 100000
ORDER BY id
LIMIT 20;
这种方式通常比 LIMIT offset, size 更高效。
3. 适合场景
- 时间线
- 滚动加载
- 后台列表连续翻页
如果业务一定要支持任意页跳转,则需要综合考虑:
- 是否能限制最大页数
- 是否能缓存结果
- 是否能改用搜索引擎或离线分页方案
十、实践六:JOIN 查询优化
1. 先看关联列是否有索引
例如:
SELECT o.id, o.amount, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.order_status = 1
AND c.city = 'Shanghai';
重点关注索引:
orders(customer_id, order_status)或围绕业务重建联合索引customers(id)通常是主键customers(city)或(city, ...)是否适合高频过滤
2. 让驱动表尽量小
如果先筛选出来的结果集很大,再去 JOIN 另一张表,性能就容易变差。
经验上:
- 尽量先过滤强的一侧
- 让驱动表返回尽可能少的行
3. 避免无意义的大字段参与连接或返回
如果只需要 id 和 name,就不要直接 SELECT *。
4. 注意子查询是否可以改写
有些子查询写法最终会被优化器很好处理,但有些场景改写成显式 JOIN 更容易观察和优化。
十一、实践七:减少不必要的数据返回
这是非常基础但经常被忽略的一点。
1. 不要习惯性 SELECT *
不推荐:
SELECT * FROM orders WHERE customer_id = 1001;
更好:
SELECT id, customer_id, order_status, created_at
FROM orders
WHERE customer_id = 1001;
收益包括:
- 减少网络传输
- 减少应用层反序列化成本
- 更容易做覆盖索引
2. 只查真正需要的行数
例如后台导出场景,不要一次把百万级数据直接同步查出并返回页面。
3. 对列表页加合理 LIMIT
很多慢查询其实只是因为“没有边界”。
十二、实践八:统计信息与优化器选择
有时候 SQL 明明有合适索引,优化器却没选,可能原因之一就是统计信息不够准确。
1. 重新分析表统计信息
ANALYZE TABLE orders;
适合场景:
- 大量数据变更后
- 索引刚调整后
- 执行计划明显异常时
2. 查看索引情况
SHOW INDEX FROM orders;
重点看:
Cardinality基数是否合理- 是否存在重复或冗余索引
3. 不要动不动强制索引
FORCE INDEX 可以作为临时排查工具,但不建议把它当成长期兜底方案。因为数据分布一变,今天的“强制最优”可能变成明天的“强制最差”。
十三、一个完整优化案例
场景
有一条列表查询:
SELECT *
FROM orders
WHERE customer_id = 1001
AND order_status = 1
ORDER BY created_at DESC
LIMIT 20;
当前现象:
- 查询耗时偏高
EXPLAIN显示有Using filesort- 查询返回的是订单列表页的前 20 条
分析
这条 SQL 有三个关键点:
- 按
customer_id过滤 - 按
order_status过滤 - 按
created_at倒序排序 - 只取前 20 条
优化思路
建立索引:
CREATE INDEX idx_customer_status_created
ON orders(customer_id, order_status, created_at);
同时把查询改成只取必要列:
SELECT id, customer_id, order_status, created_at
FROM orders
WHERE customer_id = 1001
AND order_status = 1
ORDER BY created_at DESC
LIMIT 20;
预期收益
- 更容易利用联合索引过滤
- 更容易利用索引顺序完成排序
- 更容易形成覆盖索引
- 减少回表和排序代价
验证
使用:
EXPLAIN ANALYZE
SELECT id, customer_id, order_status, created_at
FROM orders
WHERE customer_id = 1001
AND order_status = 1
ORDER BY created_at DESC
LIMIT 20;
对比优化前后:
- 实际耗时
- 扫描行数
- 是否还存在
Using filesort - 是否出现
Using index
这就是一个完整的优化闭环。
十四、查询优化中的常见误区
1. 一慢就加索引
索引很重要,但不是所有问题都靠加索引解决。
2. 只看是否走索引,不看扫描行数
走了索引,但扫了几十万行,依然可能很慢。
3. 只盯数据库,不看业务返回量
有些查询慢不是数据库算得慢,而是查得太多、传得太多、处理得太多。
4. 只优化单条 SQL,不优化整体模式
例如一个页面连续触发 50 条“小 SQL”,整体体验依然会差。
5. 线上直接盲改
优化要尽量在测试环境验证,至少做到:
- 有基线
- 有对比
- 有回滚思路
十五、查询优化实战清单
下面给一个平时很实用的排查清单:
1. SQL 本身
- 是否
SELECT * - 是否有函数计算
- 是否有隐式类型转换
- 是否存在深分页
- 是否有不必要的
OR
2. 索引设计
- 是否有合适索引
- 联合索引顺序是否合理
- 是否能覆盖查询
- 是否存在冗余索引
3. 执行计划
key是什么type是否合理rows大不大Extra是否有Using filesort/Using temporary
4. 数据分布
- 条件过滤是否真的有选择性
- 统计信息是否准确
- 是否最近数据量变化很大
5. 业务侧
- 是否真的需要这么多列
- 是否真的需要这么多行
- 是否可以缓存
- 是否可以异步化、批量化、预聚合
十六、小结
查询优化实践,归根到底不是某一个“技巧”的堆砌,而是一套持续有效的方法:
- 先定位慢 SQL,再分析执行计划。
- 优先解决扫描行数过大、回表过多、排序分组开销高的问题。
- 围绕真实高频 SQL 设计联合索引,而不是凭感觉加索引。
- 尽量减少
SELECT *、深分页、低效 JOIN 和不必要的数据返回。 - 每次优化都要验证前后执行计划和真实耗时。
如果把前面几篇文章串起来,你会发现 MySQL 优化其实有一条很清晰的主线:
- 先理解索引
- 再理解 B+Tree 和联合索引
- 然后学会看
EXPLAIN - 最后把这些能力落到真实 SQL 优化中
做到这一步,你就已经不再是“会写 SQL”,而是在逐步具备“会优化 SQL”的能力了。
📝 版权声明:本文为原创技术博客,转载请注明出处。
如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!