

这大概是每个后端开发都逃不掉的噩梦:大半夜正刷着手机,报警群里突然弹出一句,“接口超时了,数据库 CPU 100%”。心里咯噔一下,打开监控一看,又是几条熟悉的慢 SQL 在捣鬼。
干过几年活,踩坑无数之后,我慢慢攒出了一套排查慢 SQL 的思路。它不靠玄学,也不需要你把《高性能MySQL》倒背如流,只需要按“从外到内、从大到小”的次序一层层往下捋,问题基本藏不住。这份清单更偏向实战,MySQL 和 PostgreSQL 都会兼顾着说,原理相通。
1. 先别急着优化,搞清“它是什么慢”

第一步往往最容易被跳过:你到底在应付哪种慢?
- 一直慢:只要执行这条 SQL 就慢。这种最好查,大概率是没索引或者 SQL 写得烂。
- 时而快时而慢:有时候秒出,有时候卡住。这种最磨人,多半和缓存失效、锁竞争、统计信息不准有关。
- 突然变慢:昨天好好的,今天挂了。十有八九是数据量暴增、表结构被人改过、索引被误删,或者统计信息过期导致执行计划跳变。
先把这个大方向判断清楚,能省下不少走弯路的时间。比如一直慢的 SQL 你非要去查锁,那不是瞎忙活吗。
2. 在慢日志里“捞”出肇事者
生产环境上百条查询在跑,怎么知道哪条是罪魁祸首?两个渠道:
第一,直接看“现场”。连上数据库看当前谁在跑,谁跑了很久。
- MySQL 里跑
SHOW FULL PROCESSLIST;,或者查information_schema.PROCESSLIST。看到状态是Sending data、Sorting result、Copying 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?几百万行的表全表扫,神仙都救不了。如果用了索引,看是ref、range还是index,扫描行数(rows列)是否合理。 - 估算行数和实际行数差多少:这是个大坑。
EXPLAIN告诉你要扫 1 行,实际执行却扫了 10 万行,那大概率是统计信息过时了,优化器瞎选路径。这时赶紧ANALYZE TABLE(MySQL)或ANALYZE(PG)更新一下。 - 额外的排序和临时表:执行计划里出现
Using filesort、Using temporary(MySQL)或者显式的 Sort、Hash 节点(PG),说明你的ORDER BY、GROUP BY没能利用好索引,数据被迫在内存甚至磁盘上重新排队。 - 连接顺序和算法:多表关联时,驱动表应当是小表,被驱动表的连接列要有索引。如果看到嵌套循环扫描了大量行,基本就是该有的索引没建。
把执行计划当成地图,它告诉你数据库是怎么找到数据的。哪段路堵车,一眼便知。
4. 索引,最容易背锅也最容易救火
90% 的慢查询,最后都归到索引上。常见的问题无非这几种:
缺索引。WHERE 条件、JOIN 连接的列、ORDER BY 和 GROUP BY 后的字段,如果没有索引,只能全表扫。看执行计划里没有 key,直接补上复合索引往往能解决问题。
索引失效。这才是让人挠头的地方,明明有索引,优化器就是不用。几种典型的“作死”操作:
- 在索引列上用函数或计算:
WHERE DATE(create_time) = '2025-01-01',这个DATE()一上,索引直接废。 - 隐式类型转换:字段是字符串,你给个数字
WHERE phone = 13800138000,数据库得把所有手机号转成数字再比,没法用索引。 LIKE '%关键词',前面带百分号,索引失效。- 联合索引没遵循“最左前缀”:建了
(a,b,c)的索引,但你查询只有b和c条件,那对不起,用不上。
索引选择性太差。如果一个字段只有“是/否”两个值,即便有索引,返回一半数据时优化器会直接全表扫,因为这比来回跳转索引再回表要快。这不是 bug,是它更聪明的选择。
遇到这些情况,改写法、调整索引字段顺序、或者用覆盖索引来避免回表,都是常规手段。
5. 别忘了锁和长事务

有时一条 SQL 执行计划很完美,索引也都齐了,但偏偏卡着不动。这时候得往锁的方向看。
MySQL InnoDB 下,可以查 information_schema.INNODB_TRX 看当前事务,如果一个事务 trx_started 显示已经在 10 分钟前就开始了,还一直没提交,它很可能攥着一堆锁没放。再配合 performance_schema.data_locks 和 data_lock_waits(8.0 版本)就能看到谁在等谁的锁。SHOW ENGINE INNODB STATUS 里的锁信息也非常详细。
PostgreSQL 就更直接,查 pg_locks 视图,结合 pg_stat_activity 看 wait_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条件里每行都执行一次子查询,数据一大就是灾难,换成JOIN或EXISTS往往效果拔群。避免无谓的排序:
UNION会默认去重排序,如果不需要去重,用UNION ALL;GROUP BY如果已经利用索引有序了,就没必要再额外ORDER BY。
8. 定期“保养”:统计信息与表碎片
数据库跑久了,表会膨胀,统计信息会过时,这也是慢查询的慢性病。
MySQL 的 OPTIMIZE TABLE 或 PG 的 VACUUM、VACUUM 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,再用执行计划看它怎么跑,最后从索引、锁、资源、写法几个方向对症下药。 多数问题,走到第三步就已经露出马脚了。下次再碰见数据库变慢,希望这个清单能帮你稳住心态,找到病灶,而不是对着满屏告警干瞪眼。
评论(0)