news 2026/5/10 8:19:54

慢查询排查实录:从全表扫描到毫秒响应,我只改了一个索引

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
慢查询排查实录:从全表扫描到毫秒响应,我只改了一个索引

慢查询排查实录:从全表扫描到毫秒响应,我只改了一个索引

凌晨三点,生产环境订单接口突然告警,响应时间从200ms直接飙到47秒。我盯着监控大屏上飘红的告警曲线,手心全是汗。就这么一条看似人畜无害的查询语句,差点让整个交易系统瘫痪。接下来三天,我把这条SQL翻来覆去地拆解,索引策略从头捋了一遍,最终把耗时从47秒压到了0.03秒。这篇文章,就是我这次排查的全部心得,从踩坑到方法论,一个字不藏,全掏给你。

一、案发现场:一条查询是怎么把数据库拖垮的

我们先还原一下出问题的那张表。这是一张订单表,数据量大概1200万行,表结构长这样:

sql

CREATE TABLE t_order (

id BIGINT PRIMARY KEY AUTO_INCREMENT,

order_no VARCHAR(32) NOT NULL COMMENT '订单号',

user_id BIGINT NOT NULL COMMENT '用户ID',

shop_id BIGINT NOT NULL COMMENT '店铺ID',

status TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态:0待支付 1已支付 2已发货 3已完成',

total_amount DECIMAL(10,2) NOT NULL COMMENT '订单金额',

create_time DATETIME NOT NULL COMMENT '创建时间',

pay_time DATETIME DEFAULT NULL COMMENT '支付时间',

remark VARCHAR(255) DEFAULT NULL COMMENT '备注',

INDEX idx_user_id (user_id),

INDEX idx_create_time (create_time),

INDEX idx_status (status)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

慢查询日志里抓到的那条SQL是这样的:

sql

SELECT *

FROM t_order

WHERE user_id = 102345

AND status = 2

AND create_time >= '2025-01-01 00:00:00'

ORDER BY create_time DESC

LIMIT 20;

跑一遍EXPLAIN,结果让人头皮发麻:

id select_type table type possible_keys key key_len rows Extra

1 SIMPLE t_order ALL idx_user_id,idx_status,idx_create_time NULL NULL 11876543 Using where; Using filesort

type列显示的是ALL,说明全表扫描;key列是NULL,一个索引都没用上;Extra里明明白白写着Using filesort,排序在内存里硬扛。1200万行数据,逐行过滤再手动排序,47秒一点都不冤。

二、排查过程:三个单列索引为什么集体罢工

1、我第一反应是检查索引有没有建。翻了建表语句,user_id、status、create_time三个字段各有一个单列索引,看起来挺齐全的。但问题恰恰出在这里——MySQL的优化器面对多个单列索引时,在5.7之前只能选其中一个来用,这就是所谓的"索引合并"机制。而索引合并的效率,跟联合索引比起来,差的不是一点半点。

2、我先用FORCE INDEX挨个试了一遍。强制走idx_user_id,扫描行数从1200万降到了8万多,type变成了ref,但还是慢,因为后面还有两个条件要过滤。强制走idx_create_time更惨,时间范围太大,直接扫描了600多万行。单独靠任何一个索引,都接不住这条查询。

3、关键的转折出现在我重新审视WHERE条件的时候。三个条件里,user_id = 102345是等值查询,status = 2也是等值查询,只有create_time >= '2025-01-01'是范围查询。根据联合索引的最左匹配原则,等值条件必须放在最前面,范围条件放在最后。而且查询里还有ORDER BY create_time DESC,这个排序字段也得考虑进去。

4、于是我做了一个改动,把三个单列索引全部删掉,建了一个联合索引:

sql

ALTER TABLE t_order DROP INDEX idx_user_id;

ALTER TABLE t_order DROP INDEX idx_status;

ALTER TABLE t_order DROP INDEX idx_create_time;

ALTER TABLE t_order ADD INDEX idx_user_status_time (user_id, status, create_time);

5、重新EXPLAIN,结果变成了这样:

id select_type table type possible_keys key key_len rows Extra

1 SIMPLE t_order ref idx_user_status_time idx_user_status_time 17 342 Using index condition

type从ALL变成了ref,扫描行数从1200万骤降到342行,key_len是17个字节(user_id占8字节,status占1字节,create_time占8字节),Extra里的Using index condition说明索引下推(ICP)也生效了。查询耗时从47秒直接掉到了0.03秒,我盯着这个数字看了好几遍,确认不是眼花。

三、深入理解:联合索引背后的B+树逻辑

1、为什么联合索引能起飞?核心原因在于B+树的排序特性。联合索引(user_id, status, create_time)在B+树里是按照这三个字段的字典序排列的:先按user_id排,user_id相同的再按status排,status也相同的再按create_time排。所以当查询条件是user_id = 102345 AND status = 2时,MySQL可以直接在B+树里定位到这个精确的起点,然后沿着create_time的有序链表往后扫。因为索引本身就是按create_time升序排列的,而查询要的是DESC,MySQL会从起点往后扫描到满足时间范围的最后一条记录,再倒序返回,代价非常小。

2、为什么原来的三个单列索引不行?因为每个单列索引只能帮你过滤掉一部分数据。idx_user_id能快速找到这个用户的所有订单,大概8万条,但这8万条里还要再过滤status = 2和create_time的条件,而且最后还要手动排序。三个索引如果走Index Merge,MySQL需要分别扫描三个索引拿到结果集,再做交集运算,这个过程比直接走联合索引慢得多,尤其是在LIMIT 20这种需要快速返回前几条的场景下,联合索引的优势是碾压级的。

3、还有一个很多人容易忽略的细节:索引字段的顺序不是随便排的。我总结了一个口诀——等值在前,范围在后,排序字段紧跟范围条件。这条查询里,user_id和status都是等值,必须放最前面;create_time是范围,放最后;同时因为要按create_time排序,所以它必须出现在索引的最后一位,这样排序才能利用索引的有序性,否则又会出现Using filesort。

4、关于排序方向,MySQL 8.0开始支持显式指定索引的升降序了。如果你用的是8.0以上的版本,可以这样建索引:

sql

ALTER TABLE t_order ADD INDEX idx_user_status_time_desc (user_id, status, create_time DESC);

这样一来,ORDER BY create_time DESC就完全命中索引的排序方向,连倒序扫描的开销都省了。如果是5.7及以下版本,虽然不支持显式降序,但实际效果差别不大,因为MySQL会在索引扫描时做一个小的方向调整,代价可以忽略。

四、实战对比:不同查询场景下的索引策略选择

光说理论不够,我在测试环境里用同样的1200万条数据,模拟了几种常见的查询场景,把不同索引方案的效果做了对比。

1、场景一:只按用户ID查,不带状态和时间

sql

SELECT * FROM t_order WHERE user_id = 102345 LIMIT 20;

联合索引(user_id, status, create_time)依然能用,因为最左匹配只要求从最左边开始连续使用,用到user_id就够了。扫描行数大约200行,耗时0.01秒。

2、场景二:只按状态查,不带用户ID

sql

SELECT * FROM t_order WHERE status = 2 LIMIT 20;

这时候联合索引就完全用不上了,因为跳过了最左边的user_id。如果这种查询很频繁,就需要单独给status建一个索引。我加了INDEX idx_status (status)之后,扫描行数从1200万降到了380万,耗时0.18秒。虽然不如联合索引快,但比全表扫描好太多了。

3、场景三:只按时间范围查

sql

SELECT * FROM t_order WHERE create_time >= '2025-06-01' LIMIT 20;

同样的道理,联合索引用不上。单独加create_time索引后,扫描行数降到15万,耗时0.09秒。

4、我把几种方案的对比数据整理成了表格:

索引方案 场景一耗时 场景二耗时 场景三耗时 索引总数 写入开销评估

三个单列索引 0.02s 0.18s 0.09s 3个 中等

仅联合索引 0.01s 12.3s 11.7s 1个 低

联合索引+单列status 0.01s 0.15s 11.7s 2个 中低

联合索引+单列time 0.01s 12.3s 0.08s 2个 中低

联合索引+status+time 0.01s 0.15s 0.08s 3个 中等

从这张表能看出来,没有万能的方案。如果场景二和场景三的查询频率都很低,那就只保留联合索引,写入开销最小;如果场景三(按时间查)也很高频,那就联合索引加一个create_time单列索引,总共两个索引,读取和写入都能接受。索引设计本质上是一个权衡的艺术,你得根据实际的业务查询模式来做取舍,不能拍脑袋。

五、那些年我踩过的坑,每一个都是血泪教训

1、坑一:索引不是越多越好。我之前接手过一个项目,有张表建了11个索引,INSERT性能直接崩盘。每次插入一行数据,MySQL要同时维护11棵B+树,你想想那个开销有多恐怖。我的经验是,单表索引控制在5个以内比较安全,具体还得看读写比。如果是读多写少的表,可以稍微多一点;如果写入很频繁,每个索引都是负担。

2、坑二:SELECT *是性能的隐形杀手。回到最开始那条慢查询,如果把SELECT *改成只查需要的字段,比如SELECT id, order_no, total_amount, create_time,而且这四个字段恰好都在联合索引里,那MySQL就可以直接走"覆盖索引",连回表都省了。EXPLAIN的Extra列会显示Using index,性能还能再提升一个档次。很多人写SQL图省事,上来就SELECT *,在小数据量下没感觉,数据量一上来,回表的IO开销能把你拖死。

3、坑三:模糊查询的前置通配符会让索引直接报废。WHERE remark LIKE '%退款%'这种写法,B+树根本没法用,只能全表扫描。我见过有人建了索引,跑EXPLAIN发现没走索引,还以为是MySQL出bug了,其实就是查询条件里藏了一个%。如果业务确实需要这种模糊匹配,建议走Elasticsearch,别在MySQL里硬扛。

4、坑四:隐式类型转换是最隐蔽的索引杀手。有一次排查慢查询,发现user_id字段是BIGINT类型,但代码里传参的时候不小心传成了字符串'102345'。MySQL在比较的时候,会把索引列的值隐式转换成字符串,这相当于在索引上套了一个CAST()函数,索引直接失效。这种问题EXPLAIN看不出来,只能靠肉眼比对查询条件和字段类型。我后来养成了一个习惯:写SQL之前先看一眼字段类型,传参的时候严格匹配类型,这个小习惯帮我避免了好几次线上事故。

5、坑五:ORDER BY的字段如果不在索引里,LIMIT再小也救不了你。比如WHERE user_id = 102345 ORDER BY pay_time DESC LIMIT 20,如果索引是(user_id, create_time),pay_time不在索引里,MySQL还是得先把这个用户的所有订单找出来,再按pay_time排序,最后取前20条。如果这个用户有10万条订单,那前面99980条全是白查的。所以设计联合索引的时候,一定要把ORDER BY的字段考虑进去。

六、总结:一套可以复用的索引调优方法论

1、拿到慢查询,第一件事不是改SQL,而是先跑EXPLAIN。重点盯四个字段:type列看是不是全表扫描,key列看有没有走索引,rows列看扫描了多少行,Extra列看有没有filesort和Using temporary。这四个字段能帮你快速定位问题出在哪。

2、分析WHERE条件里的字段组合,按照"等值在前、范围在后、排序字段跟在范围后面"的原则设计联合索引。如果有多个等值条件,区分度高的放前面,比如user_id的区分度肯定比status高,所以user_id放status前面。

3、用EXPLAIN验证新索引的效果,重点对比扫描行数。如果从百万级降到千级甚至百级,基本就对了。如果降得不明显,回去重新审视字段顺序和索引覆盖。

4、检查SELECT的字段列表,能不用SELECT *就别用。尽量让查询走覆盖索引,Extra列显示Using index的时候,性能是最优的。

5、最后别忘了在测试环境跑一下写入性能测试。索引调优不能只看读,写的代价也得算进去。我一般会用sysbench跑一批并发插入,对比加索引前后的TPS变化,确保写入性能在可接受的范围内。

这次从47秒到0.03秒的经历,让我真正体会到了一句话:数据库调优不是靠感觉,是靠方法论。 索引策略看起来简单,但字段顺序、覆盖范围、读写权衡,每一步都有讲究。希望这篇文章能帮你少走几步弯路,下次遇到慢查询的时候,心里有底,手上有招。

💡注意:本文所介绍的软件及功能均基于公开信息整理,仅供用户参考。在使用任何软件时,请务必遵守相关法律法规及软件使用协议。同时,本文不涉及任何商业推广或引流行为,仅为用户提供一个了解和使用该工具的渠道。

你在生活中时遇到了哪些问题?你是如何解决的?欢迎在评论区分享你的经验和心得!

希望这篇文章能够满足您的需求,如果您有任何修改意见或需要进一步的帮助,请随时告诉我!

感谢各位支持,可以关注我的个人主页,找到你所需要的宝贝。

博文入口:https://blog.csdn.net/Start_mswin 复制到【浏览器】打开即可,宝贝入口:https://pan.quark.cn/s/b42958e1c3c0 宝贝:https://pan.quark.cn/s/1eb92d021d17

作者郑重声明,本文内容为本人原创文章,纯净无利益纠葛,如有不妥之处,请及时联系修改或删除。诚邀各位读者秉持理性态度交流,共筑和谐讨论氛围~

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/10 8:19:54

B站视频转文字终极工具:如何用bili2text实现高效内容提取

B站视频转文字终极工具:如何用bili2text实现高效内容提取 【免费下载链接】bili2text Bilibili视频转文字,一步到位,输入链接即可使用 项目地址: https://gitcode.com/gh_mirrors/bi/bili2text 还在为B站视频内容整理而烦恼吗&#xf…

作者头像 李华
网站建设 2026/5/10 8:19:54

NCM解密技术深度解析:揭秘网易云音乐格式转换的终极解决方案

NCM解密技术深度解析:揭秘网易云音乐格式转换的终极解决方案 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM加密文件无法在其他设备播放而烦恼吗?ncmdump作为一款专业的NCM格式解密…

作者头像 李华
网站建设 2026/5/10 8:15:08

终极解决方案:QMCDecode快速解锁QQ音乐加密文件

终极解决方案:QMCDecode快速解锁QQ音乐加密文件 【免费下载链接】QMCDecode QQ音乐QMC格式转换为普通格式(qmcflac转flac,qmc0,qmc3转mp3, mflac,mflac0等转flac),仅支持macOS,可自动识别到QQ音乐下载目录,默认转换结果…

作者头像 李华
网站建设 2026/5/10 8:15:02

无障碍测试的兴起:为残障人士构建友好数字世界

一、无障碍测试的时代背景与行业价值在数字化浪潮席卷全球的当下,软件产品已成为社会运行和个人生活的核心基础设施。据世界卫生组织统计,全球约有10亿人存在不同形式的残疾,而随着人口老龄化加剧,我国60岁及以上老年人口已超2.96…

作者头像 李华
网站建设 2026/5/10 8:14:00

ncmdump开源工具:解锁数字音乐资产自由的技术解决方案

ncmdump开源工具:解锁数字音乐资产自由的技术解决方案 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 在数字音乐生态中,技术赋能让音乐收藏真正属于用户自己。ncmdump作为一款开源解密工具,通过创…

作者头像 李华