数据库又慢了。后台报警刷屏,接口超时,前端白转圈,老板在群里问“怎么回事”,你打开终端,脑子一片空白——这种情况干过线上运维的人都不陌生。数据库慢是一个现象,不是结论,你不可能听它说“我慢”就去调参数、加机器、重启实例,那样大概率越弄越糟。真正要做的是,在最短时间内判断出慢在哪一层:是某条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 |
| 写入偶发变慢 | 磁盘IO | iostat -x 1 |
| 应用超时/数据库正常 | 网络层/连接池 | ping + 连接池指标 |
| 固定时间点变慢 | 定时任务/备份 | 查看crontab与备份窗口 |
还有一个很容易被忽略的问题:主从架构下,有时候主库正常,从库的查询很慢。这种情况除了看从库的硬件指标,还要看从库的复制延迟。主库写入量大、大事务提交,或者从库磁盘慢,都可能导致复制追不上,从库上的报表查询就会读到一份并不完整的旧数据,而且特别慢。排查的时候看一眼SHOW SLAVE STATUS里的Seconds_Behind_Master,经常能直接找到答案。
另外提一嘴我自己常用的一个小习惯:排查数据库问题的时候全程开着终端记录,每一步操作和结果都留下日志。不是每次都能一次定位,回头翻记录的时候,经常能发现第一次忽略的线索。这也是为什么我推荐你固定一套排查顺序——只有顺序固定了,记录才有可比性,排查才不会像无头苍蝇。
数据库慢这个问题,说到底是个“系统性”问题:一条SQL慢可能是索引,全都慢可能是连接或资源,突然卡住可能是锁,定时变慢可能是任务冲突。你不需要成为某个领域的专家才能上手,你只需要有一套固定的排查顺序,然后按顺序一步一步确认。我这个五站式排查法在多次线上实战里都帮我最快锁定了问题,希望对你也有帮助。最后再提醒一句:永远不要在慌乱中重启数据库,也不要贸然改全局参数——先看完这五个地方,绝大多数问题都藏不住。