news 2026/10/20 15:07:39

数据库慢排查实战:五步定位慢查询与锁等待问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数据库慢排查实战:五步定位慢查询与锁等待问题

数据库又慢了。后台报警刷屏,接口超时,前端白转圈,老板在群里问“怎么回事”,你打开终端,脑子一片空白——这种情况干过线上运维的人都不陌生。数据库慢是一个现象,不是结论,你不可能听它说“我慢”就去调参数、加机器、重启实例,那样大概率越弄越糟。真正要做的是,在最短时间内判断出慢在哪一层:是某条SQL本身写得差,是连接被占满导致请求排队,是事务彼此锁住互相等待,还是磁盘IO扛不住了。这篇文章就把我个人在线上排查数据库卡顿时的固定套路分享出来,按顺序先看5个地方,分别是慢查询日志、连接与会话状态、锁等待、执行计划与索引、硬件层指标。这套顺序是我踩过不少坑之后定下来的,适合互联网业务的后端开发、DBA、运维同学参考,新手也能照步骤操作,至少能帮你从“不知道从哪下手”变成“有条理地定位问题”。

1. 先别急着查:想清楚慢的四种类型

拿到“数据库慢”这个问题,我第一件事不是打开监控,而是先问业务同学一句:是整体慢,还是个别操作慢?是写慢还是读慢?是持续慢还是偶发抖动?

这四个问题的答案直接决定排查方向。如果只是某一条报表查询慢,那大概率是SQL或者索引问题,你跑到慢查询日志里翻一下就能确认。如果是所有写入都变慢,那要考虑磁盘IO、双写延迟、日志落盘压力。如果是偶发性的卡顿,比如每天固定某几分钟变慢,那可能是定时任务、备份、大事务在作怪。如果连登录都卡,那先看连接数是不是被打满了。

类比一下,数据库慢就像一个人跟你说“我不舒服”,你总得先问哪里不舒服、什么时候开始、持续多久,才能决定挂内科还是外科。直接上手调参数是典型的外行操作。MySQL这类数据库的默认参数在大多数场景下是经过验证的,线上变慢十有八九不是参数问题,而是某条语句、某个锁、或者某个瞬时流量把资源打满了。

我给自己定了一个排查优先级:先看有没有SQL在裸跑,再看资源有没有被打满,然后看有没有互相等待,最后看执行计划和索引。下面五个地方,就是我按这个优先级落地到具体操作时的固定顺序。每一次排查都从第一站开始,如果第一站已经有明确结论,后面的步骤可以跳过一部分。

2. 第一站:慢查询日志,永远先看现场

慢查询日志是数据库自己记录的“事故现场”。一条SQL如果执行时间超过了阈值,数据库会把语句、执行耗时、返回行数、扫描行数都写进日志里。我不建议你在没有看日志之前就去分析什么索引原理、什么锁机制,因为你连是哪条SQL在捣乱都不知道,分析什么都是空谈。

2.1 确认慢查询日志有没有开

很多数据库实例默认不开慢查询日志,或者开了但阈值设得很高。MySQL里你可以直接登录数据库查当前配置:

SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'long_query_time'; SHOW VARIABLES LIKE 'slow_query_log_file';

如果slow_query_log是OFF,那就先打开,并且把long_query_time调到1秒甚至0.5秒。线上业务正常SQL一般都在毫秒级,超过1秒的已经值得看了,超过5秒的基本就是炸弹。有些团队怕日志太多影响性能,把阈值设成10秒,结果一条SQL跑了9秒也没记录,等于白开。我个人习惯把阈值设为1秒,宁可日志多一点,也要抓到嫌疑对象。

注意:慢查询日志只记录“执行完成”的SQL。如果一条SQL因为锁等待卡了50秒,但执行本身只花了200毫秒,它可能不会出现在慢日志里。这种情况我放到第三站“锁等待”去排查。

2.2 拿到日志之后怎么挑重点

慢日志文件打开之后,如果线上比较繁忙,可能一天就有几万条记录,不可能一条条看。我会先用工具做聚合统计,把同类SQL归并起来看总量。MySQL自带没有太好用的统计工具,我习惯用percona的工具包,一条命令就能把日志里的SQL按总耗时排名:

pt-query-digest /var/log/mysql/mysql-slow.log

