数据库慢查询的根因大多落在存储侧而非计算侧,SQL和索引只是表层的替罪羊。
同一个查询,在测试环境几十毫秒,上了生产就成了三秒,很多团队的第一反应是改写SQL,结果优化半天收效甚微,真正的问题往往藏在那块默默扛下所有压力的磁盘里,存储层要负责把数据从物理介质搬到内存,这个速度一旦跟不上,再漂亮的执行计划都得等着。
数据库慢查询排查:为什么根因常常不在计算侧
CPU和内存负责计算,存储负责搬运,慢查询日志里记录的时间,包含的是从请求发出到结果返回的完整链路,其中CPU真实计算的时间占比很小,大部分时间都消耗在等待数据上。
慢查询日志记录的不只是SQL自身开销
开启MySQL慢查询日志后,很多人只盯着query_time那一列,这里有个细节:query_time包含执行时间和等待时间,后者往往占比更高,用pt-query-digest分析慢日志,把相同模式的SQL聚合起来,再对比Rows_examined和Rows_sent,你会发现相当一部分慢查询扫描的行数是返回行数的几百倍,扫描这么多行,说明数据页需要频繁从磁盘加载,存储IO自然成为瓶颈。
某个订单查询每天跑几十万次,单次耗时从80毫秒涨到800毫秒,执行计划显示索引正常,SQL也没改过,后来排查发现,同一块云盘上还运行着另一个业务的日志写入任务,IO队列被堵死,查询全部卡在磁盘等待上,这就是典型的存储侧问题。
全表扫描的锅,不全在SQL写法
业内专家指出,MySQL优化器决定全表扫描,有时候是统计信息过期,有时候是存储成本计算失真,InnoDB的统计信息基于采样,当表数据量剧烈变化后未更新,优化器可能误判走索引的成本比全表扫描更高,这种情况你重写SQL也没用,跑一次ANALYZE TABLE往往就解决了。
换个角度,存储热数据如果长期无法全部放进缓冲池,每次查询都要走磁盘,InnoDB缓冲池命中率低到90%以下时,物理读的比例会明显上升,这时候加索引只能缩小每次读取的数据量,但磁盘寻道和传输的延迟仍是真实成本。
存储性能瓶颈如何定位:从等待事件到磁盘时序
与其猜,不如测,定位存储瓶颈有一套固定的操作路径,线上排查时按顺序执行,基本能锁定问题层级。
第一步看数据库等待事件
登录数据库后先执行SHOW ENGINE INNODB STATUS,重点查看I/O Thread状态和Pending writes数量,如果I/O线程长期处于waited状态,说明存储设备响应已接近上限,再看performance_schema里的events_waits_summary_global_by_event_name,把wait/io/file/innodb/innodb_data_file这类事件的总等待时间拉出来排个序,占比最高的就是存储层。
MySQL 5.7以上可以查sys.session视图,找到当前阻塞最久的会话,看它的wait_event字段,如果大量会话的等待事件是innodb_data_file_read或innodb_log_write,基本可以判定问题在存储侧。
第二步看磁盘工具输出的关键指标
数据库层判断完,再下沉到操作系统,执行iostat -dx 1观察%util、await和svctm。%util接近100%说明磁盘接近饱和,await持续超过20毫秒说明IO延迟偏高,在云数据库环境里还可以看控制台的IOPS和延迟监控曲线,确认是持续高位还是间歇性尖刺。
有个容易被忽略的点:SSD和HDD的%util意义不同,SSD的%util经常到100%但其实延迟很低,因为NVMe协议下队列深度很深,这时候应该重点看await和writeback的数量,而不是死盯着占用率。
一个典型场景:凌晨跑批与IO排队
整个架构很长一段时间没有变更,慢查询却每天定时出现,时间点卡在凌晨两点,正好是大数据部门跑批任务的时间段,数据库侧慢查询日志显示一批INSERT和UPDATE语句延迟升高,SQL本身非常轻量,每次只操作几行数据。
用iostat排查时发现,跑批任务瞬间打满磁盘带宽,数据库事务日志写入和脏页刷盘都在排队,提交延迟被拉长到几百毫秒,业务侧看到的是事务提交慢,实际上磁盘被批处理任务阻塞,解决办法不是改SQL,而是把跑批任务的数据源切换到只读副本,或者限制跑批的IO优先级。
| 排查动作 | 计算侧表现 | 存储侧表现 |
|---|---|---|
| 慢日志分析 | CPU time占比高 | IO wait占比高 |
| 执行计划 | 明显缺索引 | 索引正常但扫描行数多 |
| 数据库状态 | 线程运行中 | 线程等IO事件 |
| 操作系统指标 | CPU占用高 | await和%util偏高 |
慢查询优化实操:存储优先还是SQL优先
很多人学“数据库性能调优SQL”这门课,背了一堆优化口诀,碰到线上问题还是无从下手,核心原因是顺序搞反了,看到慢查询先改SQL,而不是先确认瓶颈到底在哪一层。
先回答一个问题:执行计划和实际耗时哪个可信
执行计划描述的是优化器的意图,实际耗时反映的是硬件的真实反馈,两者冲突时,以实际耗时为基准,用EXPLAIN ANALYZE(MySQL 8.0.18+)可以拿到每条算子真实执行时间和行数,如果某个节点显示actual time远高于estimated time,大概率是这一步触发了物理IO。
举个例子,一条关联查询走了索引,EXPLAIN显示只扫描几百行,但实际执行了1.5秒,用ANALYZE一看,嵌套循环里的内层查询每次都要回表查另一张表,而那张表的索引页恰好不在缓冲池里,优化方案是把相关表的热数据预热进缓存,或者调整read_ahead策略,而不是改写JOIN语法。
加索引无效时,看看缓冲池与冷热数据
行业共识认为,优化器选择全表扫描或索引失效,超过一半的情况与数据分布和缓冲池状态有关,你在测试环境验证索引效果良好,但生产环境的数据量是测试的几十倍,热数据分布完全不同,索引页和聚簇索引页频繁在内存中被淘汰,物理读不可避免。
这时可以调大innodb_buffer_pool_size
到物理内存的60%到70%,再把高频查询涉及的表用SELECT COUNT()强制预热一遍,实操中很多慢查询不需要动业务代码,重启实例后缓冲池冷启动,第一波请求全走磁盘,必然慢。
存储选型影响慢查询的几个隐藏参数
存储层不稳定,数据库参数调得再激进也没用,云盘和本地盘的延迟差异在随机读写场景下非常明显,本地NVMe盘随机读延迟在几十微秒量级,普通云盘却可能飙到几毫秒,差一个数量级,数据库的innodb_io_capacity和innodb_io_capacity_max参数要按实际存储能力设置,设置过高会导致刷脏过快,反而加重磁盘负担。
数据库响应慢是什么原因,排查顺序先把存储层翻个底朝天。await持续偏高时,检查是不是同时开启了备份任务、pt-table-checksum或者大查询,把这些任务错峰或者迁移到只读实例,慢查询数量会肉眼可见地下降,近年来不少团队的优化经验证明,存储侧的调整往往比SQL改写带来的收益更大。
关于数据库慢查询存储侧的常见问题
Q:慢查询日志里SQL执行时间长,一定是SQL语句本身的问题吗?
不确定,执行时间由CPU计算、内存访问、磁盘IO和网络传输共同构成,多数慢查询的时间主要耗在磁盘IO等待上,先通过performance_schema看等待事件占比,再决定改SQL还是调存储,顺序不能反。
Q:加了索引还是慢,下一步该检查什么?
检查索引列的顺序是否与查询条件匹配,确认统计信息是否最新,查看缓冲池命中率,如果三者都正常,重点排查是否存在存储热点,比如同一块磁盘上同时运行着备份、导出和其他业务的高IO任务。
Q:数据库响应慢是什么原因,哪些检查要第一时间做?
第一看当前等待事件,用SHOW ENGINE INNODB STATUS观察I/O线程状态;第二看磁盘层的await和%util指标;第三看慢查询日志Top N的等待时间分布,多数情况下,这三步就能在存储侧定位到问题根因。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/637847.html





