📖 本篇简介:线上突然告警"接口超时""数据库 CPU 打满",是每个后端与 DBA 都躲不掉的真实战场。本篇以"场景题 10 连问"的形式,带你走完一次完整的慢查询现场排查:从慢日志没开怎么紧急定位,到用 EXPLAIN 看清执行计划、改写 SQL、验证效果、止血、再到监控闭环与长效治理。每个场景都给出可直接执行的命令与 SQL(基于 MySQL 8.0),并标注考察点与加分点。学完你应该能在面试中条理清晰地还原一次真实排障过程,而不是背"建索引"三个字。
目录
- 1 本篇导读与慢查询排查总流程
- 2 场景一 慢查询日志未开启时如何紧急定位
- 3 场景二 慢日志开启后如何定位 TOP N 慢 SQL
- 4 场景三 拿到慢 SQL 后如何用 EXPLAIN 分析
- 5 场景四 EXPLAIN 显示全表扫描怎么改写
- 6 场景五 索引建了却没走如何排查索引失效
- 7 场景六 SQL 改写后如何验证优化效果
- 8 场景七 慢查询导致连接堆积 CPU 飙高如何止血
- 9 场景八 如何区分是 SQL 问题还是服务器参数问题
- 10 场景九 如何建立慢查询监控与告警闭环
- 11 场景十 慢查询治理的长效机制与团队规范
- 12 慢查询排查速查表
- 13 常见坑与注意事项
- 14 练习题与自测
1 本篇导读与慢查询排查总流程
慢查询排查不是"看到一条慢 SQL 就建索引"那么简单,它是一条有先后顺序的流水线。面试官想听的是你在高压下有没有章法,而不是零散的知识点堆砌。
一次完整的慢查询现场排查,通常按下面这条主线推进:
1. 止血优先:先让系统活过来(杀会话、限流、切只读),再谈根因;
2. 定位现场:慢日志、processlist、性能模式(sys schema)三管齐下;
3. 看清计划:用 EXPLAIN / EXPLAIN ANALYZE 判断访问类型、扫描行数、Extra;
4. 改写与加索引:先改 SQL 写法,再补合适的联合索引;
5. 验证效果:对比执行时间、扫描行数、CPU/负载;
6. 闭环:把慢 SQL 沉淀到监控平台,加告警,做定期review。
下面 10 个场景就是这条主线的逐步展开,场景之间是层层递进的追问关系:面试官抛出第一个问题后,会顺着你的回答一路追问下去。
-- 本示例基于 MySQL 8.0
-- 排障第一原则:先看当前数据库到底在忙什么
-- 查看当前正在执行的所有线程,按耗时倒序,快速找到"跑了很久"的那条 SQL
SELECT id, user, host, db, command, time, state, info
FROM information_schema.processlist
WHERE command <> 'Sleep' -- 过滤空闲连接
ORDER BY time DESC -- time 是该线程已执行的秒数
LIMIT 20;
2 场景一 慢查询日志未开启时如何紧急定位
场景描述:线上业务反馈接口大面积超时,你登录数据库后发现慢查询日志(slow log)根本没开,slow_query_log=OFF,现在手头没有任何历史慢 SQL 记录。你该怎么紧急定位到底是哪条 SQL 把库拖慢了?
考察点:考察你在"没有慢日志"这种裸奔现场下的应急手段,区分"事后分析"与"实时现场抓取"的能力。
解答思路:
不要纠结于"现在开慢日志来得及吗"——慢日志是统计历史的,当下正在跑的 SQL 要靠实时视图抓。
第一步,立刻看 processlist,把跑了最久的非 Sleep 线程抓出来(上一节那条 SQL 就是干这个的)。第二步,用 MySQL 8.0 自带的 sys schema,它把性能模式(Performance Schema)的数据整理成了人话:
-- 本示例基于 MySQL 8.0
-- sys.session 是 information_schema.processlist 的增强版,直接按耗时排序
-- 比 processlist 多了归一化后的 SQL 摘要,并标注了内存消耗
SELECT conn_id, user, db, command, state, time,
current_statement, -- 当前正在执行的完整 SQL
statement_latency, -- 该语句已执行时长
rows_examined, -- 已扫描行数(慢查询核心指标)
tmp_tables, tmp_disk_tables -- 是否落临时磁盘表
FROM sys.session
WHERE command = 'Query' -- 只看正在跑的语句
ORDER BY time DESC
LIMIT 10;
第三步,如果现场已经抓到嫌疑 SQL,但它还在跑且严重拖库,可以先 KILL 掉它止血(KILL <conn_id>;),再把它的 SQL 文本复制出来事后分析。第四步,止血后立刻把慢日志临时打开,让后续慢查询有记录:
-- 本示例基于 MySQL 8.0,在线动态开启慢日志,无需重启
SET GLOBAL slow_query_log = 'ON'; -- 开启慢日志
SET GLOBAL long_query_time = 1; -- 超过 1 秒就记录(紧急时可降到 0 抓全量)
SET GLOBAL log_queries_not_using_indexes = 'ON'; -- 顺便记录没走索引的语句
-- 注意:这些是在线参数,重启后丢失;事后要把长期配置写进 my.cnf
加分点:能说出"紧急时把 long_query_time 临时设为 0 抓全量,但量太大容易把磁盘打满,要及时调回",以及"performance_schema 没开的话 sys schema 是空的,要先确认 performance_schema=ON"。
3 场景二 慢日志开启后如何定位 TOP N 慢 SQL
场景描述:慢日志开了一晚上,导出了一个好几个 G 的 slow.log,里面几万条记录。你不可能一条条看。如何从海量慢日志里快速找出最值得优化的 TOP N 慢 SQL?
考察点:考察你对慢日志分析工具(mysqldumpslow、pt-query-digest)的熟悉程度,以及"按什么维度排序"的判断力。
解答思路:
原始慢日志是给机器看的,人要先做归一化和聚合。两条思路:
第一,MySQL 自带的 mysqldumpslow,零成本应急:
# 按累计总耗时排序,取最慢的前 10 类 SQL(-s t 按时间排序)
# -t 10 只显示前 10 条;-g 后面可接正则过滤
mysqldumpslow -s t -t 10 /data/mysql/slow.log
# 按扫描行数排序(-s rows),找出"扫了百万行却只返回几行"的 SQL
mysqldumpslow -s rows -t 10 /data/mysql/slow.log
# 按出现次数排序,找出最频繁的慢 SQL(高频小慢 SQL 累积伤害大)
mysqldumpslow -s c -t 10 /data/mysql/slow.log
第二,生产环境更推荐 Percona 工具箱的 pt-query-digest,它能做更精细的指纹聚合和统计:
# 生成慢查询报告:默认按总耗时占比排序,输出每类 SQL 的次数、平均/最大耗时、扫描行数
pt-query-digest /data/mysql/slow.log > slow_report.txt
# 只分析最近 1 小时的慢日志(配合 flush logs 切割后分析新文件更精准)
pt-query-digest --since '2026-10-07 10:00:00' /data/mysql/slow.log
pt-query-digest 报告顶部会有一个汇总表,告诉你"哪类 SQL 吃掉了 90% 的慢查询时间",优化那一类收益最大。排序维度的选择本身就是考点:优化"累计耗时最高"的 SQL 收益最大,而不是单条最慢的。一条每天跑 1 万次每次 1 秒的 SQL,危害远大于每天跑 1 次每次 10 秒的 SQL。
加分点:能说出"归一化(把具体参数变成占位符)很重要,否则 where id=1 和 where id=2 会被当成两条 SQL",以及"慢日志里 Rows_examined 和 Rows_sent 的比值是判断索引好坏的金标准——扫 10000 行返回 1 行就是烂查询"。
4 场景三 拿到慢 SQL 后如何用 EXPLAIN 分析
场景描述:你从慢日志里抓到这样一条 SQL:SELECT * FROM orders WHERE user_id=12345 AND status=2 ORDER BY create_time DESC LIMIT 20;,平均执行 3 秒。怎么用 EXPLAIN 把它看清?
考察点:这是慢查询排查的核心场景,考察 EXPLAIN 各列含义,尤其是 type、key、rows、Extra 四列。
解答思路:
先执行 EXPLAIN,重点看四列:
-- 本示例基于 MySQL 8.0
EXPLAIN
SELECT * FROM orders
WHERE user_id = 12345 AND status = 2
ORDER BY create_time DESC
LIMIT 20;
典型输出逐列解读:
| 列名 | 关注什么 | 坏信号 |
|---|---|---|
| type | 访问类型,从好到坏:const > eq_ref > ref > range > index > ALL | 出现 ALL(全表扫描)必须优化 |
| key | 实际命中的索引 | 为 NULL 表示没走索引 |
| rows | 优化器预估要扫描的行数 | 越大越危险,与返回行数悬殊要警惕 |
| Extra | 额外信息 | Using filesort、Using temporary 都是坏信号 |
这条 SQL 常见的坏输出是:type=ALL、key=NULL、rows=全表行数、Extra=Using filesort。这说明:既没走 user_id 的索引,排序还额外做了一次文件排序。
MySQL 8.0 还提供了更直观的 EXPLAIN ANALYZE,它会真实执行 SQL 并输出每个环节的实际耗时与行数,比老版 EXPLAIN 的"预估"更准:
-- 本示例基于 MySQL 8.0,真实执行并输出每个算子的实际耗时
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE user_id = 12345 AND status = 2
ORDER BY create_time DESC
LIMIT 20;
加分点:能主动提到"Extra 里如果出现 Using index 是好事,说明走了覆盖索引不用回表",以及"rows 是预估值不是精确值,统计信息过期时可能失真,可以 ANALYZE TABLE orders; 刷新"。
5 场景四 EXPLAIN 显示全表扫描怎么改写
场景描述:上一条 SQL 的 EXPLAIN 确认是 type=ALL 全表扫描,orders 表有 5000 万行。你打算怎么改?
考察点:考察联合索引设计与最左前缀原则的落地,以及 ORDER BY 如何利用索引避免 filesort。
解答思路:
这条 SQL 的过滤条件是 user_id=? AND status=?,排序是 create_time DESC。最优索引要同时服务"等值过滤"和"排序"。按最左前缀原则,把等值列放前面、排序列放后面:
-- 本示例基于 MySQL 8.0
-- 建联合索引:user_id(等值)在前,status(等值)其次,create_time(排序)最后
-- 这样走到 user_id 后,同一 user_id 下的数据已按 create_time 有序,可避免 filesort
ALTER TABLE orders
ADD INDEX idx_uid_status_ctime (user_id, status, create_time);
为什么 status 放中间而不是放最后?因为 user_id 和 status 都是等值条件,放在一起能把数据先精确过滤出来;把 create_time 放在最后,B+ 树叶子节点在 (user_id, status) 相同的内部是按 create_time 递增存的,ORDER BY create_time DESC LIMIT 20 可以顺着索引倒序扫 20 条就停,无需排序。
如果业务上 status 区分度很低(比如只有 0/1/2 三种值),也可以考虑把它去掉,只建 (user_id, create_time),在回表时再过滤 status。这取决于区分度,要拿实际数据验证:
-- 本示例基于 MySQL 8.0
-- 先看 status 的区分度:不同值越少,越不适合作为索引列
SELECT status, COUNT(*) FROM orders GROUP BY status;
改写后再 EXPLAIN 一次,期望看到:type=ref、key=idx_uid_status_ctime、rows≈20、Extra 里不再有 Using filesort。
加分点:能说出"SELECT * 是坏味道,如果只需要个别字段,建覆盖索引把查询字段都包进去,连回表都省了",以及"LIMIT 20 配合有序索引是'延迟关联'优化的好搭档"。
6 场景五 索引建了却没走如何排查索引失效
场景描述:你明明给 user_id 建了索引,但 EXPLAIN 显示这条 SQL 就是不走,依旧 type=ALL。可能的原因有哪些?怎么逐个排除?
考察点:这是高频面试点,考察索引失效的全部经典场景。
解答思路:
索引失效常见原因,按排查顺序排:
-- 本示例基于 MySQL 8.0
-- 原因1:索引列上做了函数/计算,导致索引失效
-- 错误写法:对列用函数,B+树里存的是原始值,函数后的值无法定位
SELECT * FROM orders WHERE DATE(create_time) = '2026-10-07';
-- 正确写法:改成范围查询,让索引列保持"裸列"
SELECT * FROM orders
WHERE create_time >= '2026-10-07 00:00:00'
AND create_time < '2026-10-08 00:00:00';
-- 原因2:隐式类型转换。若 phone 是 varchar,传数字会触发全表扫
-- 错误写法:字符串列传数字,MySQL 会把列转成数字再比较,索引失效
SELECT * FROM users WHERE phone = 13800000000;
-- 正确写法:类型与列定义一致
SELECT * FROM users WHERE phone = '13800000000';
-- 原因3:联合索引不满足最左前缀,跳过了第一列
-- 索引是 (a,b,c),这里直接用 b,走不到
SELECT * FROM t WHERE b = 1;
其他常见失效点:LIKE '%xxx' 前导通配符、OR 连接了无索引的列、优化器估算全表扫比走索引还快(数据占比太大,比如 where 命中了表 30% 以上数据,优化器主动放弃索引)。最后这种"优化器主动不走路"很容易被忽略:
-- 本示例基于 MySQL 8.0
-- 强制走索引验证:如果强制走索引后确实更快,说明是优化器估算失误
EXPLAIN SELECT * FROM orders FORCE INDEX (idx_uid_status_ctime)
WHERE user_id = 12345 AND status = 2;
-- 若是统计信息过期导致估算失误,刷新一下统计信息
ANALYZE TABLE orders;
加分点:能把"索引失效"和"优化器主动放弃索引"区分开——前者是写法问题,后者是成本估算问题,两者处理方式完全不同。
7 场景六 SQL 改写后如何验证优化效果
场景描述:你加了索引、改了 SQL,测试环境跑确实快了。但你不敢直接上线,怎么科学地验证优化效果,而不是"我觉得快了"?
考察点:考察优化前后的对比验证方法,区分"单条 SQL 变快"与"整体负载变好"。
解答思路:
验证要分三层做:
第一层,单条 SQL 对比。用 SQL_NO_CACHE 排除查询缓存干扰(8.0 已移除查询缓存,这一点反而简单),分别测改写前后的执行时间和扫描行数:
-- 本示例基于 MySQL 8.0
-- 改写前后各跑 3 次取平均值,排除冷/热数据差异
-- 重点对比 EXPLAIN 的 rows 列(扫描行数)从几百万降到几十
第二层,看数据库整体负载。单条快了不代表整体好,要观察 Threads_running、CPU、慢查询数量是否下降:
-- 本示例基于 MySQL 8.0
-- 实时看当前活跃线程数,优化前如果飙到几十,优化后应回落到个位数
SHOW GLOBAL STATUS LIKE 'Threads_running';
-- 看慢查询累计数,优化后这个增量应明显放缓
SHOW GLOBAL STATUS LIKE 'Slow_queries';
第三层,灰度验证。不要一把全量上,先在从库或灰度流量上跑,观察一段时间。加索引本身也要谨慎:ALTER TABLE 在大表上会锁表或耗时很长,8.0 的 Online DDL 虽然不锁表,但仍会消耗 IO,要在低峰期做。
-- 本示例基于 MySQL 8.0
-- 8.0 建索引支持 Instant/Online,指定 ALGORITHM 和 LOCK 明确预期
ALTER TABLE orders
ADD INDEX idx_uid_status_ctime (user_id, status, create_time),
ALGORITHM=INPLACE, LOCK=NONE; -- 尽量不锁表,业务无感
加分点:能说出"验证要看 P99 延迟而不是平均耗时,因为慢查询往往拖垮的是尾部请求",以及"上线后要回滚预案——如果新索引反而拖慢了写入,要能快速 DROP"。
8 场景七 慢查询导致连接堆积 CPU 飙高如何止血
场景描述:大促期间数据库 CPU 突然跑到 100%,应用层连接池被打满,接口全部超时。这时你不能慢条斯理地分析根因,先怎么止血?
考察点:考察"止血优先"的应急意识,以及杀会话、限流、切换等手段的优先级排序。
解答思路:
止血顺序要清晰,记住"先保整体可用,再查局部根因":
第一步,快速识别并杀掉最耗资源的会话。用 sys.session 找到跑最久、扫描行数最多的几个连接,逐个 KILL:
-- 本示例基于 MySQL 8.0
-- 找出跑了超过 30 秒的查询会话,准备 KILL
SELECT conn_id, time, current_statement
FROM sys.session
WHERE command = 'Query' AND time > 30
ORDER BY time DESC;
-- 杀掉指定会话(conn_id 替换成上面查到的 id)
KILL 123456;
第二步,如果是某一类 SQL 集中爆发(比如某个报表任务被误触发),可以在应用层或网关层先限流、熔断下游依赖,或者临时把读流量切到从库,给主库减压。第三步,紧急情况下甚至可以临时把大查询对应的业务功能下线/降级,保住核心交易链路。
-- 本示例基于 MySQL 8.0
-- 紧急时可临时设置最大连接数相关保护,防止连接把内存吃光
-- 注意 max_connections 调小要谨慎,别把正常业务也挡在外面
SHOW VARIABLES LIKE 'max_connections';
止血后第一件事是保留现场:把 processlist 快照、慢日志、监控曲线截图存下来,再去做根因分析。很多人一上来就重启数据库,结果现场没了,事后完全无法复盘。
加分点:能说出"KILL 不是万能的,大事务 KILL 后还要做回滚,回滚期间照样占资源",以及"止血阶段不要同时做多个变更,否则出了事不知道是哪个动作引起的"。
9 场景八 如何区分是 SQL 问题还是服务器参数问题
场景描述:业务说数据库慢,但你翻慢日志发现并没有特别离谱的 SQL。这时怎么判断是 SQL 写得烂,还是服务器配置/资源本身有问题?
考察点:考察系统层面的排查能力,跳出"就 SQL 论 SQL",看内存、IO、连接数、参数配置。
解答思路:
按"资源维度"逐项排除:
-- 本示例基于 MySQL 8.0
-- 1. 看 InnoDB 缓冲池命中率,低于 99% 说明内存不够,数据在频繁读磁盘
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';
-- 命中率 = 1 - Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests
-- reads 是从磁盘读的次数,这个值持续上涨就说明 buffer_pool 太小
-- 2. 查看当前连接数是否逼近上限
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW VARIABLES LIKE 'max_connections';
-- 3. 看是否有大量临时表落磁盘(内存排序/临时表撑爆)
SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables';
系统层面再用 top、iostat、vmstat 看 CPU、磁盘 IO、内存 swap:如果 iostat 显示磁盘 %util 接近 100%,那瓶颈在磁盘而不是 SQL;如果内存快用完开始 swap,数据库性能会断崖式下跌。
还有一种常见情况:统计信息过期 + 优化器选错计划,平时跑得好好的 SQL 突然变慢,这时 ANALYZE TABLE 刷新统计信息往往立竿见影。
加分点:能说出"排查慢要先画一条资源时间线:CPU 高是 CPU bound,IO 高是 IO bound,连接满是并发 bound,三种瓶颈的解法完全不同"。
10 场景九 如何建立慢查询监控与告警闭环
场景描述:这次慢查询事件处理完了,但你不想下次再被凌晨的告警叫醒。怎么建立一套慢查询的监控与告警闭环,把问题消灭在告警阶段而不是故障阶段?
考察点:考察从"被动救火"到"主动治理"的运维思维。
解答思路:
监控闭环分三层:
第一层,采集。把慢日志持续采集到统一平台(如 PT 报告定时任务、或接入 Prometheus + Grafana),每天自动生成慢 SQL TOP 榜。第二层,告警。设定合理阈值:
-- 本示例基于 MySQL 8.0
-- 关键监控指标与建议告警阈值
-- Slow_queries 每分钟增量 > 50 → 慢查询突增告警
-- Threads_running > 20 → 活跃会话堆积告警
-- InnoDB 行锁等待 time 持续上涨 → 锁竞争告警
SHOW GLOBAL STATUS LIKE 'Slow_queries';
第三层,治理。每周做一次慢 SQL review,把 TOP N 慢 SQL 分派给业务负责人优化,形成"发现→分派→优化→验证→销项"的闭环。新 SQL 上线前,在 CI 环节接入 SQL 审核工具(比如 Soar),自动拦截全表扫描、无索引 SQL。
# 本示例为 sqlite3 演示:用一张表模拟"慢 SQL 治理台账",记录每条慢 SQL 的状态流转
import sqlite3
conn = sqlite3.connect(":memory:") # 内存库,仅演示流程
cur = conn.cursor()
# 建一张慢 SQL 治理台账表
cur.execute("""CREATE TABLE slow_sql (
id INTEGER PRIMARY KEY,
sql_fingerprint TEXT, -- SQL 归一化指纹
avg_time_ms INTEGER, -- 平均耗时
owner TEXT, -- 负责业务方
status TEXT)""") -- 状态:待处理/优化中/已解决
# 插入两条待治理的慢 SQL
cur.executemany("INSERT INTO slow_sql(sql_fingerprint,avg_time_ms,owner,status) VALUES(?,?,?,?)",
[("SELECT * FROM orders WHERE user_id=?", 3200, "交易组", "待处理"),
("SELECT * FROM orders ORDER BY create_time=?", 5800, "报表组", "优化中")])
conn.commit()
# 查询:统计各状态数量,用于周会看板
for row in cur.execute("SELECT status, COUNT(*) FROM slow_sql GROUP BY status"):
print(row)
conn.close()
上面这段脚本实跑输出为:('待处理', 1) 和 ('优化中', 1),代表治理台账里还有未闭环的项。
加分点:能说出"告警要分级——慢查询数涨是预警,连接堆积是紧急,不要把所有告警都做成电话叫醒,否则会告警疲劳"。
11 场景十 慢查询治理的长效机制与团队规范
场景描述:你发现慢查询反复出现,根因不是某个人技术差,而是团队没有规矩。要建立什么样的长效机制,才能从制度上减少慢查询?
考察点:考察技术管理与规范建设能力,从"修一条 SQL"上升到"建一套机制"。
解答思路:
长效机制的核心是"把好入口关",让烂 SQL 根本进不了生产:
1. 建表规范:强制每张表必须有合适索引,禁止大表 SELECT *;
2. 上线审核:所有 SQL 变更走审核流程,大表加索引必须 DBA 评审;
3. 定期巡检:每周慢 SQL review,每月容量与索引健康检查;
4. 压测前置:核心 SQL 在上线前用生产级数据量压测,别等线上才暴露;
5. 权限隔离:禁止业务账号在主库跑大报表,报表流量强制走从库。
-- 本示例基于 MySQL 8.0
-- 一份最基本的建表规范示范:字段、索引、注释一应俱全
CREATE TABLE goods_order (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
user_id BIGINT UNSIGNED NOT NULL COMMENT '下单用户ID',
status TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态 0待支付1已支付2已发货',
amount DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '订单金额',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id),
KEY idx_uid_ctime (user_id, create_time) -- 高频查询组合提前规划
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
加分点:能说出"治理慢查询最大的阻力不是技术而是习惯,要靠 CI 卡点和 review 制度把规范固化下来,而不是靠人肉记忆"。
一次完整的慢查询事件复盘,建议按下面这个模板归档,它能帮团队把每次踩坑变成可复用的经验:
| 复盘项 | 记录内容示例 |
|---|---|
| 故障时间线 | 14:02 告警 → 14:05 抓到慢 SQL → 14:10 KILL 止血 → 14:30 加索引恢复 |
| 根因 | 报表任务误触发全表扫描,缺失联合索引 |
| 止血动作 | KILL 会话、读流量切从库 |
| 根治动作 | 补 (user_id,status,create_time) 索引、报表走从库 |
| 遗留风险 | 统计信息过期会复发,已加 ANALYZE TABLE 定时任务 |
这套模板坚持用半年,团队的"排障肌肉记忆"就建立起来了,新人遇到同类问题也能照单执行。
12 慢查询排查速查表
下面这张表可以在面试前快速过一遍,对应排查流程的每一步该看什么。
| 排查阶段 | 核心命令 / 工具 | 关键看什么 |
|---|---|---|
| 抓现场 | sys.session / processlist | 跑最久、扫描行数最多的会话 |
| 历史分析 | mysqldumpslow / pt-query-digest | 按累计耗时排序的 TOP N 慢 SQL |
| 看计划 | EXPLAIN / EXPLAIN ANALYZE | type、key、rows、Extra |
| 判索引 | SHOW INDEX FROM 表 | 联合索引列顺序、区分度 |
| 验资源 | buffer_pool 命中率、Threads_running | 内存、连接、IO 是否打满 |
| 止血 | KILL conn_id | 杀大查询,保核心链路 |
| 闭环 | 慢日志采集 + 阈值告警 | Slow_queries 增量、P99 延迟 |
| EXPLAIN 的 Extra 字段 | 含义 | 好坏 |
|---|---|---|
| Using index | 覆盖索引,无需回表 | 好 |
| Using where | 在引擎层过滤 | 中性 |
| Using filesort | 额外排序 | 坏,要优化 ORDER BY |
| Using temporary | 用了临时表 | 坏,常见于 GROUP BY |
| Using join buffer | JOIN 没走索引 | 坏 |
13 常见坑与注意事项
排查慢查询时,以下坑最容易踩:
1. 一上来就重启数据库:现场全丢,事后无法复盘。正确做法是先快照现场再处理。
2. 只看单条最慢 SQL:忽略了高频小慢 SQL 的累积伤害,要按"累计耗时"排序。
3. 加索引不加测试:大表加索引在高峰期做,把 IO 打满,反而引发新故障。要低峰期、Online DDL。
4. 把 `SELECT *` 当无所谓:即使走了索引,回表取所有字段依然慢,能覆盖索引就覆盖索引。
5. `KILL` 之后不观察回滚:KILL 大事务后 InnoDB 还要做 undo 回滚,期间资源照样占用。
6. 在线参数改了不写配置文件:SET GLOBAL 重启即失效,下次重启问题复现。
7. 迷信 EXPLAIN 的 rows:它是预估值,统计信息过期会失真,要用 EXPLAIN ANALYZE 实测。
14 练习题与自测
练习 1:线上慢日志没开,你手头只有一个能登录的数据库,说出至少三种抓取当前慢 SQL 的方法。
参考答案:①SELECT ... FROM processlist ORDER BY time DESC;②SELECT ... FROM sys.session(性能模式开启时);③ 紧急SET GLOBAL long_query_time=0在线开慢日志抓后续语句。
练习 2:一条 SQL 的 EXPLAIN 显示 type=ALL、rows=5000000、Extra=Using filesort,请写出你判断的问题和优化方向。
参考答案:全表扫描 + 额外排序。优化方向:为 WHERE 等值列 + ORDER BY 列建联合索引,让过滤和排序都走索引,消除 filesort。
练习 3:你建了索引但 SQL 就是不走,列出至少 4 种可能原因。
参考答案:① 索引列上用了函数;② 隐式类型转换;③ LIKE 前导 %;④ 联合索引违反最左前缀;⑤ 优化器估算全表扫更快而主动放弃;⑥ OR 连接了无索引列。
练习 4:止血阶段,数据库 CPU 100%、连接池打满,你的前三步操作是什么?
参考答案:①sys.session找最耗会话并KILL止血;② 应用层限流/降级非核心功能、读流量切从库;③ 保留 processlist 与监控现场后再做根因分析。
练习 5(自测):用自己的话口述一遍"从告警到闭环"的完整慢查询排查流程,要求覆盖止血、定位、EXPLAIN、改写、验证、监控六个环节,并说出每个环节的至少一个具体命令。
自测要点:能否说出 processlist/pt-query-digest/EXPLAIN/FORCE INDEX/Threads_running/慢日志告警 这六个关键词对应的动作。
下一篇预告:第 22 章 死锁与锁等待现场处理
文章回复
0 条公开回复