SQL 跑得慢?别慌,这里有份排查手册

线上一条 SQL 突然变慢,往往是开发与 DBA 最头疼的问题。本文不讲晦涩理论,直接梳理一套落地的排查路径:从快速定位问题 SQL,到读懂执行计划、揪出索引与锁的毛病,再到系统资源与配置调优。下次再遇慢查询,照着清单查一遍,多半能找到病根。

SQL 跑得慢?别慌,这里有份排查手册


这大概是每个后端开发都逃不掉的噩梦:大半夜正刷着手机,报警群里突然弹出一句,“接口超时了,数据库 CPU 100%”。心里咯噔一下,打开监控一看,又是几条熟悉的慢 SQL 在捣鬼。

干过几年活,踩坑无数之后,我慢慢攒出了一套排查慢 SQL 的思路。它不靠玄学,也不需要你把《高性能MySQL》倒背如流,只需要按“从外到内、从大到小”的次序一层层往下捋,问题基本藏不住。这份清单更偏向实战,MySQL 和 PostgreSQL 都会兼顾着说,原理相通。

1. 先别急着优化,搞清“它是什么慢”


第一步往往最容易被跳过:你到底在应付哪种慢?

  • 一直慢:只要执行这条 SQL 就慢。这种最好查,大概率是没索引或者 SQL 写得烂。
  • 时而快时而慢:有时候秒出,有时候卡住。这种最磨人,多半和缓存失效、锁竞争、统计信息不准有关。
  • 突然变慢:昨天好好的,今天挂了。十有八九是数据量暴增、表结构被人改过、索引被误删,或者统计信息过期导致执行计划跳变。

先把这个大方向判断清楚,能省下不少走弯路的时间。比如一直慢的 SQL 你非要去查锁,那不是瞎忙活吗。

2. 在慢日志里“捞”出肇事者

生产环境上百条查询在跑,怎么知道哪条是罪魁祸首?两个渠道:

第一,直接看“现场”。连上数据库看当前谁在跑,谁跑了很久。

  • MySQL 里跑 SHOW FULL PROCESSLIST;,或者查 information_schema.PROCESSLIST。看到状态是 Sending dataSorting resultCopying to tmp table 甚至 Waiting for lock 的,就要特别留意了。
  • PostgreSQL 里查 pg_stat_activity,把 state = 'active' 且排除自己会话的行捞出来。如果一堆会话都在等锁,先跳到锁排查那步。

第二,从慢日志里翻旧账。建议把慢查询阈值设到你能容忍的上限,比如 1 秒,然后把慢日志打开。MySQL 可以用 pt-query-digest 工具分析,它会帮你按总耗时、执行次数排序,谁拖后腿一目了然。PostgreSQL 则强烈推荐开启 pg_stat_statements 扩展,直接查询这个视图,按 mean_time 排序,一条 SQL 平均跑多慢清清楚楚。

这一步完成后,你应该能拿到三五条具体的、有问题的 SQL 文本了。

3. 执行计划,你得读懂它

拿到 SQL 之后,别上来就加索引,先看执行计划。这是整个排查的心脏。

EXPLAIN 看预估路径,用 EXPLAIN ANALYZE(PostgreSQL)或者 EXPLAIN 跟上实际执行(MySQL 8.0 的 EXPLAIN ANALYZE 也 OK)看真实统计。盯住这几个地方:

  • 访问方式:是不是走了 ALL 全表扫描,或者 Seq Scan?几百万行的表全表扫,神仙都救不了。如果用了索引,看是 refrange 还是 index,扫描行数(rows 列)是否合理。
  • 估算行数和实际行数差多少:这是个大坑。EXPLAIN 告诉你要扫 1 行,实际执行却扫了 10 万行,那大概率是统计信息过时了,优化器瞎选路径。这时赶紧 ANALYZE TABLE(MySQL)或 ANALYZE(PG)更新一下。
  • 额外的排序和临时表:执行计划里出现 Using filesortUsing temporary(MySQL)或者显式的 Sort、Hash 节点(PG),说明你的 ORDER BYGROUP BY 没能利用好索引,数据被迫在内存甚至磁盘上重新排队。
  • 连接顺序和算法:多表关联时,驱动表应当是小表,被驱动表的连接列要有索引。如果看到嵌套循环扫描了大量行,基本就是该有的索引没建。

把执行计划当成地图,它告诉你数据库是怎么找到数据的。哪段路堵车,一眼便知。

4. 索引,最容易背锅也最容易救火

90% 的慢查询,最后都归到索引上。常见的问题无非这几种:

缺索引WHERE 条件、JOIN 连接的列、ORDER BYGROUP BY 后的字段,如果没有索引,只能全表扫。看执行计划里没有 key,直接补上复合索引往往能解决问题。

索引失效。这才是让人挠头的地方,明明有索引,优化器就是不用。几种典型的“作死”操作:

  • 在索引列上用函数或计算:WHERE DATE(create_time) = '2025-01-01',这个 DATE() 一上,索引直接废。
  • 隐式类型转换:字段是字符串,你给个数字 WHERE phone = 13800138000,数据库得把所有手机号转成数字再比,没法用索引。
  • LIKE '%关键词',前面带百分号,索引失效。
  • 联合索引没遵循“最左前缀”:建了 (a,b,c) 的索引,但你查询只有 bc 条件,那对不起,用不上。

