慢查询不是一句“加索引”能解决
MySQL 慢查询最常见的误区是:看到接口慢,马上给 where 字段加索引。
有时这确实有效,但更多时候问题没这么简单:
- SQL 写法导致索引用不上
- where 条件选择性太差
- order by 和 limit 触发 filesort
- join 顺序不合理
- 查询返回字段太多
- 应用层一次发了太多查询
- 慢的不是 SQL,而是锁等待或网络传输
所以慢查询排查要按流程来,先确认现象,再定位 SQL,最后才谈改索引。
第一步:确认慢在哪里
用户说“页面很慢”,不等于数据库慢。
先拆成几段:
浏览器渲染时间
↓
接口响应时间
↓
后端业务处理时间
↓
数据库查询时间
↓
网络传输时间如果接口本身 80ms,页面 3 秒才可交互,那是前端问题。
如果接口 5 秒,但 SQL 只有 20ms,可能是后端循环、第三方接口或文件处理。
如果接口 5 秒,SQL 占 4.8 秒,再进入数据库排查。
不要跳过这个步骤。否则很容易优化了半天索引,结果瓶颈在别处。
第二步:找到具体 SQL
慢查询需要定位到具体 SQL,不能只看接口名。
常用方法:
- 打开 MySQL slow query log
- 在应用日志里记录 SQL 耗时
- 对可疑接口加临时计时
- 查看数据库监控里的 Top SQL
记录 SQL 时最好同时记录:
- SQL 模板
- 参数
- 执行耗时
- 返回行数
- 调用接口
- 用户或请求 id
只知道“某接口慢”不够,因为一个接口里可能有多条 SQL。
第三步:用 EXPLAIN 看执行计划
拿到 SQL 后,先跑:
EXPLAIN SELECT ...重点看这几个字段:
| 字段 | 关注点 |
|---|---|
| type | 访问类型,ALL 通常表示全表扫描 |
| possible_keys | 理论上可用的索引 |
| key | 实际使用的索引 |
| rows | 预计扫描行数 |
| filtered | 条件过滤比例 |
| Extra | 是否 Using filesort / Using temporary |
一个典型问题:
type: ALL
key: NULL
rows: 500000
Extra: Using where这说明 MySQL 可能在扫全表。
但看到 ALL 也不要立刻恐慌。如果表只有几百行,全表扫描可能比走索引还快。优化要看数据量和真实耗时。
第四步:检查 where 条件
索引能不能用上,很大程度取决于 where 条件。
常见导致索引失效的写法:
WHERE DATE(created_at) = '2026-06-08'函数包住了索引列,可能导致索引用不上。更好的写法:
WHERE created_at >= '2026-06-08 00:00:00'
AND created_at < '2026-06-09 00:00:00'另一个常见问题是前置通配:
WHERE title LIKE '%nuxt%'普通 BTree 索引很难支持这种查询。此时要考虑全文索引、搜索引擎或降低需求范围。
第五步:设计联合索引
单列索引不是万能的。真实查询经常是:
SELECT id, title, published_at
FROM posts
WHERE status = 'published'
AND category_id = 3
ORDER BY published_at DESC
LIMIT 20这类 SQL 可以考虑联合索引:
CREATE INDEX idx_posts_status_category_published
ON posts (status, category_id, published_at);联合索引设计要看:
- 等值条件放前面
- 范围条件通常放后面
- order by 能否顺着索引走
- 选择性太低的字段单独建索引意义不大
不要机械背“最左前缀”,要结合具体 SQL 看执行计划。
第六步:减少返回和扫描
有时慢不是因为 where,而是因为返回太多。
坏例子:
SELECT *
FROM access_logs
ORDER BY created_at DESC
LIMIT 1000;如果 request_body、user_agent 这类字段很长,SELECT * 会带来额外 IO 和网络传输。
更好的做法:
SELECT id, ip, method, path, status_code, response_time_ms, created_at
FROM access_logs
ORDER BY created_at DESC
LIMIT 100;原则:
- 只查页面真正需要的字段
- 列表页不要带详情大字段
- 分页 limit 不要过大
- 详情页再查详情
第七步:关注排序和分页
分页越往后越慢,常见于:
SELECT id, title
FROM posts
ORDER BY created_at DESC
LIMIT 100000, 20;MySQL 需要跳过前面 100000 行,成本很高。
如果业务允许,可以用游标分页:
SELECT id, title, created_at
FROM posts
WHERE created_at < ?
ORDER BY created_at DESC
LIMIT 20;后台管理列表通常还能接受传统分页,但高流量前台列表或无限滚动更适合游标分页。
第八步:验证而不是凭感觉
改完索引或 SQL 后,至少验证:
- EXPLAIN 是否使用预期索引
- 扫描 rows 是否下降
- 实际耗时是否下降
- 返回结果是否一致
- 写入性能是否受影响
- 是否影响其他查询
索引不是免费午餐。它会增加写入成本和存储空间。小表可以少建索引,大表要谨慎建对。
一个排查清单
遇到慢查询,按这个顺序看:
- 慢的是页面、接口还是 SQL?
- 具体慢 SQL 是哪一条?
- SQL 参数和返回行数是多少?
- EXPLAIN 的 type、key、rows、Extra 怎么样?
- where 条件是否对索引列做了函数或隐式转换?
- 是否需要联合索引而不是单列索引?
- order by / limit 是否能利用索引?
- 是否 SELECT 了不必要的大字段?
- 深分页是否需要改成游标分页?
- 改完后是否做了对照验证?
总结
慢查询优化的核心不是“多建索引”,而是减少不必要的扫描、排序和传输。
先定位 SQL,再看执行计划,再根据查询模式设计索引和改写 SQL。每一步都要验证,不要靠感觉。这样做虽然慢一点,但能避免把数据库优化成一堆没人敢删的索引。
下次见。
-EOF-