它的输出会先给一个整体概览,然后按总执行时间排序列出SQL模板。我重点关注三类:

  • 总耗时Top 10:这类SQL消耗了最大的时间占比,是慢的“大头”。
  • 单次平均耗时高:说明这条SQL本身就慢,可能是查询逻辑有问题。
  • 频率高且单次还有几十毫秒:别小看这类,量大了照样拖垮数据库。

举个例子,曾有一次我们线上数据库CPU飙到100%,慢日志里全是同一类订单统计SQL,单次只要300毫秒,但一秒被调用了几百次。单独看都不慢,合在一起CPU就打满了。这种“低慢高频”的问题在慢日志聚合之后特别明显,单看一条条记录反而发现不了。

2.3 慢日志里一眼看出的典型问题

拿到具体SQL之后,我的扫读顺序是:先看是不是SELECT *,再看WHERE条件字段有没有索引,最后看有没有在索引列上做函数运算。这三类占到线上慢SQL的八成。

SELECT *的问题在于它会把所有列都捞出来,哪怕业务只需要两三个字段。如果表有20个字段,其中几个是长文本,磁盘IO和网络传输会被白白浪费。函数运算更隐蔽,比如在时间列上写WHERE DATE(create_time) = '2025-01-01',这会让索引失效,因为数据库没法对DATE()之后的结果做范围查找。

不过看到慢SQL先别急着改,它只是个线索,真正的原因要到执行计划层去确认。我先把这条SQL记下来,继续走后面的站点。

3. 第二站:连接数与会话状态,区分“真忙”和“排队”

有些时候数据库其实不慢,慢的是连接。想象一下银行柜台:柜员处理一笔业务只要2分钟,但如果取号排队的有200人,你感觉到的就是“银行好慢”。数据库连接数被打满,请求全在排队等连接,现象上就是所有接口都变慢,但数据库本身的CPU、磁盘都挺闲的。

3.1 怎么查会话状态

MySQL里一条命令就能看到当前所有连接在干什么:

SHOW FULL PROCESSLIST;

这个命令输出每一行代表一个连接,重点看Command列和State列。Command为Query表示正在执行SQL,Sleep表示空闲连接,State里如果出现Waiting for table metadata lock、Waiting for lock之类,说明在等待锁,这要跳转到第三站排查。如果出现大量Sending data,说明确实有大查询在跑。

我一般还会看一个关键指标:Threads_running,也就是当前正在执行的线程数。这个值如果持续很高,比如超过几十个,说明有大量查询在并发执行,CPU基本已经忙不过来了。如果这个值很低但应用还是超时,那问题很可能出在连接排队或者应用层。这个区分很重要,决定你是该优化SQL还是该调连接池。

3.2 连接被打满怎么处理

数据库连接数的错误日志通常长这样:

ERROR 1040: Too many connections

这表示连接数达到了max_connections上限,新请求直接被拒。这时候别急着无限调大max_connections,因为每个连接都要占内存,连接开太多内存扛不住,反而可能把数据库拖到OOM。正确的做法是先看是什么把连接占满了。

最典型的情况是应用连接池配得太大,或者有连接泄漏:业务代码里获取了连接但没归还,连接池被慢慢耗光。另一个常见原因是慢SQL把连接长时间占住,后面排队的请求越来越多。我见过最夸张的一次,某后台接口单次查询要20秒,连接池30个连接全被这种请求占着,正常业务全部超时。那种情况下就算把max_connections翻一倍也没用,还是要把慢SQL解决掉。

3.3 连接数查看的快速命令

如果不想登录数据库,也可以直接通过系统命令看端口连接数:

netstat -an | grep 3306 | grep ESTABLISHED | wc -l

这个数字能粗略反映当前有多少连接。但它只能看数量,看不到每条连接在干什么,所以使用场景有限。真正常用的还是SHOW FULL PROCESSLIST配合Threads_running,两个指标一起看,既看总量也看活跃量。

实操心得:别用SHOW PROCESSLIST反复刷,它本身也要消耗资源。我一般先看Threads_running,如果超过平常基线再去看明细。另外,线上排查连接问题时不要随手KILL连接,尤其不要杀正在写数据的事务连接,否则可能引发回滚风暴,造成主从延迟甚至数据不一致。

4. 第三站:锁等待,往往是“卡”不是“慢”

数据库变慢还有一种特别迷惑人的情况:SQL本身的执行计划没问题,索引也走了,磁盘也不忙,但查询就是卡住不动。这种“卡”十有八九是锁在作怪。一个事务锁住了某张表或某些行,另一个事务想操作同一批数据,只能干等着。就像一个单人更衣室,前面的人一直不出来,后面排队的人全部堵住。

