返回首页

13|查询优化实践

适用版本:MySQL 8.0.x

一、为什么“会建索引”还不够

很多人学 MySQL 优化时,往往先学会两件事:

  • 建索引
  • EXPLAIN

但真正进入项目后,很快就会发现:

  • 建了索引,SQL 还是慢
  • EXPLAIN 看起来没问题,线上依然卡
  • 慢查询并不总是由索引缺失引起
  • 有些 SQL 慢在排序、回表、分页、JOIN、统计、返回结果过大

所以,查询优化不能只盯着某一个点,而要形成一套完整的方法。本文就从实际开发最常见的几个维度,整理一套可落地的 MySQL 查询优化实践。


二、先明确:查询为什么会慢

一条 SQL 慢,常见原因通常落在以下几类:

1. 扫描的数据太多

典型表现:

  • 全表扫描
  • 索引选择性差
  • 范围过大
  • 分页偏移量过深

2. 回表太多

虽然用了二级索引,但每命中一条记录都要去聚簇索引取整行,累计成本很高。

3. 排序或分组代价高

典型信号:

  • Using filesort
  • Using temporary

4. JOIN 方式不理想

例如:

  • 驱动表结果过大
  • 关联列没有索引
  • 子查询写法导致执行路径变复杂

5. SQL 写法阻碍索引使用

比如:

  • 对索引列做函数
  • 隐式类型转换
  • LIKE '%xxx'
  • OR 使用不当

6. 返回结果本身就过大

即使路径合理,如果一次查几十万行数据,网络传输、应用层处理也会很慢。


三、查询优化的基本流程

我更建议把 SQL 优化当成一个固定流程,而不是“猜哪里慢”。

第一步:确认慢在哪里

常见工具:

  • 慢查询日志(slow query log)
  • performance_schema
  • sys 库视图
  • 业务监控平台

第二步:查看执行计划

常见工具:

  • EXPLAIN
  • EXPLAIN ANALYZE
  • EXPLAIN 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 temporary
  • Using 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. 避免无意义的大字段参与连接或返回

如果只需要 idname,就不要直接 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 有三个关键点:

  1. customer_id 过滤
  2. order_status 过滤
  3. created_at 倒序排序
  4. 只取前 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. 业务侧

  • 是否真的需要这么多列
  • 是否真的需要这么多行
  • 是否可以缓存
  • 是否可以异步化、批量化、预聚合

十六、小结

查询优化实践,归根到底不是某一个“技巧”的堆砌,而是一套持续有效的方法:

  1. 先定位慢 SQL,再分析执行计划。
  2. 优先解决扫描行数过大、回表过多、排序分组开销高的问题。
  3. 围绕真实高频 SQL 设计联合索引,而不是凭感觉加索引。
  4. 尽量减少 SELECT *、深分页、低效 JOIN 和不必要的数据返回。
  5. 每次优化都要验证前后执行计划和真实耗时。

如果把前面几篇文章串起来,你会发现 MySQL 优化其实有一条很清晰的主线:

  • 先理解索引
  • 再理解 B+Tree 和联合索引
  • 然后学会看 EXPLAIN
  • 最后把这些能力落到真实 SQL 优化中

做到这一步,你就已经不再是“会写 SQL”,而是在逐步具备“会优化 SQL”的能力了。


📝 版权声明:本文为原创技术博客,转载请注明出处。

如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!

上一篇

12| 执行计划 EXPLAIN 分析

下一篇

14 |事务与 ACID 特性