MySQL 慢查询优化做了这么多,为什么还是慢

MySQL 慢查询优化做了这么多,为什么还是慢

加了索引还是慢,EXPLAIN 看了半天看不懂,优化后没什么效果——这是很多团队做慢查询优化时的真实状态。

问题往往不是某个技巧没掌握,而是分析路径走反了。

一个典型场景

一张订单表,1000万数据,业务反馈后台导订单越来越慢:

SELECT * FROM orders
WHERE user_id = 12345
AND status = 'paid'
AND created_at > '2026-01-01'
ORDER BY created_at DESC
LIMIT 50;

执行时间 8 秒。团队开始排查:

  • 查了执行计划,用了 idx_user_status_created 索引
  • 查了慢日志,单次查询 8 秒
  • 加了复合索引 (user_id, status, created_at)

然后呢?还是 8 秒。

问题不在索引够不够多,在于分析路径走反了。

大多数人会先查 EXPLAIN,但 EXPLAIN 看什么

EXPLAIN SELECT * FROM orders
WHERE user_id = 12345
AND status = 'paid'
AND created_at > '2026-01-01'
ORDER BY created_at DESC
LIMIT 50;

结果大概是:

id  select_type  table    type   key                   rows     Extra
1   SIMPLE       orders   ref    idx_user_status_created  523847  Using index condition; Using where; Using filesort

typeref,走了索引,rows 返回 52 万行——问题就藏在这里。

rows 不是“返回 50 行”,而是“扫描了 52 万行再过滤”。这个数字才是关键。

很多人只看 type 是否 ALL,以为不是全表扫描就没事。但索引扫描 50 万行再取 50 条,本身就是性能杀手。

真正的问题:数据分布决定了查询成本

这条 SQL 慢,不是因为索引不够,而是因为 user_id = 12345 这个用户有 52 万条订单。

MySQL 选错索引,或者被迫选了一个过滤性差的索引,根因往往是统计信息不准。

-- 查看表统计信息
SHOW TABLE STATUS FROM db LIKE 'orders';

-- 查看字段基数
SHOW INDEX FROM orders;

-- 重新统计
ANALYZE TABLE orders;

ANALYZE TABLE 是很多人忽略的一步。InnoDB 的统计信息是采样的,大数据量变化后可能严重失真,导致优化器选错索引。

怎么确认慢在哪

EXPLAIN ANALYZE(MySQL 8.0+)可以看实际执行成本:

EXPLAIN ANALYZE SELECT * FROM orders
WHERE user_id = 12345
AND status = 'paid'
AND created_at > '2026-01-01'
ORDER BY created_at DESC
LIMIT 50;

输出会包含:

  • 预估 rows 和实际 rows
  • 每一阶段的实际耗时
  • 索引有没有真正被用上

如果 rows 预估和实际差距极大,先跑 ANALYZE TABLE

另一个容易忽略的点:ORDER BY 带了 filesort

看前面的 EXPLAIN 结果,Extra 里有一项 Using filesort

这才是真正的性能瓶颈。filesort 是 MySQL 在内存里做排序,当数据量大时:

  • 排序数据超过 sort_buffer_size,会落盘
  • 跨索引排序本身就要回表

如果排序字段在索引里,可以直接利用索引顺序,避免 filesort

-- 把 created_at 加入索引尾部
ALTER TABLE orders
ADD INDEX idx_user_status_created (user_id, status, created_at);

-- 查询改为覆盖索引,无需回表
SELECT created_at, order_id, status, amount FROM orders
WHERE user_id = 12345
AND status = 'paid'
AND created_at > '2026-01-01'
ORDER BY created_at DESC
LIMIT 50;

但注意:ORDER BY created_at DESC 需要跟索引顺序一致。如果索引是 ASC 而查询是 DESC,MySQL 可能无法利用索引顺序。

还有一个常见问题:LIMIT offset 过大

后台分页常见的写法:

SELECT * FROM orders
WHERE user_id = 12345
ORDER BY created_at DESC
LIMIT 100000, 50;

这种写法 MySQL 需要先扫描前 10 万行,再返回后面的 50 行。翻页越深越慢。

优化思路:

  1. 改用游标分页(keyset pagination)
-- 第一页
SELECT * FROM orders
WHERE user_id = 12345
ORDER BY created_at DESC
LIMIT 50;

-- 后续页:记住上一页最后一条的 created_at
SELECT * FROM orders
WHERE user_id = 12345
AND created_at < '2026-04-01 10:30:00'
ORDER BY created_at DESC
LIMIT 50;
  1. 或者限制最大翻页深度,超过后提示用户精确搜索

加了索引还是慢,常常是这几个原因

  1. 统计信息不准ANALYZE TABLE 可以解决
  2. 选了过滤性差的索引:强制指定索引 USE INDEX
  3. ORDER BY 无法利用索引顺序:调整索引列顺序或排序方向
  4. 回表成本高:改用覆盖索引
  5. offset 过大:改用游标分页

一个完整的排查流程

-- 1. 开启慢查询日志,看哪些查询经常超时
SHOW VARIABLES LIKE 'slow_query_log%';
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;

-- 2. 查看慢查询记录
SHOW GLOBAL STATUS LIKE 'Slow_queries';
-- 或直接查慢日志文件

-- 3. 确认统计信息
ANALYZE TABLE orders;

-- 4. 看执行计划
EXPLAIN ANALYZE SELECT ...;

-- 5. 如果有问题,强制指定索引验证
EXPLAIN SELECT * FROM orders USE INDEX (idx_xxx) WHERE ...;

-- 6. 改写 SQL 后再验证
EXPLAIN SELECT ... LIMIT offset, count;

最关键的一条:先确认问题在哪,再动手

大多数慢查询优化失败,是因为:

  • 一上来就加索引,没有先确认问题是索引选错还是统计信息不准
  • 改了索引但没有重验证 EXPLAIN
  • 改了 SQL 但没有确认执行计划变化

慢查询优化是个验证循环:

定位慢 SQL → 看执行计划 → 确认瓶颈 → 修改 → 验证效果

每一步都要有数据支撑,不是凭感觉加索引。

后来怎么优化的

回到开头的场景,加了索引、做了 ANALYZE TABLE、改了 SQL 结构后:

  • 单次查询从 8 秒降到 200ms
  • 主要改动:强制使用 (user_id, created_at) 索引,改为覆盖索引,移除 status 条件(业务实际不需要精确过滤)

优化不是堆索引,是让查询精准命中数据。

最后更新 4/20/2026, 6:02:32 AM