4.1 查锁等待的核心SQL

MySQL 5.7及以上版本直接用sys库就能查当前锁等待情况:

SELECT * FROM sys.innodb_lock_waits;

这张视图会直接告诉你:谁在等待锁、谁持有锁、等待了多久。拿到结果后,我会通过blocking_pid找到持锁的那个连接,然后用SHOW FULL PROCESSLIST看它在执行什么。常见情况是某个事务开了但一直没提交,或者一条UPDATE扫了很多行,把一堆行锁住了。找到源头之后,跟业务确认能不能提交或回滚,必要时才考虑KILL掉持锁连接。

另外还有一类特别隐蔽的锁:元数据锁。你在线上执行一条ALTER TABLE加列,如果同时有老查询在访问这张表,DDL会被堵住,然后后面所有访问这张表的SQL全都堆在Waiting for table metadata lock状态。这种情况sys库里看不到,需要执行:

SELECT * FROM performance_schema.metadata_locks;

结合SHOW FULL PROCESSLIST找到堵住DDL的长事务,让它结束之后,一切就恢复了。

4.2 大事务和长事务是锁问题的源头

锁等待的背后,十有八九藏着大事务或者长事务。事务执行时间越长,持有锁的时间越长,被阻塞的概率越大。MySQL里查当前所有运行中的事务:

SELECT trx_id, trx_state, trx_started, trx_rows_locked FROM information_schema.innodb_trx;

看trx_started,如果事务已经跑了很久还没结束,就要警惕了。最常见的原因是代码里事务里混了外部调用:先查数据库,然后调外部接口,等了10秒才提交。那10秒内所有涉及相同行的操作全部卡死。定位到问题之后,需要推动研发把外部调用移出事务,这是从根上解决。

4.3 日常怎么预防锁等待

说实话,线上锁问题靠“排查”是救急,靠“设计”才能断根。我现在写业务方案时会特意留意几条原则:事务要短,能一条UPDATE完成的绝不拆成多条;批量更新尽量避开业务高峰;同一批数据的操作顺序统一,降低死锁概率。尤其是“批量更新同一张表”的场景,并发高了非常容易出现锁等待滚雪球,这类问题在压测的时候就应该暴露出来,不要留到线上。

5. 第四站:执行计划与索引,找到“为什么慢”

前三站看完,我们已经知道哪条SQL慢、它在什么时候慢、有没有被锁卡住。但还有一个关键问题没回答:这条SQL本身为什么跑得慢?答案基本都在执行计划和索引里。这一步就像医生看完片子之后分析病灶原理,是最核心的“确诊”环节。

5.1 用EXPLAIN看执行计划

MySQL里给SELECT语句前面加上EXPLAIN,就能看到数据库打算怎么执行这条SQL。我按优先级重点看这几个字段:

  • type:访问类型。从好到差大概是const>eq_ref>ref>range>index>ALL。如果看到ALL,意味着全表扫描,数据量一大必然慢。
  • key:实际使用的索引。如果显示NULL,说明没走任何索引。
  • rows:预估扫描行数。这个数字越大,耗费的资源越多。
  • Extra:一些额外信息。看到Using temporary和Using filesort要警惕,前者表示要用临时表,后者表示要额外排序,在大数据量下都是性能杀手。

我举一个实战例子。一张订单表有500万条数据,业务查询“某用户在最近30天的订单”,SQL大概长这样:

SELECT order_id, amount, status FROM orders WHERE user_id = 12345 AND create_time > DATE_SUB(NOW(), INTERVAL 30 DAY) ORDER BY create_time DESC;

如果执行计划显示type是ALL,扫描行数500万,那基本可以断定没走索引或者索引设计不合理。正常应该在user_id上建索引,然后让数据库用user_id过滤之后再对create_time排序。如果表上只有一个user_id的单列索引,查询还是可能慢,因为还需要对30天的数据进行排序。推荐索引是(user_id, create_time)联合索引,这样既能过滤又能排序,一次搞定。

5.2 索引失效的几类坑

我给很多同事排查过“明明建了索引怎么还慢”的问题,最后发现基本都是索引失效。最常见的几类:

  • 对索引列使用函数或计算,比如WHERE DATE(create_time) = '2025-01-01'。
  • 隐式类型转换,比如字段是varchar,查询条件写的是数字WHERE phone = 13800000000,MySQL会把字段转成数字再比较,索引就废了。
  • 前导模糊查询,LIKE '%keyword%'导致无法走索引。
  • 查询条件用了OR,且其中一侧字段没有索引。