索引选择性太差。如果一个字段只有“是/否”两个值,即便有索引,返回一半数据时优化器会直接全表扫,因为这比来回跳转索引再回表要快。这不是 bug,是它更聪明的选择。

遇到这些情况,改写法、调整索引字段顺序、或者用覆盖索引来避免回表,都是常规手段。

5. 别忘了锁和长事务

有时一条 SQL 执行计划很完美,索引也都齐了,但偏偏卡着不动。这时候得往锁的方向看。

MySQL InnoDB 下,可以查 information_schema.INNODB_TRX 看当前事务,如果一个事务 trx_started 显示已经在 10 分钟前就开始了,还一直没提交,它很可能攥着一堆锁没放。再配合 performance_schema.data_locksdata_lock_waits(8.0 版本)就能看到谁在等谁的锁。SHOW ENGINE INNODB STATUS 里的锁信息也非常详细。

PostgreSQL 就更直接,查 pg_locks 视图,结合 pg_stat_activitywait_event_type = 'Lock' 的会话,然后用 pg_blocking_pids(pid) 一把找出阻塞源。

发现长事务或锁等待,策略通常是 kill 掉阻塞源头(比如那个忘记提交的只读事务),或者优化应用代码,把大事务拆小,尽快提交。

6. 往外看:系统资源和配置

如果 SQL 和索引都挑不出毛病,问题很可能在数据库外面一圈,或者在配置上。

  • CPU 飙高:大量排序、计算,或者全表扫描把 CPU 吃满,查询自然排队。看 top 谁在干活。
  • 磁盘 IO 告急iowait 高,说明缓冲池不够大,数据没法全部缓存到内存,频繁读磁盘。innodb_buffer_pool_size(MySQL)或 shared_buffers(PG)要是只设了几百 MB,而数据几百 GB,那每次查询都相当于去硬盘上刨地。
  • 内存配置抠搜:排序缓冲区(sort_buffer_size)、临时表大小(tmp_table_size)、PG 的 work_mem 设得太小,会导致稍大一点的排序或 Hash 操作就溢出到磁盘,慢得离谱。
  • 连接数爆炸:每个连接都吃内存,几百个连接上下文切来切去,CPU 光忙活调度就饱了。
  • 网络延迟:如果数据库和应用程序不在一个机房,而 SQL 又要返回几万行结果,网络传输本身就会成为瓶颈。

调整这些参数需要结合服务器的实际内存来,不是越大越好,但默认值往往偏保守。让热数据尽量待在内存里,是性价比最高的优化。

7. SQL 写法,顺手治治“代码洁癖”

有些 SQL 天生就慢,换种写法可能就跑得飞快。

  • 别贪图方便写 SELECT *:只要三五列,却把整行全拖出来,白白增加网络传输和回表开销。

  • 深分页老老实实延迟关联LIMIT 100000, 10 这样的翻页,数据库需要查出前 100010 行再扔掉 10 万行。不如先用覆盖索引取到主键,再回表拿数据,也就是常说的“延迟关联”。

  • 相关子查询能改写就改写WHERE 条件里每行都执行一次子查询,数据一大就是灾难,换成 JOINEXISTS 往往效果拔群。

  • 避免无谓的排序UNION 会默认去重排序,如果不需要去重,用 UNION ALLGROUP BY 如果已经利用索引有序了,就没必要再额外 ORDER BY

8. 定期“保养”:统计信息与表碎片

数据库跑久了,表会膨胀,统计信息会过时,这也是慢查询的慢性病。

MySQL 的 OPTIMIZE TABLE 或 PG 的 VACUUMVACUUM FULL 能回收空间,让扫描更紧凑。统计信息则需要定期或按需执行 ANALYZE,尤其在大批量写入或删除之后。优化器只有看得清数据分布,才能选对路。

当单表数据量真到了几千万甚至上亿,再怎么优化索引都会吃力,这时候就要考虑分区、分库分表,或者冷热数据分离了,但那属于架构演进的范畴,不是单条 SQL 能全解决的。

9. 把排查变成常态:监控与工具

别每次出问题都火急火燎地手动敲命令。花点时间把监控搭起来,能省下半条命。

MySQL 生态里,pt-query-digest + 慢日志是最基础的,进一步可以用 PMM(Percona Monitoring and Management),慢查询、锁、资源占用全看得见。PostgreSQL 则用 pg_stat_statements 搭配 pgBadger 分析日志,或者接上 pgwatch2。应用层挂上 APM(像 Pinpoint、SkyWalking),能直接看到哪条 SQL 在拖慢哪个接口,把锅精准分配给对应的人。


排查慢 SQL 这套活儿,说到底就一句话:先定位是哪条 SQL,再用执行计划看它怎么跑,最后从索引、锁、资源、写法几个方向对症下药。 多数问题,走到第三步就已经露出马脚了。下次再碰见数据库变慢,希望这个清单能帮你稳住心态,找到病灶,而不是对着满屏告警干瞪眼。

上一篇:数据库索引的定海神针:B+ 树凭什么这么能打? 下一篇:被误解的 Redis 单线程:为什么它依然是快如闪电的秘密

评论

评论(0)

暂无评论