1. 先弄清楚“数据库变慢”到底慢在哪一环?
先说个很真实的场景。半夜两点收到告警,说数据库响应时间从 20ms 飙到了 800ms,链路监控上整个接口的耗时长了一大截。这个时候如果直接冲到服务器上看 CPU、看内存、看磁盘,大概率会被一堆指标绕晕。因为“数据库慢”这四个字本身就是个黑盒,你看到的只是现象,真正的瓶颈可能藏在好几层中间件后面。
我干了这么些年数据库运维,最深的体会是:排查性能问题,第一步永远是定位“慢在哪个环节”,而不是急着看某个指标。
1.1 五种最常见的线上表现
线上数据库变慢,通常逃不出下面五种表现:
- 接口响应时间变长:这是业务方最先感知到的,特征是整体超时率上升,但数据库 CPU 未必高。
- 数据库 CPU/IO 打满:监控面板上某个单点指标跑满,内核日志里大量 task 阻塞。
- 并发连接数暴涨:连接数超过阈值后,新连接进不来,表现为数据库“拒绝连接”。
- 主从延迟暴涨:主库没事,从库跟不上,读多写少的业务会大量命中从库然后超时。
- 特定 SQL 突然变慢:原来执行只要 10ms 的 SQL 变成 10s,页面上某些功能点开就卡。
这五种表现对应的排查方向完全不一样。如果你连“慢”属于哪一类都没有判断清楚,就开始优化索引、调参数,很容易白忙活。
1.2 我自己的排查顺序与记录习惯
我个人有一套固定的打法,碰到数据库慢,先看这五个地方,顺序基本固定:
- 第一看慢查询日志,找最刺眼的 SQL;
- 第二看锁等待,看有没有堵车;
- 第三看连接数与会话堆积;
- 第四看索引和统计信息是否失效;
- 第五看系统资源和主从延迟。
这套顺序不是随便定的,它的逻辑是“从业务影响面最大的点往底层影响面最小的点”推。慢查询直接影响用户体验,锁等待和连接堆积影响系统可用性,索引和统计信息影响的是单条 SQL 的效率,而系统资源通常只是前面问题的“果”而不是“因”。
另外我强烈建议养成一个习惯:每次排查完,把时间、现象、根因、处理手段、持续时间记在一个小本本上。别嫌麻烦。数据库性能问题的特点是“同样的表象,完全不同的病因”,你上个月遇到的那个高 CPU 事件和这个月的高 CPU 事件,根因可能是两个方向。记录多了之后,你会发现自己判断问题越来越快。
2. 第一个必看位置:慢查询日志里的“抄底SQL”
慢查询日志是数据库性能排查的第一站,没有之一。它相当于飞机的黑匣子,记录着每一个执行时间超过阈值的 SQL 语句。线上数据库卡,大概率有某条 SQL 在拖后腿。
2.1 慢查询日志怎么开、参数怎么设才合理
MySQL 里和慢查询相关的参数主要有三个:
-- 查看当前慢查询配置 SHOW VARIABLES LIKE 'slow_query_log%'; SHOW VARIABLES LIKE 'long_query_time'; SHOW VARIABLES LIKE 'log_queries_not_using_indexes';slow_query_log:是否开启慢查询日志,线上一定要设为 ON。long_query_time:阈值,单位秒。默认是 10 秒,这个值在实践中太宽松了。对于一个正常业务系统,超过 1 秒的 SQL 就已经值得关注了。我一般建议从小流量业务开始设 1,核心交易库可以更严格,设 0.5。log_queries_not_using_indexes:记录所有不走索引的 SQL。这个参数很有价值,很多慢 SQL 在变成慢 SQL 之前,最早是从“没走索引”开始的。
动态修改命令是:
SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1; SET GLOBAL log_queries_not_using_indexes = 'ON';注意,long_query_time修改之后,已经存在的连接不会立即生效,不影响新连接。如果你用的是云数据库,很多云厂商默认已经开启了慢查询,只是阈值可能比较大,记得手动调小。
2.2 定位到 SQL 之后,我每次必做的三件事
找到慢 SQL 列表只是开始,真正的工作在拿到具体语句之后:
第一件事:看执行计划。
EXPLAIN SELECT * FROM order WHERE user_id = 123 AND create_time > '2025-01-01';我会重点看type字段、key字段和rows字段。type是ALL说明全表扫描,key是NULL说明没走到索引,rows如果比预期大几个数量级,说明估算严重不准。这三个字段基本能告诉你这条 SQL 为什么慢。
第二件事:把 SQL 的过滤条件拆开分析。
看 WHERE 后面的每个条件是否独立能走索引,看 ORDER BY、GROUP BY 是否触发了 filesort 或临时表。很多时候慢 SQL 不是单条件有问题,而是多个条件组合导致索引选择失败。
第三件事:回归到业务的真实访问模式。
这是我踩过坑之后学乖了的地方。一条 SQL 慢,有时候不是 SQL 本身写得差,而是业务方在拿一个 OLTP 库跑 OLAP 式查询,比如一次性查半年的流水做报表,或者用SELECT *拉几万行到应用层做内存计算。这种时候你在数据库层怎么优化都有上限,正确的做法是拉着业务方一起聊清楚需求边界,引导他们用离线数仓或者改变查询方式。
慢查询日志的分析,我建议不要一条条人肉看,线上慢 SQL 一多,人肉根本看不过来。可以定期做聚合,比如用pt-query-digest这类工具按 SQL 指纹聚合,把同类型的慢查询归并统计,一眼就能看出哪类模子的 SQL 出现频率最高、累计耗时最长。
提示:
pt-query-digest是 Percona Toolkit 里的一个工具,分析慢查询日志非常好用。它能按响应时间、执行次数、总耗时排序,直接输出 Top N 模板,省掉大量人工翻日志的时间。
3. 第二个必看位置:锁等待与阻塞链
如果慢查询日志里找不到特别离谱的 SQL,或者找到了但优化完之后问题依然在,那就要考虑锁等待了。锁这个东西很有意思,它造成的现象是“数据库整体变慢”,但你去看单条 SQL,每条单独执行都快得很。因为 SQL 本身不慢,慢的是它在排队等锁。
3.1 怎么看锁等待、怎么区分行锁表锁
MySQL InnoDB 引擎的行锁机制,简单理解就是:事务 A 锁住了某一行数据没提交,事务 B 想要修改同一行,就只能等。这个等待时间一长,业务上就表现为更新卡住、接口超时。
遇到卡顿,先问自己两个问题:
- 是行锁还是表锁?
- 是读阻塞还是写阻塞?
在 MySQL 8.0 里,可以直接查performance_schema中的锁等待信息:
SELECT * FROM performance_schema.data_lock_waits; SELECT * FROM performance_schema.data_locks;8.0 之前的版本没有这些表,但可以用SHOW ENGINE INNODB STATUS看事务列表,虽然可读性差一点,不过里面会打印出等待锁的 SQL 和持有锁的事务。
另外还有一个非常实用的表——sys.innodb_lock_waits:
SELECT * FROM sys.innodb_lock_waits;它会直接告诉你哪个事务在等哪个锁,以及这个锁的等待时间和被阻塞的 SQL 文本,排查起来比直接翻原始状态快太多。
3.2 真实案例:一条 UPDATE 卡死整张业务表的排查
我印象很深的一次事故,业务方报障说订单系统写库特别慢,下单接口超时率飙到 30% 以上。查慢查询没有明显慢 SQL,CPU 也很正常,但sys.innodb_lock_waits里显示有二十多个事务都在等同一把锁。
顺着锁等待信息找到持有锁的事务,发现是一张报表的定时任务在凌晨跑批,事务长时间未提交,导致后续所有对该表的写操作全部排队。最终定位到的原因就很搞笑:跑批任务里有一段逻辑把事务打开了,但没 commit 就退出了,连接也没释放,锁被一直攥在手里。
当时的处理步骤:
- 找到持锁的会话 ID;
- 和业务方确认不是正在执行的关键逻辑,确认可以打断;
- 执行
KILL杀掉持锁会话; - 让业务方修复跑批代码,补上
COMMIT或ROLLBACK。
因为每晚都可能重跑,这个坑如果不从代码层面堵住,第二天会准时复发。
锁等待这个问题的排查难点在于:它涉及的是多个事务之间的互动关系,单看任何一方都不完整。一个事务握着锁不撒手,另一个事务干着急,你得把整个“锁等待链路”串起来看,才会看到全貌。
4. 第三个必看位置:连接数暴涨与会话堆积
锁问题排查完之后,下一个要确认的是连接数。在数据库层面,有个很常见的“假慢”现象:数据库本身性能没毛病,但连接池满了,应用端拿到不到连接,超时,于是业务感知就是“数据库变慢”。
4.1 连接池参数、活跃连接数与线程跑满
先看几个核心指标:
Threads_connected:当前有多少连接;Threads_running:当前有多少连接正在执行语句;max_connections:最大允许连接数;Connection_errors_max_connections:有多少次因为连接数满而被拒绝的连接请求。
SHOW STATUS LIKE 'Threads_connected'; SHOW STATUS LIKE 'Threads_running'; SHOW VARIABLES LIKE 'max_connections';为什么会连接数暴涨?通常有三类原因:
- 应用连接池设置过大:比如一个 Java 应用配了 200 个连接,十个应用实例就是 2000 个连接,数据库
max_connections只有几百,瞬间打满。 - 应用侧连接泄漏:连接没正确归还到池子里,导致连接池被耗尽,应用层不断尝试新建连接,形成恶性循环。
- 某条 SQL 阻塞导致连接堆积:连接进来了,但事务一直不提交,新请求只能继续创建连接往库里挤,最终把数据库连接数压爆。
4.2 会话堆积的快速止血与根因处理
连接数暴涨这种场景,我的建议是一定要先止血,再聊根因。
止血操作分两步:
- 第一步,把应用方的连接池参数落到一个合理的范围。别想着一次性把所有应用实例的连接数都调上来,数据库承载的连接总数是有上限的。
- 第二步,把异常会话杀掉。注意,不是说所有堆积的会话都要杀,而是要识别哪些会话是真正的坏会话。判断方法很简单:
State在Sleep、且持续很长时间不释放、事务没有提交的,多半是异常会话。
-- 查看异常会话 SELECT id, user, host, db, command, time, state, info FROM information_schema.processlist WHERE command = 'Sleep' AND time > 100;但这种操作一定要谨慎。先确认这些会话背后没有正在运行的关键业务,再确认是不是应用连接池里健康保活的连接。保活的空闲连接是正常的,不代表有问题。
等止血完成之后,再往回倒退,查应用日志、查慢查询、查锁等待,找到最初始的根因把问题彻底堵住。我见过很多团队只做了止血没管根因,第二天同一时间同一症状准时复发,那就很被动了。
5. 第四个必看位置:索引失效和执行计划偏移
聊完负载层面的问题,再往细了说。很多时候数据库变慢,是某条核心 SQL 的执行计划变了,从走索引变成全表扫描。这类问题最要命的地方在于,它不是突然出现的,而是慢慢恶化的。今天慢 100ms,明天慢 500ms,后天 2 秒,你没点对比数据,根本感知不到恶化趋势。
5.1 常见索引失效场景,别只盯着类型转换
一提到索引失效,很多人第一反应就是“查询字段上用了函数导致索引失效”。这个没错,但它只是众多坑里的一个。我实际开发中遇到比较多的还有这么几类:
第一类:隐式类型转换。
比如表里user_id是 bigint,查询条件却传了一个字符串'123'。MySQL 有隐式类型转换的规则,如果字段类型和参数类型不一致,有时能走到索引,有时走不到,完全看优化器怎么想。这种情况我建议在应用层就把类型转换做掉,不要指望数据库帮你判断。
-- 不推荐:user_id 为 bigint,传入字符串 SELECT * FROM user_order WHERE user_id = '123'; -- 推荐:直接传数值类型,或在SQL中明确类型 SELECT * FROM user_order WHERE user_id = 123;第二类:字符集不一致导致的无法索引关联。
这个坑隐蔽性极强。两张表关联字段都是 varchar,但表结构一个是 utf8mb4 一个是 latin1,关联时 MySQL 需要做字符集转换,结果就是索引失效。这种问题在 ER 图上完全看不出来,只有执行计划出来之后才能看到转换的痕迹。
第三类:前导模糊匹配。
LIKE '%关键词%'这种写法,因为没法利用 B+ 树的顺序特性,只能扫全表。如果业务真要靠中间匹配,可以评估全文索引或者上专门的搜索引擎,不要把压力全丢给数据库。
第四类:OR 条件混用。
一个 OR 条件里,如果某个分支不能走索引,整个查询可能退化成全表扫描。比如WHERE id = 123 OR status = 1,如果status上没有索引,优化器会放弃id上的索引去扫全表。这种情况可以改写为UNION ALL或者给两个条件都建上合适的索引。
5.2 统计信息过期导致的执行计划漂移
执行计划漂移是“数据库变慢”里最难排查的一类,因为它不是代码变了,不是数据量大规模涨了,也不是索引被删了,纯粹是优化器基于过期的统计信息做出了错误的判断。
MySQL 里通过ANALYZE TABLE来重新采集统计信息:
ANALYZE TABLE user_order;建议在数据量发生明显变化之后手动触发一次。依赖自动采样的话,部分长时间跑批后数据量翻倍的场景,统计信息跟不上变化的速度。
如何确认是执行计划漂移?方法很简单:
-- 执行一次,记录执行时间 SELECT * FROM user_order WHERE user_id = 123 AND create_time > '2025-01-01'; -- 再强制走某个索引对比 SELECT * FROM user_order FORCE INDEX (idx_user_time) WHERE user_id = 123 AND create_time > '2025-01-01';如果强制走索引之后速度大幅度提升,那基本可以确定是优化器选了错误的执行计划。接下来要看是不是统计信息过期,重新ANALYZE之后如果恢复正常,那就没问题。如果反复漂移,可能需要你手动干预,比如调整optimizer_switch,或者通过FORCE INDEX固定执行计划。
但这里有个经验之谈:不要一遇到慢 SQL 就想着固定执行计划。固定了这条路,如果将来数据量继续变大,或者索引结构变了,固定的计划一样会变成坏计划。要先理解为什么优化器做了这个选择,再从统计信息、索引设计、SQL 写法三个维度找平衡。
6. 第五个必看位置:系统资源与主从延迟
前面四处都排查完还没找到问题,或者问题已经定位到某一块了但对不上号,这个时候再去看系统层面:CPU、磁盘 IO、内存、网络、主从延迟。很多 DBA 新手习惯于先看资源再看日志,这种顺序其实容易误判。资源的异常往往是某个具体问题的表现,而不是起因,先看日志锁定目标,再回来看资源,会快很多。
6.1 CPU、磁盘IO、内存怎么联动分析
CPU 高,不一定需要扩容。我就遇到过 CPU 跑到 90% 以上,结果是某个 SQL 在做大量逻辑读,因为索引没命中,一次查询扫描了几千万行。这个时候加 CPU 一点用没有,把索引建上,CPU 自己就降下来了。
判断逻辑很简单:如果 CPU 高,但磁盘 IO 很低,多半是逻辑读问题,也就是 SQL 扫描行数太多或者计算量太大;如果 CPU 高且磁盘 IO 也很高,那是物理读问题,可能数据不在内存里,需要频繁读盘;如果 CPU 不高但磁盘 IO 打满,那一般是刷脏页或者大事务落盘导致的。
命令示例,以最常见的 Linux 系统为例:
# 看CPU和IO整体情况 top # 看磁盘IO是不是打满 iostat -x 3 # 看数据库进程的线程状态 pidstat -t -p <mysqld_pid> 2内存方面,不要只看剩余内存,要看 InnoDB Buffer Pool 的命中率。一个容易忽略的点是:内存明明还有很多,但 Buffer Pool 太小,导致热数据无法缓存,频繁发生磁盘读。这种场景加内存、调大innodb_buffer_pool_size往往立竿见影,如果命中率已经接近 99% 了,那再加意义也不大。
6.2 主从延迟的排查次序
主从延迟是一个特别容易被误当成“数据库变慢”的问题。现象上完全一致:应用查询读的是从库,从库延迟大,查询到的数据不新鲜,或者从库应用回放跟不上导致查询请求堆积。如果业务架构上有读写分离,排查顺序一定不要乱。
第一步,先看延迟时间是多长:
SHOW SLAVE STATUS;重点看Seconds_Behind_Master这个字段。如果这个值是 0,那说明从库回放正常,问题不在主从。
第二步,如果延迟很大,看协程/线程的状态。是单线程复制还是多线程复制?大事务是不是导致回放变慢了?这里有个很典型的问题:主库上执行了一小时的批量更新,从库只有一个 SQL 线程在回放,延迟就会逼近一小时。这种场景下,从库的 CPU 反而不会太高,因为它在慢慢处理一条大事务。
第三步,确认延迟持续的时间窗口和业务高峰是否吻合。如果延迟只在特定时间段出现,比如凌晨跑批时固定出现,那基本是跑批任务造成的。如果不是固定窗口,而是随机延迟,则需要检查网络延迟、从库机器性能、以及是否有其他慢查询在抢占从库资源。
主从延迟的根因千奇百怪,但排查的框架不外乎上面三步,剩下的就是往框架里填具体证据。
7. 几个实战下来容易忽略的点
最后分享几个排查中容易踩的坑,这些细节用钱都不一定能买到,基本都是靠一次次被线上故障教育出来的。
第一,别忽略“看起来不慢”的 SQL。
慢查询日志设置的阈值是 1 秒,但一条执行 900ms 的 SQL 如果每天被调用一百万次,哪怕它不进入慢查询统计,对整个系统的压力也一样巨大。排查性能问题,单看单条 SQL 的耗时是不够的,要看整体 QPS 和单条 SQL 耗时的乘积。高频次的中低耗时 SQL,优化后的收益往往比低频次的超慢 SQL 更大。
第二,事务隔离级别会影响锁行为。
很多人排查死锁和锁等待的时候,默认用的是 REPEATABLE READ,这个隔离级别下间隙锁(Gap Lock)的行为更复杂。如果你业务确实允许,且场景合适,切到 READ COMMITTED 可以减少不少锁冲突。但切换前一定要充分评估业务语义,不能只看性能。
第三,业务高峰期的自动运维操作要格外小心。
我遇到过好几次“数据库突然变慢”,最后定位原因是运维平台在高峰期自动执行了批量ANALYZE或者自动发起了全库逻辑备份,占了大量 IO。这种问题,从数据库日志看不出来,只能通过窗口期对比和运维审计日志来定位。
第四,连接数不等于并发度。
有很多数据库连接数显示几千,但真正活跃执行的Threads_running只有十几个。连接数多不丢人,可怕的是Threads_running长时间很高。高并发会话数有限的数据库,性能问题大概率出在执行计划或锁上,而不是连接池配置上。
第五,看数据增长趋势。
数据库今天慢,不代表今天才出问题。排查时把近七天甚至近三十天的数据量和慢查询日志趋势拉出来对比,很多问题其实在几天前就有苗头了。性能问题最好的处理时机,是在它产生线上影响之前。
数据库诊断这件事,越往后越觉得“功底”比“技巧”重要。技巧是那些让人眼前一亮的具体手段,功底则是知道什么时候该用哪个技巧、用完之后怎么验证、验证之后怎么防止复发。上面这五个检查点,是我个人这几年做线上问题处理时最先看的地方,说不上高深,但确实在一次次告警中帮我快速从“不知道哪里有问题”走到了“确认了问题在哪”。
如果你有自己的排查顺序和踩坑经历,强烈建议也梳理成一套自己固定的步骤。把排查流程规范下来,比每次遇到问题临时拍脑袋要靠谱得多,尤其在凌晨两点被叫起来的时候。