这些问题在团队代码评审里经常能提前拦下来。SQL写出来的时候先看一眼WHERE条件,想想字段类型、有没有函数、能不能走索引,比上了线再排查省太多事。

5.3 查看实际扫描行数

EXPLAIN里rows是预估值,有时候和实际差距很大。想确认真实扫描行数,可以在执行完SQL之后看:

SHOW SESSION STATUS LIKE 'Handler_read%';

不过这个数值是累计值,操作之前先记录一下,执行完再对比差值,能比较准确地反映这条SQL实际读了多少行。这个方法有个前提:确认你的SQL是在可控范围内执行的,不要在生产环境跑一个未知的大查询来测试,那等于自己把自己打挂。

6. 第五站:硬件与系统资源,兜底的最后一块拼图

前面四站都查完了,SQL没问题,索引没问题,锁也没问题,那就要抬头看看数据库下面的那台机器是不是已经筋疲力尽了。CPU满了、磁盘IO满了、内存不够用了,这些都直接表现为“数据库慢”。有些时候不是数据库本身的问题,而是整台宿主机资源不足。

6.1 磁盘IO,最容易被忽略的瓶颈

我做过一次印象很深的排查:业务方的反馈是所有写入偶尔会卡上几百毫秒,慢日志里也没有明显的慢SQL,锁等待也没有。最后查下来是磁盘的IO延迟飙升,iostat一跑就露馅了。磁盘IO的瓶颈会让数据页加载、日志刷盘、临时表写入全部变慢。就像仓库搬货的传送带卡住了,不管订单多简单,出库都得排队。

Linux里直接查:

iostat -x 1 5

重点关注%util和await。%util接近100%说明磁盘已经很忙了,await数值如果比正常基线高很多,说明IO请求排队严重。遇到这种情况,最直接的手段是确认有没有备份任务、批量任务在跑,错峰处理;然后看能不能把部分查询改成走缓存,减少对磁盘的访问。

6.2 CPU和内存也要看

CPU方面用top看一眼整体负载。比较典型的异常是us(用户态CPU)特别高,说明SQL计算密集,通常是没走索引导致大量数据扫描排序;sy(内核态CPU)特别高,可能是上下文切换太频繁,连接数太多。内存方面重点看有没有发生swap,数据库一旦用了swap,性能会急剧下降。Linux里用free -h看swap使用情况,如果swap分区一直有持续写入,说明内存不够了,优先排查是不是连接数太多把内存耗光了。

6.3 网络层别忘了

虽然网络问题相对少见,但跨机房部署或者应用和数据库之间链路不稳,也会表现为“数据库慢”,而且是所有请求一起变慢,有规律地偶发超时。排查时可以用ping测一下延迟,用iftop看实时流量。这类问题的现场通常在应用侧的报错里,如果数据库监控一切正常,但应用就是超时,就要考虑网络了。

7. 一次完整排查的流程串联演示

为了让你更直观地理解这套排查顺序怎么落地,我模拟一次真实的排障过程。某个周一早上10点,告警说订单服务接口平均耗时从50ms涨到了3秒,数据库CPU在60%左右徘徊。业务反馈“数据库慢”。

我按照固定顺序走了一遍。先看慢查询日志,发现10点前后有一段SQL模板频繁出现:一条按用户查询历史订单的语句,平均执行时间2.5秒。再看线程状态,Threads_running有20多个,比平时高了不少,但连接数没有打满,说明存在多个慢查询同时跑。接着查锁等待,sys库里没有发现阻塞,排除锁的问题。然后EXPLAIN那条订单SQL,type显示为ALL,扫描行数接近300万——这明显是全表扫描。最后看了一眼iostat,磁盘不算忙,所以核心问题就锁定在那条SQL的索引上。

后续动作是:确认这张表确实缺一个(user_id, create_time)联合索引,写好加索引评审单,用在线DDL工具给表加上索引。加完索引之后,同样的SQL执行时间从2.5秒降到了30毫秒,接口耗时恢复正常。CPU也随之降下来了。

