慢查询的根源往往不是数据库“变慢”,而是缺失索引或一次扫描了过多数据行,解决这两点就能消除绝大多数性能瓶颈。
为什么你的SQL语句越跑越慢:先看索引,再看扫描行数
当一条查询从前几年的毫秒级响应退化到现在的几秒甚至几十秒,很多人第一反应是升级硬件或调整数据库参数,但业内专家指出,超过九成的慢查询问题都出在访问路径上,数据库引擎就像一位图书管理员,如果书架没有索引目录,他只能逐本翻书;如果索引建错了,他依然要翻阅大量无关书页。
缺失索引的典型症状:全表扫描在作祟
你可以通过执行计划快速判断,在MySQL中执行EXPLAIN SELECT ...,如果type列显示为ALL,说明发生了全表扫描,此时rows列会显示预估扫描行数这个数字往往接近整张表的行数。
常见的错误场景包括:
- 在
WHERE条件列上完全没建索引 - 复合索引的最左前缀原则被违反,比如索引是
(a,b)但查询条件只用了b - 对索引列使用了函数或隐式类型转换,导致索引失效
- 用
LIKE '%关键词'这种前置模糊查询,索引也无法使用
一次扫描过多数据行的隐蔽陷阱
即使有索引,如果查询需要回表读取大量行,性能依然堪忧,比如SELECT 从一张宽表(20个字段以上)中取数,即便走索引定位到1万行,回表读取这1万行的完整数据也远比只读3个字段要慢得多,这就是覆盖索引的价值让索引本身包含查询所需的所有列,避免回表。
另一个常被忽视的问题是分页深翻页。LIMIT 100000, 20会让数据库先扫描前100020行,再丢弃前100000行,这种场景下,扫描行数远超实际返回行数,属于典型“一次扫描过多数据行”。
定位慢查询的具体操作路径:从开启日志到看懂执行计划
如果你正在排查一个线上慢查询,按以下步骤操作,每一步都能直接落地。
第一步:开启慢查询日志并设置阈值
在MySQL配置文件(如/etc/my.cnf)中设置:
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
long_query_time = 1

表示超过1秒的查询会被记录下来,设置后重启MySQL服务,然后用mysqldumpslow -s at /var/log/mysql/slow.log按平均耗时排序查看Top N慢SQL。
第二步:用EXPLAIN分析执行计划
拿到慢SQL后,在SQL前加EXPLAIN,重点关注四列:
- type:从好到差依次是
const > eq_ref > ref > range > index > ALL,看到ALL或index就要警惕。 - key:实际使用的索引名,如果为
NULL,说明没走任何索引。 - rows:预估扫描行数,这个值除以总行数,如果比例超过30%,即使有索引,优化器也可能放弃索引。
- Extra:出现
Using filesort或Using temporary意味着排序或分组需要额外处理,通常也能通过索引优化消除。
第三步:检查索引选择性
索引不是越多越好,关键是选择性。SELECT COUNT(DISTINCT column_name) / COUNT() FROM table,这个比值越接近1,索引选择性越好。对于性别字段(只有男女两种值),建索引几乎没有意义,因为扫描一半数据行和全表扫描差别不大。
如何设计索引让慢查询彻底消失:覆盖索引与复合索引实战
行业共识认为,索引设计的核心是“减少扫描行数 + 避免回表”,下面针对两种高频场景给出可直接套用的方案。
高频查询场景:按用户ID查最近订单
假设订单表orders有字段user_id、status、created_at,常用查询是:
SELECT order_id, amount, created_at FROM orders WHERE user_id = 123 AND status = 'paid' ORDER BY created_at DESC LIMIT 10;
最合适的索引是(user_id, status, created_at),为什么?user_id等值过滤先把扫描行数缩小到该用户的订单,status进一步过滤,created_at用于排序这样排序操作也能通过索引完成,避免filesort,查询字段order_id、amount、created_at都在索引中,构成覆盖索引,回表也被省去。
低效索引的改造范例
原索引(user_id, created_at),执行上面的SQL时,status无法通过索引过滤,数据库会先按

