📖 本篇简介:慢查询不是"慢一点"那么简单——一条没走索引的 SQL,在高并发下可以把整个库的 CPU、IO、连接池全部打满,演变成全站不可用。本章复盘 10 个真实感极强的慢查询事故,每个复盘点按"事故经过 → 根因 → 处理 → 预防"四要素展开。所有 SQL 示例基于 MySQL 8.0 语法。读完你能在事故发生的前 5 分钟就判断出该 kill 哪条 SQL、该加哪个索引、该拦哪类请求。
本章适合已经背过索引原理、但缺少"线上救火"经验的工程师。建议配合第 13 章(EXPLAIN)、第 14 章(SQL 优化)、第 21 章(慢查询现场排查)一起阅读。
慢查询事故的共同点是:单条 SQL 本身没什么大错,但是在高并发、大数据量、统计信息过期、索引设计不合理等因素叠加下,被放大成全站事故。复盘的目的不是追责,而是把"下次再遇到该怎么处理"变成肌肉记忆。
本章 10 个复盘点都是从真实线上事故抽象出来的典型场景,覆盖了索引失效、大结果集、N+1、深分页、JOIN 顺序、函数包裹、隐式转换、实时聚合、filesort、导出打满 IO 这十类最常见的慢查询事故类型。读完一遍,下次值班遇到类似情况至少不会慌。
每个复盘点都按"事故经过 → 根因 → 处理 → 预防"四要素展开,这是事故复盘的标准结构。建议你读完每个案例后,先自己想"如果是我会怎么处理",再对照文中的做法,差距就是你要补的功课。
目录
- 坑 1:后台运营报表全表扫描拖垮主库
- 坑 2:漏写 WHERE 的 UPDATE 锁全表
- 坑 3:ORM N+1 在促销日被放大 1000 倍
- 坑 4:深分页 LIMIT 10000050 拖垮从库
- 坑 5:JOIN 顺序错误引发笛卡尔积
- 坑 6:函数包裹索引列导致索引失效
- 坑 7:隐式类型转换让手机号索引失效
- 坑 8:大表实时 COUNT 拖垮列表页
- 坑 9:filesort 大结果集落盘打满临时目录
- 坑 10:未限行数的导出任务打满 IO
- 慢查询事故的共性规律
- 值班 Checklist:慢查询事故 15 分钟处置流程
- 事故复盘报告模板
- 慢查询相关监控指标速记
- 新手值班常见误区
- 速查表:慢查询事故应急流程
- 自测:如果是你值班
- 一句话总结
坑 1:后台运营报表全表扫描拖垮主库
事故经过:周一上午 10 点,运营同事在后台跑"上月订单汇总报表",一条 SQL 跑了 3 分钟没出结果。期间主库 CPU 从 30% 飙到 100%,在线接口 P99 从 50ms 涨到 3 秒,告警群炸锅。DBA 上去 SHOW PROCESSLIST,看到这条 SQL 状态是 Sending data,已经扫描了 800 万行。业务方开始催"订单接口怎么都超时了",客服那边已经收到 20 多客诉。
根因:报表 SQL 写的是 SELECT COUNT(*), SUM(amount) FROM orders WHERE created_at BETWEEN ...,但 orders 表上只有 created_at 单列索引,没有覆盖到 amount;更要命的是运营选了"近一年"的范围,结果集本身就大。优化器估算错误,选了全表扫。
处理:立刻 KILL <process_id> 杀掉这条会话;5 秒后主库 CPU 回落。把报表流量切到只读从库,并加了超时限制(MAX_EXECUTION_TIME=3000)。
-- 事后给报表 SQL 加覆盖索引
ALTER TABLE orders ADD INDEX idx_created_amount (created_at, amount);
-- 强制限制单条 SQL 最长执行 3 秒
SET SESSION MAX_EXECUTION_TIME = 3000;
预防:后台报表系统必须走独立的只读从库,不能跟在线业务抢主库;所有报表 SQL 必须设超时;大报表走预聚合(每日凌晨跑结果落表),不要在线实时算。事后还补了一条监控规则:任何单条 SQL 执行超过 5 秒就自动 kill 并告警,不再等人工处理。
坑 2:漏写 WHERE 的 UPDATE 锁全表
事故经过:开发写数据订正脚本,本意是"把 user_id=10086 的订单状态从 0 改成 1",结果手滑漏写了 WHERE,脚本一执行:
-- 事故 SQL
UPDATE trade_order SET status = 1;
-- 1 亿行订单全部被加了 X 锁,整表阻塞
30 秒内所有订单相关接口超时,连接池被打满。运维监控看到数据库连接数从 200 飙到 800(max_connections=1000),立刻拉起告警。
根因:UPDATE 不带 WHERE 等于锁全表,InnoDB 会对扫描到的每一行加行锁,虽然 MySQL 8.0 有"不带 WHERE 的 UPDATE 相当于全表锁"的保护,但脚本通过 ORM 执行时绕过了保护。
处理:DBA 立刻 KILL 这条会话;InnoDB 回滚这个大事务花了 8 分钟,期间业务仍然阻塞。事后从 binlog 闪回,把不该改的行恢复。
-- 事后:用 binlog2sql / my2sql 闪回
-- 先找到对应位点,生成反向 SQL
-- 本示例基于 MySQL 8.0,binlog_format=ROW
预防:所有写操作必须带 WHERE 与 LIMIT;SQL 审核平台直接拦截"UPDATE/DELETE 不带 WHERE";写操作前先用 SELECT COUNT(*) 评估影响行数。这次事故后,团队立下铁律:任何写脚本上线前必须在测试库跑一遍,并且由第二个人 review 才能上生产。
坑 3:ORM N+1 在促销日被放大 1000 倍
事故经过:大促当天,商品详情接口 P99 从 80ms 涨到 4 秒。监控发现数据库 QPS 从 2000 涨到 5 万,但应用 QPS 只涨了 3 倍。慢日志里同一条 SELECT * FROM sku WHERE id=? 出现了几万次。客服那边已经开始收到"商品页打不开"的反馈,运营在大促群里急得不行。
根因:商品列表页代码里,循环每个商品再单独查一次 SKU 详情,典型的 N+1。平时 QPS 低没感觉,促销日 QPS 一放大,数据库瞬间被打爆。
# 事故代码(SQLAlchemy 伪代码)
products = session.query(Product).limit(50).all()
for p in products:
sku = session.query(Sku).filter_by(product_id=p.id).first() # N 次查询
处理:紧急下线商品列表页的"SKU 详情"模块,P99 立刻回落。当晚紧急发版改成 IN 批量查询。
# 修复后
product_ids = [p.id for p in products]
skus = session.query(Sku).filter(Sku.product_id.in_(product_ids)).all()
预防:Code Review 看到 for 循环里查库一律打回;压测时开启 ORM SQL 日志,数一下接口的 SQL 条数;N+1 这类问题必须在压测阶段就暴露。事后团队把"接口 SQL 条数"纳入压测报告,超过 20 条就必须优化。
坑 4:深分页 LIMIT 100000,50 拖垮从库
事故经过:后台"用户订单列表"运营翻到第 2000 页,接口跑了 20 秒。这条 SQL 打到只读从库,把从库 IO 打满,导致所有走从库的查询都变慢,主从延迟从 0 涨到 30 秒。在线下单接口读从库查不到刚下的订单,用户反复刷新以为没下单成功。
-- 事故 SQL
SELECT * FROM orders WHERE user_id = 10086
ORDER BY created_at DESC
LIMIT 100000, 50;
根因:LIMIT offset, size 的执行逻辑是"扫描 offset+size 行,丢弃前 offset 行"。offset=10 万时,MySQL 要扫 100050 行才能返回 50 行,成本随 offset 线性增长。
处理:把后台翻页限制在 100 页以内,超过就提示用户加筛选条件;同时把深分页改成游标分页。
-- 修复后:记住上一页最后一行的 created_at
SELECT * FROM orders WHERE user_id = 10086
AND created_at < '2026-09-01 10:00:00'
ORDER BY created_at DESC
LIMIT 50;
预防:后台系统不要让用户"跳到任意页";深分页一律用游标;从库也必须配慢查询告警,不能觉得"从库就是给人查的"。事后把后台翻页 UI 改成"上一页/下一页",去掉"跳到第 N 页"输入框,从产品层面杜绝深分页。
坑 5:JOIN 顺序错误引发笛卡尔积
事故经过:订单列表关联用户表和商品表,一条 SQL 跑了 60 秒。EXPLAIN 一看,rows 列显示预估扫描 2 亿行。这条 SQL 是新版本上线带过来的,灰度 10% 流量时还没暴露,全量后问题立刻放大。
-- 事故 SQL
SELECT o.order_no, u.name, p.title
FROM orders o
JOIN user u ON o.user_id = u.id
JOIN product p ON o.product_id = p.id
WHERE u.city = '北京';
根因:统计信息过期,优化器选错了驱动表:先从 orders 全表扫(1 亿行),再对每行去 JOIN user 和 product。本来应该先把"北京用户"筛出来(可能几万行),再 JOIN orders。
处理:手动 ANALYZE TABLE orders; 更新统计信息,优化器立刻选对了执行计划,SQL 从 60 秒降到 80ms。
-- 强制指定 JOIN 顺序(紧急时用)
SELECT o.order_no, u.name, p.title
FROM user u
STRAIGHT_JOIN orders o ON o.user_id = u.id
STRAIGHT_JOIN product p ON o.product_id = p.id
WHERE u.city = '北京';
预防:大表导入或大量更新后,主动 ANALYZE TABLE;EXPLAIN 审查所有多表 JOIN 的 SQL;STRAIGHT_JOIN 是紧急手段,不是常规写法。事后在 ETL 任务结束后自动跑一遍 ANALYZE,避免统计信息长期过期。
坑 6:函数包裹索引列导致索引失效
事故经过:运营按"下单日期"查订单,SQL 写的是:
SELECT * FROM orders WHERE DATE(created_at) = '2026-10-01';
-- created_at 上有索引,但被 DATE() 函数包了一层
本来预期走索引,结果全表扫,1 亿行扫了 40 秒。这条 SQL 是运营自己拼的查询条件,开发 review 时没注意到函数包裹。
根因:MySQL 索引存的是 created_at 的原始值,对列套函数后,B+ 树无法定位。DATE(created_at) 是一个新表达式,索引帮不上忙。
处理:改写成范围查询,让索引列本身出现在 WHERE 里。
-- 修复后
SELECT * FROM orders
WHERE created_at >= '2026-10-01 00:00:00'
AND created_at < '2026-10-02 00:00:00';
预防:Code Review 时看到 WHERE 函数(列) = 常量 一律打回;需要按天查询就建合适的索引或用范围条件;MySQL 8.0 支持函数索引,但那是兜底方案。事后把"日期范围查询"写成团队模板,新人入职直接抄。
坑 7:隐式类型转换让手机号索引失效
事故经过:登录接口按手机号查用户,平时 5ms,某天突然 3 秒。慢日志一看:
-- user 表 phone 是 VARCHAR,建了索引
SELECT * FROM user WHERE phone = 13800001111;
-- 应用层把 phone 当 Long 传进来了
事故发生在一次版本升级后,某个序列化库把手机号自动转成了 Long,问题才暴露。
根因:MySQL 把 phone 列 CAST 成 DOUBLE 再比较,相当于在索引列上套函数,索引失效。某个版本的序列化升级后,手机号从 String 变成了 Long,问题才暴露。
处理:紧急回滚序列化版本;同时把代码里所有 WHERE phone = ? 的参数类型统一成 String。
-- 正确写法
SELECT * FROM user WHERE phone = '13800001111';
预防:字符串列(手机号、订单号、身份证)一律 VARCHAR,参数绑定类型严格一致;ORM 开启参数类型检查;Code Review 专门盯"数字传字符串列"。事后给所有字符串 WHERE 条件加了单元测试,验证传入数字时也能命中索引。
坑 8:大表实时 COUNT 拖垮列表页
事故经过:商品列表页右下角显示"共 1234567 件商品",每次刷新都跑一次 SELECT COUNT(*) FROM product WHERE status=1。商品表 800 万行,COUNT 要 2 秒。列表页 QPS 100,数据库瞬间被 200 个并发 COUNT 打满。
根因:InnoDB 因为 MVCC 无法维护全局精确行数,COUNT(*) 必须扫索引。列表页每次刷新都实时算,把数据库当计数器用。
处理:立刻把总数改成 Redis 缓存值,商品上架/下架时维护计数器;列表页 P99 从 2 秒降到 20ms。
-- 事后给状态字段加索引,至少让 COUNT 走二级索引
ALTER TABLE product ADD INDEX idx_status (status);
-- 但即使走索引,800 万行 COUNT 仍然要扫很久,缓存才是正解
预防:任何"总数"需求优先用 Redis 计数器或预聚合表;实在要精确 COUNT,放到后台低峰执行,不要在线接口里跑。事后产品经理把"精确总数"改成"约 123 万件",业务上完全接受,技术上轻松很多。
坑 9:filesort 大结果集落盘打满临时目录
事故经过:后台"订单导出"按 created_at 排序,结果集 200 万行。MySQL 排序缓冲区 sort_buffer_size 只有 4MB,放不下,开始用磁盘临时文件排序。临时目录 /tmp 被 20GB 的排序文件占满,MySQL 报错退出。
根因:ORDER BY 的结果集超过 sort_buffer_size 时,MySQL 用磁盘 merge sort,临时文件暴涨;并发几个这样的查询,/tmp 直接满。
处理:紧急清理临时文件,把 sort_buffer_size 临时调到 16MB;限制后台导出任务并发数。
-- 查看排序是否走磁盘
SHOW STATUS LIKE 'Sort_merge_passes';
-- 这个值很大,说明频繁落盘
-- 合理配置(本示例基于 MySQL 8.0)
SET GLOBAL sort_buffer_size = 4 * 1024 * 1024; -- 4MB 起步
预防:导出任务必须分页 + 流式;不要在线做"大结果集排序";临时目录单独挂载并监控容量。事后把 sort_buffer_size 调到 8MB,并在 /tmp 挂载独立磁盘,容量告警阈值 70%。
坑 10:未限行数的导出任务打满 IO
事故经过:财务月底跑"全量交易明细导出",一条 SQL 拉了 3000 万行。这条 SQL 跑在主库上,IO 利用率 100%,在线业务全部变慢。
-- 事故 SQL
SELECT * FROM account_flow WHERE created_at >= '2026-09-01';
-- 3000 万行,通过网络推给客户端
根因:导出任务没有走从库,没有限流,没有分批;大结果集通过网络传输,同时打满 IO 和网络带宽。
处理:把导出任务切到独立的备份从库,限并发 1;同时改成分批流式导出。
-- 修复:用主键游标分批
SELECT * FROM account_flow
WHERE created_at >= '2026-09-01' AND id > #{last_id}
ORDER BY id
LIMIT 5000;
预防:导出系统必须独立资源池,不能跟在线业务抢;大导出走 INTO OUTFILE 在数据库端直接落文件,不要走应用层。事后专门建了一台"导出从库",只跑报表和导出,跟在线业务物理隔离。
慢查询事故的共性规律
复盘完 10 个案例,会发现它们惊人地相似。把这些共性提炼出来,下次遇到新事故时能快速定位。
| 共性模式 | 典型表现 | 出现频率 |
|---|---|---|
| 索引失效 | 函数包裹列、隐式转换、左前缀 LIKE | 极高 |
| 大结果集 | 没 LIMIT、导出任务、深分页 | 高 |
| N+1 | ORM 循环查库 | 高 |
| 统计信息过期 | JOIN 顺序选错、走错索引 | 中 |
| 大表实时聚合 | COUNT、SUM、GROUP BY 在线跑 | 中 |
| 后台打主库 | 运营报表、导出走主库 | 高 |
这六类问题里,索引失效和后台打主库出现频率最高,占了慢查询事故的一半以上。值班时优先盯这两类:看慢日志里有没有全表扫的 SQL,看主库有没有异常大查询。
下面这段 SQL 是 DBA 值班时每天必跑的巡检,用来提前发现"正在变坏"的 SQL:
-- 巡检:最近 24 小时最慢的 TOP10 SQL 形态(本示例基于 MySQL 8.0)
SELECT DIGEST_TEXT,
COUNT_STAR AS exec_count,
SUM_TIMER_WAIT/1e12 AS total_sec,
AVG_TIMER_WAIT/1e6 AS avg_ms,
SUM_ROWS_EXAMINED AS rows_examined_total
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME NOT IN ('mysql','sys')
AND SUM_TIMER_WAIT > 0
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
-- 重点看 rows_examined_total:如果一条 SQL 平均扫描 10 万行但只返回 10 行,就是索引问题
值班时另一个必看的指标是 Threads_running:如果它从平时的 5 一下涨到 50,说明有大量查询在排队,八成是慢查询打满了 CPU。
值班 Checklist:慢查询事故 15 分钟处置流程
下面这张表是值班工程师遇到"数据库告警"时的标准动作,按时间顺序执行。不要跳步,不要凭感觉。
| 时间 | 动作 | 目的 |
|---|---|---|
| 0~1 分钟 | 看 Grafana:CPU、IO、连接数、慢查询条数 | 判断是整体问题还是单 SQL |
| 1~2 分钟 | SHOW FULL PROCESSLIST 排序 time 列 | 找到跑最久的几条 SQL |
| 2~3 分钟 | SELECT * FROM information_schema.innodb_trx | 找长事务,看持锁情况 |
| 3~5 分钟 | 评估:是 KILL 还是观察? | 看是否影响在线业务 |
| 5~8 分钟 | 决定 KILL,记录 processlist 快照 | 止损 |
| 8~12 分钟 | EXPLAIN 肇事 SQL,分析执行计划 | 找根因 |
| 12~15 分钟 | 临时方案:切从库 / 限流 / 降级 | 保住在线业务 |
-- 一键收集现场快照(本示例基于 MySQL 8.0)
-- 值班时把这段存成存储过程或脚本,告警来了直接跑
SHOW MASTER STATUS;
SHOW REPLICA STATUS\G;
SHOW ENGINE INNODB STATUS\G;
SELECT * FROM information_schema.innodb_trx ORDER BY trx_started;
SELECT * FROM performance_schema.threads WHERE PROCESSLIST_ID IS NOT NULL;
-- 这些输出存下来,复盘时就是第一手资料
关键原则:先止损再查因。不要花 10 分钟分析"这条 SQL 为什么慢",期间业务一直挂着。先 KILL 把 CPU 抢回来,再慢慢分析。
事故复盘报告模板
每次慢查询事故处理完,24 小时内必须写复盘报告。下面这个模板是公司内部通用的,照着填就行。
| 复盘项 | 填写要求 |
|---|---|
| 事故等级 | P0/P1/P2,按影响用户数和时长定 |
| 事故时间线 | 精确到分钟:告警时间、KILL 时间、恢复时间 |
| 影响面 | 多少接口超时、多少客诉、损失估算 |
| 肇事 SQL | 完整 SQL 文本 + EXPLAIN 截图 |
| 根因分析 | 5 Why 法,至少追问 3 层 |
| 临时处理 | 当时做了什么止损操作 |
| 长期改进 | 加索引 / 改 SQL / 补监控 / 补规范,每条都要有 owner 和 deadline |
| 经验教训 | 一句话总结,贴到团队 Wiki |
-- 复盘时必查的几件事:
-- 1. 这条 SQL 是哪个版本上线带进来的?
-- 2. 为什么压测没测出来?
-- 3. 为什么监控没提前告警?
-- 4. 下次同类问题怎么在 5 分钟内发现?
-- 把这四个问题的答案写进报告,复盘才不算白开
报告写完后,存到团队 Wiki 的事故复盘目录,按年月归档。以后新人入职,翻一遍过去半年的事故报告,比看十篇技术博客都有用。
复盘会不要追责,要追"系统为什么没拦住"。一个人写错 SQL 是难免的,但 SQL 审核平台没拦住、监控没告警、值班没及时发现,这才是系统要改的地方。
复盘会一般开 1 小时:前 20 分钟由值班工程师讲时间线,中间 20 分钟大家讨论根因和改进项,最后 20 分钟逐条确认改进项的 owner 和 deadline。会后 24 小时内发出会议纪要,抄送全体相关方。
慢查询相关监控指标速记
值班时盯哪些指标,决定了你是"被告警叫醒"还是"出事后才知道"。
| 指标 | 正常范围 | 告警阈值 | 说明 |
|---|---|---|---|
| Threads_running | < 20 | > 50 | 正在执行的查询数 |
| Threads_connected | < 500 | > 800 | 连接总数 |
| CPU 使用率 | < 60% | > 85% | 主库 CPU |
| IO 利用率 | < 40% | > 80% | 磁盘 IO |
| 慢查询数 / 分钟 | < 10 | > 50 | 慢日志新增条数 |
| 主从延迟 | < 1s | > 10s | 秒级 |
| InnoDB 行锁等待 | < 5/s | > 50/s | 锁等待次数 |
这些指标要接到 Grafana 大屏,值班工程师一眼就能看到哪条线在抖。告警要分级:预警发群消息,紧急打电话。
除了这些"即时"指标,还要看"趋势"指标:慢查询条数的日环比、TOP SQL 形态的变化。如果昨天还没出现的 SQL 形态今天突然进入 TOP10,很可能就是新版本带进来的,要提前介入。
另外还要关注缓冲池命中率:SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%',命中率低于 99% 说明内存不够或者数据量涨了,要考虑加内存或者加从库分流。
速查表:慢查询事故应急流程
下面这张表是本章 10 个事故的浓缩版。值班时贴在显示器边上,遇到告警对着一步步来。
| 时间窗口 | 动作 | 命令 |
|---|---|---|
| 0~1 分钟 | 定位肇事 SQL | SHOW FULL PROCESSLIST |
| 1~3 分钟 | 评估是否 KILL | 看 trx 时长、影响行数 |
| 3~5 分钟 | 紧急止损 | KILL <id> |
| 5~15 分钟 | 拉执行计划 | EXPLAIN 慢 SQL |
| 15~60 分钟 | 修复 | 加索引 / 改写 SQL / 切从库 |
| 24 小时内 | 复盘 | 写事故报告,补监控规则 |
新手值班常见误区
| 误区 | 正确做法 |
|---|---|
| 一上来就重启数据库 | 先 SHOW PROCESSLIST 定位,重启解决不了慢 SQL |
| 慌了手忙脚乱敲一堆命令 | 按 Checklist 一步步来,每一步都有目的 |
| KILL 一条 SQL 就以为完事 | 检查是否还有其他慢 SQL,检查连接池是否恢复 |
| 不存现场快照就清理 | 出事第一时间把 processlist、innodb status 存下来 |
| 复盘会只追责不改进 | 每条改进都要有 owner 和 deadline |
新手最容易犯的错是"重启试试"。重启确实能清掉所有慢 SQL,但也会让所有在途事务回滚、所有连接重连,业务受损更严重。记住:慢查询问题 99% 不需要重启,KILL 肇事 SQL 就够了。
另一个常见误区是"等 DBA 来"。开发同学值班时遇到慢查询告警,不要干等 DBA 介入,自己先 SHOW PROCESSLIST 看一眼,能 KILL 就先 KILL,把影响降到最低。DBA 来了再一起分析根因。
还有一个误区是"只看主库不看从库"。很多慢查询是打到从库的,把从库 IO 打满后,主从延迟上来,在线读请求就受影响。值班时主从都要看,不能只盯主库。
-- KILL 有两种:
-- KILL CONNECTION:断开整个连接,正在执行的事务回滚
-- KILL QUERY:只杀掉当前 SQL,连接保留
-- 一般用 KILL CONNECTION,更彻底
-- 注意:KILL 后 InnoDB 回滚需要时间,期间锁不释放,业务仍会阻塞
-- 不要连续 KILL 多条,否则回滚风暴更糟
什么时候不要 KILL:如果这条 SQL 是大事务的最后一条语句,KILL 后回滚时间可能比让它执行完还长,要权衡。一般规则:已经跑了 1 分钟以内的 SQL,KILL;已经跑了 10 分钟以上的大事务,评估后再决定。
KILL 之后要观察什么:KILL 完不要以为完事了,要观察 CPU、连接数是否回落,业务接口是否恢复。如果 KILL 一条后 CPU 还在高位,说明还有其他慢 SQL,继续找。
自测:如果是你值班
下面 10 道题模拟值班时可能遇到的场景,先自己想答案,再展开参考答案。
题 1:主库 CPU 突然 100%,你的第一步是什么?
<details>
<summary>参考答案</summary>
不要慌,先 SHOW FULL PROCESSLIST 看当前正在跑的所有会话,找到那条 state='Sending data' 或 'Copying to tmp table'、time 最大的 SQL。再查 information_schema.innodb_trx 看长事务。评估影响后决定是直接 KILL 还是观察。整个过程不要超过 2 分钟。
</details>
题 2:一条 UPDATE 漏写 WHERE,锁了全表怎么办?
<details>
<summary>参考答案</summary>
立刻 KILL 这条会话,InnoDB 会回滚;回滚期间业务仍然阻塞,要做好预期。事后用 binlog 闪回被误改的行。长期预防:SQL 审核平台拦截无 WHERE 的写操作,写前先 SELECT COUNT(*) 评估。
</details>
题 3:为什么 WHERE DATE(created_at)='2026-10-01' 会让索引失效?
<details>
<summary>参考答案</summary>
索引存的是 created_at 原始值,对列套函数后 B+ 树无法定位。改写成范围 created_at >= '2026-10-01' AND created_at < '2026-10-02',让索引列本身出现在 WHERE 里,就能走索引区间扫描。
</details>
题 4:深分页 LIMIT 100000,50 为什么慢?怎么改?
<details>
<summary>参考答案</summary>
MySQL 要扫描 100050 行才能返回 50 行,offset 越大丢弃越多。改成游标分页:记住上一页最后一行的 created_at,下一页用 WHERE created_at < ? 直接定位,配合 (user_id, created_at) 联合索引,翻到第几页都是常数成本。
</details>
题 5:大表 COUNT(*) 为什么不能在线跑?
<details>
<summary>参考答案</summary>
InnoDB 因为 MVCC 无法维护全局精确行数,COUNT(*) 必须扫索引。800 万行的 COUNT 就要秒级,高并发下数据库被打满。替代方案:Redis 计数器、预聚合表、EXPLAIN 估算值。精确 COUNT 放到后台低峰跑。
</details>
题 6:为什么后台报表要走从库,不能直接查主库?
<details>
<summary>参考答案</summary>
报表 SQL 通常结果集大、扫描行数多,跟在线小查询抢主库资源会直接拖垮在线业务。从库即使慢一点,也只影响报表用户,不影响在线交易。更进一步,报表和导出应该走独立的"分析从库",跟在线只读从库都要分开。
</details>
题 7:KILL 掉一条慢 SQL 后业务还是没恢复,可能是什么原因?
<details>
<summary>参考答案</summary>
几种可能:1)这条 SQL 已经持锁很久,KILL 后 InnoDB 还要回滚,回滚期间锁不释放;2)还有其他慢 SQL 在跑,只杀了一条;3)连接池被打满,新请求进不来;4)主从延迟导致读从库读到旧数据。要逐个排查,不要 KILL 一条就以为完事。
</details>
题 8:怎么提前发现慢查询,而不是等告警?
<details>
<summary>参考答案</summary>
三件事:1)开慢查询日志,long_query_time 设 200ms;2)用 performance_schema.events_statements_summary_by_digest 每天出 TOP 慢 SQL 报表;3)压测阶段就把所有核心接口的 SQL 跑一遍,超过 100ms 的就优化。事前预防比事后救火重要十倍。
</details>
题 9:EXPLAIN 结果里哪些字段最关键?
<details>
<summary>参考答案</summary>
重点看四个:type(访问类型,至少要 range,最好 ref/const,ALL 就是全表扫)、key(实际用到的索引)、rows(预估扫描行数)、Extra(Using index 是覆盖索引,Using filesort 要警惕,Using temporary 要警惕)。type=ALL 且 rows=百万级,这条 SQL 一定要优化。
</details>
题 10:慢查询事故后,DBA、开发、产品各自该做什么?
<details>
<summary>参考答案</summary>
DBA:补索引、补监控、补巡检脚本;开发:改 SQL、补单测、把这条 SQL 加进 SQL 审核平台规则;产品:评估是否可以降级(比如去掉非核心字段、限制翻页深度)。三方一起改,才能保证下次同类问题不再发生。
</details>
自测评分标准:10 道题答对 8 道以上,说明你已经能独立处理慢查询事故;答对 5~7 道,建议再复习一遍第 13、14、21 章;答对 5 道以下,先不要值班,跟资深工程师跟一次真实事故再说。
一句话总结
慢查询事故的应急口诀:先 KILL 止损,再 EXPLAIN 找原因,最后加索引/改 SQL/切从库。不要一上来就重启数据库、不要一上来就扩容,那都是慌了的表现。冷静地 SHOW PROCESSLIST + EXPLAIN,80% 的慢查询事故 15 分钟内能搞定。
最后送新手值班工程师一句话:数据库不会因为你紧张就变好,但会因为你乱操作而变得更糟。遇到告警先深呼吸,按 Checklist 一步步来,比手忙脚乱敲一堆命令强得多。真正的 DBA 高手,不是能解决多难的问题,而是能在事故现场保持冷静,每一步都做对。
把本章的 Checklist 打印出来,贴在工位上。下次值班遇到慢查询告警,照着一步步走,你会发现事故处理其实没那么可怕。
祝你值班顺利,永远用不上这张表。
(本章完,下一章讲死锁与主从延迟事故)
—— 74 章完 ——
下一篇预告:第 75 章 事故复盘:死锁风暴与主从延迟
文章回复
0 条公开回复