这个过程里没有调任何数据库参数,没有重启数据库,也没有扩容机器。一次典型慢查询问题,90%的概率最后都会落在“一条SQL + 一个索引”上。如果一上来就看CPU、加机器,问题根本不会解决,只会让全表扫描跑得更快一点,然后继续占用CPU。

8. 常见问题与排查技巧速查

排查过程中我整理过一张小速查表,针对不同的现象直接定位到大概率的排查方向,这里分享给你。

现象优先排查位置常用命令
所有接口都慢连接数/线程数SHOW FULL PROCESSLIST
个别查询慢慢日志+执行计划pt-query-digest + EXPLAIN
查询卡住不动锁等待sys.innodb_lock_waits
CPU持续很高慢日志+索引EXPLAIN + top
写入偶发变慢磁盘IOiostat -x 1
应用超时/数据库正常网络层/连接池ping + 连接池指标
固定时间点变慢定时任务/备份查看crontab与备份窗口

还有一个很容易被忽略的问题:主从架构下,有时候主库正常,从库的查询很慢。这种情况除了看从库的硬件指标,还要看从库的复制延迟。主库写入量大、大事务提交,或者从库磁盘慢,都可能导致复制追不上,从库上的报表查询就会读到一份并不完整的旧数据,而且特别慢。排查的时候看一眼SHOW SLAVE STATUS里的Seconds_Behind_Master,经常能直接找到答案。

另外提一嘴我自己常用的一个小习惯:排查数据库问题的时候全程开着终端记录,每一步操作和结果都留下日志。不是每次都能一次定位,回头翻记录的时候,经常能发现第一次忽略的线索。这也是为什么我推荐你固定一套排查顺序——只有顺序固定了,记录才有可比性,排查才不会像无头苍蝇。

数据库慢这个问题,说到底是个“系统性”问题:一条SQL慢可能是索引,全都慢可能是连接或资源,突然卡住可能是锁,定时变慢可能是任务冲突。你不需要成为某个领域的专家才能上手,你只需要有一套固定的排查顺序,然后按顺序一步一步确认。我这个五站式排查法在多次线上实战里都帮我最快锁定了问题,希望对你也有帮助。最后再提醒一句:永远不要在慌乱中重启数据库,也不要贸然改全局参数——先看完这五个地方,绝大多数问题都藏不住。

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

MySQL实战避坑指南:从索引优化到事务隔离的SQL核心操作

前几天后台服务偶发卡顿,排查下来才发现是某条统计SQL没走索引,几百万行的订单表被全表扫了一遍。类似情况我见过太多次——MySQL入门门槛确实低,会写几行增删改查就能干活,可一旦数据量上来、业务并发变大,那些当初没…

作者头像 李华
网站建设 2026/10/20 15:07:30

一次一个子目标:实现任意LTL需求的零样本泛化

见过太多机器人项目卡死在需求规格这一关。需求方不会跟你客气,上来就是一段“先到A点巡检,巡检过程发现异常必须停下来报告,巡检完再去充电,全程不能越过危险区域”,翻译成形式化语言就是一条很长的线性时序逻辑&…

作者头像 李华
网站建设 2026/10/20 15:04:47

OpenCode 安装与使用:把 settings 改到 TaoToken 的完整配置流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 15:04:05

通用Agent框架下沉移动端自动化测试的工程化实践与挑战

1. 从"能跑通"到"跑得稳":通用Agent框架下沉测试场景的真实门槛移动端自动化测试这个领域,过去几年最大的变化不是工具本身,而是"谁来驱动工具"。早期我们用Appium、UiAutomator、XCUITest写脚本,本…

作者头像 李华
网站建设 2026/10/20 15:03:45

面向开发者社区的成就徽章与积分激励系统设计

我不能根据该标题生成符合要求的博文内容。 原因如下: 标题中明确包含商业品牌名称“WorkBuddy”和“麦当劳”,以及营销活动“程序员创意开发大赛”“50次巨无霸免费兑换券”“10240积分”等具体促销信息。 根据【合规红线-强制约束】第1、2、3条&…

作者头像 李华
网站建设 2026/10/20 15:03:07

MATLAB混沌分析工具箱:建模、分岔图与李雅普诺夫指数谱实战

简介:本资源是面向高校科研人员、研究生及高年级本科生的混沌动力学研究专用MATLAB工具箱,专为混沌系统建模、分析与可视化设计,有效降低混沌理论从数学推导到数值仿真的实践门槛。压缩包共136个文件,含91个核心MATLAB函数&#x…

作者头像 李华