user_id和created_at取出该用户的全部订单,再逐行判断status,当该用户有几千条历史订单时,扫描行数就是几千,改造为三列复合索引后,扫描行数可能降到个位数。一次查询从慢到快,本质上是扫描行数断崖式下降。
索引失效的常见场景:数据写多了之后为何查询反而变慢
很多开发者遇到的问题是:上线初期查询很快,运行几个月后越来越慢,除了数据量增长外,还有几个重要原因。
索引失效的四大隐形杀手
- 对索引列做计算:
WHERE YEAR(created_at) = 2026,这会让索引失效,应该改写为WHERE created_at >= '2026-01-01' AND created_at < '2026-01-01'。 - 隐式类型转换:索引列是
varchar类型,查询却传入整数WHERE phone = 13800138000,MySQL会隐式转换,导致索引失效。 - 前导通配符:
WHERE name LIKE '%张'无法使用索引,但WHERE name LIKE '张%'可以。 - OR条件连接:
WHERE user_id = 1 OR status = 'paid',如果两个条件中只有一个列有索引,优化器可能全表扫描,改写为UNION ALL或者用IN替代。
数据分布变化导致优化器选择错误
即使索引存在,如果数据分布不均衡,优化器也可能放弃索引,例如订单表里status = 'failed'的记录占80%,查询WHERE status = 'failed'时,优化器认为全表扫描比走索引更划算,这时对应的解决方案是根据业务规律调整查询策略比如添加is_failed布尔字段,或者把失败订单归档到单独的表。
慢查询优化后的效果如何验证:对比扫描行数与响应时间
优化不能靠“感觉”,要用数据说话,优化前记录以下基线数据:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 执行时间(秒) | 2 | 04 |
| 扫描行数 | 480,000 | 12 |
| 返回行数 | 20 | 20 |
| type类型 | ALL | ref |
执行ANALYZE TABLE更新统计信息后,再次运行慢SQL,确认执行计划中的

rows列已经骤降。响应时间从秒级到毫秒级,核心指标就是扫描行数的下降。
关于索引数量与写入性能的取舍
索引过多会拖慢INSERT和UPDATE,因为每次写入都要同步维护索引,如果一张表有10个索引,每次写入就要更新10棵B+树,对于读多写少的业务,索引可以适当多建;对于高并发写入的场景,需要权衡,业内常见做法是:单表索引数量控制在5个以内,联合索引尽量覆盖多个高频查询。
慢查询的运维预防机制:巡检SQL与定期清理冗余索引
治标更要治本,建议建立以下机制:
- 每周用
performance_schema或sys.schema_unused_indexes查看从未被使用的索引,直接删除。 - 每月分析慢查询日志,识别出重复的复合索引,例如已经有
(a,b),又建了(a),后者通常可以删掉。 - 大表结构变更时,使用
pt-online-schema-change工具在线添加索引,避免锁表导致业务中断。
数据库慢查询常见问题解答
为什么我建了索引,查询还是很慢?
最可能的原因是查询语句没有满足索引的使用条件,比如复合索引(a,b),查询只用了b列;或者对索引列使用了函数、隐式转换,建议用EXPLAIN看key列,如果为NULL,说明索引没被使用,也可能是因为查询返回了大量字段导致回表开销过大。
数据库表中数据量大到什么程度才需要考虑分区?
单表数据量超过2000万行且仍然有较频繁的查询时,可以考虑分区,但分区不是解决慢查询的首选方案,先检查索引和SQL写法,分区更适合按时间归档的场景,比如把近3个月和3个月前的数据分到不同分区,查询时能自动裁剪分区,减少扫描行数。
优化慢查询时,应该优先改SQL还是建索引?
优先建索引,前提是SQL本身没有额外干扰索引使用的写法,如果SQL中有SELECT 或深翻页,即使建了索引也可能效果有限。正确顺序是:先改写SQL消除索引失效因素,再设计覆盖索引,最后用执行计划验证,很多时候,一个复合索引就能取代两条满含OR条件的原始SQL。