上周优化了一条慢查询,从最开始的2秒多,最后优化到20毫秒以内。过程中踩了好几个索引的坑,有些是以为建了索引就会走,有些是索引建了但没生效。记录一下,也给大家提个醒。
问题背景
事情是这样的,运营反馈后台的文章列表页打开特别慢,有时候要转好几秒。我看了一下慢查询日志,发现这条查询耗时2.3秒:
SELECT * FROM blog_posts
WHERE status = 'published'
AND category_id = 2
AND created_at > '2026-01-01'
ORDER BY created_at DESC
LIMIT 20;
表数据量大概50万条,按说这个查询应该很快才对。一看索引,status、category_id、created_at三个字段分别都建了索引,但就是慢。
误区一:每个字段都建索引就会走索引
很多人以为,WHERE条件里的每个字段都建了索引,查询就会快。其实不是这样的。
MySQL在大多数情况下,一条查询只能使用一个索引(除非是index merge,但适用场景有限)。如果三个字段分别建了三个独立索引,优化器会选择其中一个选择性最好的,其他条件只能在回表后再过滤。
比如上面的查询,如果优化器选择了category_id的索引,先找出分类2的所有文章(可能有十几万条),然后回表逐条判断status和created_at,再排序,最后取20条。这能快才怪。
正确做法:建联合索引,把WHERE条件里的字段和ORDER BY的字段组合起来。
ALTER TABLE blog_posts
ADD INDEX idx_cat_status_time (category_id, status, created_at);
这样MySQL可以直接在索引里完成过滤和排序,不需要回表,也不需要额外排序。
误区二:联合索引的字段顺序无所谓
联合索引的字段顺序非常重要,遵循"最左前缀原则"。
比如上面的联合索引(category_id, status, created_at),可以支持:
- WHERE category_id = ?
- WHERE category_id = ? AND status = ?
- WHERE category_id = ? AND status = ? AND created_at = ?
但不能支持:
- WHERE status = ?(跳过了最左的category_id)
- WHERE created_at = ?(跳过了前两个字段)
所以建联合索引的时候,要把等值查询的字段放前面,范围查询和排序的字段放后面。一般来说,选择性高的字段(区分度大的)放前面效果更好。
误区三:索引建了就一定会用
建了索引不代表优化器一定会用。优化器会基于成本估算选择执行计划,如果它觉得全表扫描更快,就会放弃索引。
常见的索引失效场景:
1. 对索引列做函数运算或类型转换
-- 索引失效
WHERE DATE(created_at) = '2026-01-01'
-- 索引生效
WHERE created_at >= '2026-01-01' AND created_at < '2026-01-02'
2. 隐式类型转换
比如字段是字符串类型,但查询时传了数字,MySQL会做隐式转换,导致索引失效。
3. LIKE以通配符开头
-- 索引失效
WHERE title LIKE '%PHP%'
-- 索引生效(前缀匹配)
WHERE title LIKE 'PHP%'
4. OR连接的条件
如果OR两边的条件有一个没有索引,整个查询都不会走索引。
5. 数据量小的时候
表只有几百条数据的时候,全表扫描可能比走索引还快,优化器会选择全表扫描。这是正常的,不用纠结。
误区四:ORDER BY的字段单独建索引
很多人会给ORDER BY的字段单独建索引,以为这样排序就快了。其实如果WHERE条件和ORDER BY用的不是同一个索引,MySQL还是需要回表后再排序(Using filesort)。
正确的做法是把WHERE条件和ORDER BY的字段建在同一个联合索引里,让索引本身就是有序的,这样就不需要额外排序了。
比如我们的查询,联合索引(category_id, status, created_at)中,created_at是最后一个字段,在前面两个字段等值的情况下,created_at本身就是有序的,所以ORDER BY created_at DESC可以直接利用索引的顺序,不需要filesort。
误区五:EXPLAIN看到Using index就够了
EXPLAIN是优化SQL的必备工具,但不要只看type和key,还要看Extra和rows。
- type:最好是const、eq_ref、ref,避免ALL(全表扫描)
- key:实际使用的索引
- rows:预估扫描的行数,越小越好
- Extra:
- Using index:覆盖索引,不需要回表,最好
- Using where:回表后过滤
- Using filesort:需要额外排序,不好
- Using temporary:需要临时表,更不好
优化完我们的查询后,EXPLAIN结果:type=ref,key=idx_cat_status_time,rows=1000左右,Extra=Using index condition。完美。
最终效果
优化前:2.3秒,扫描15万行,Using filesort
优化后:18毫秒,扫描1000行,Using index condition
性能提升了100多倍。
一些建议
1. 不要盲目建索引。索引不是越多越好,每个索引都会增加写入的开销,也会占用存储空间。建索引前先想清楚查询模式。
2. 用EXPLAIN分析慢查询。不要靠猜,看执行计划才是最靠谱的。
3. 联合索引优于多个单列索引。对于多条件查询,一个好的联合索引比三个单列索引有用得多。
4. 定期检查慢查询日志。把慢查询阈值设低一点(比如100ms),定期分析,把问题消灭在萌芽状态。
5. 索引也要维护。随着数据变化,索引的选择性可能会变化,定期用ANALYZE TABLE更新统计信息。
数据库优化是个细活,没有银弹。多观察、多分析、多测试,才能找到最适合自己业务的方案。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录