Skip to content
Charles Shao
Go back

MySQL 内核 · 执行计划与慢查询优化

views

索引建了、事务也对了,线上照样会冒出「一条 SQL 把 CPU 打满」。优化器有时选错路,开发有时写出让索引失效的谓词,运营后台则爱 LIMIT 100000,20。本篇站在内核与执行计划一侧,讲清:EXPLAIN 看什么、优化器怎么估价、常见失效与 JOIN/分页怎么改——广告配置库与报表库的日常兵器。

本文是 MySQL 内核系列的第 4 篇(执行计划与慢查询优化)。 全系列 5 篇:

  1. MySQL 内核(开篇)· InnoDB 存储结构与 B+ 树索引
  2. MySQL 内核 · 事务隔离、MVCC 与锁
  3. MySQL 内核 · redo log、undo log 与 binlog
  4. MySQL 内核 · 执行计划与慢查询优化(本篇)
  5. MySQL 内核 · 主从复制与高可用

一句话定位:慢查询治理 = 读懂执行计划 + 让谓词与索引形状匹配 + 控制扫描行数与排序/临时表;先量(慢日志 / EXPLAIN ANALYZE),再改,最后才谈硬件。

TL;DR

Table of contents

Open Table of contents

1. 优化器在干什么

InnoDB 负责存与取页;优化器负责在多条「能算出正确答案」的路径里挑估算代价最低的一条:用哪个索引、表连接顺序、是否物化临时表、是否 filesort 等。依据主要是:

统计过期 → 估错 rows → 选错索引:广告配置表剧烈更新后偶发「计划飘了」,需要 ANALYZE TABLE 或开持久化统计。

从 SQL 经 Parser、Optimizer 到执行计划与 Executor,以及 EXPLAIN 关键列。

EXPLAIN 是估算;EXPLAIN ANALYZE 才给你真实时间与实际行数(MySQL 8.0.18+)。

2. EXPLAIN 实战读法

2.1 type:从好到差的直觉

常见由优到劣大致:system / consteq_refrefrangeindexALL

2.2 key / possible_keys / rows

广告配置表里 status 往往极倾斜(大量下线、少量在线)。优化器若低估/高估过滤性,可能弃用看起来「完美」的联合索引。此时更新统计、加直方图,或把「永远只要在线」写成生成列/分区冷热,比盲目 FORCE INDEX 健康。

2.3 Extra 危险信号

一张「看完 EXPLAIN 下一步干什么」的口诀:

  1. type 太差 → 先造/改索引或改写谓词;
  2. key 为空或离谱 → 查隐式转换、统计、最左前缀;
  3. rows 巨大且 filtered 低 → 过滤条件没推下去;
  4. filesort / temporary → 看能否用索引顺序或缩小排序集;
  5. 一切「好看」仍慢 → 上 EXPLAIN ANALYZE 找真正耗时的算子(往往在回表或网络)。

3. 索引失效与选错

六类高频索引失效/用错场景与对策卡片。

写 SQL 时先问:条件能否「直接贴」在索引列上,还是被函数/隐式转换包住了。

补充几点 AdTech 常见写法:

-- 坏:函数包住列
WHERE DATE(created_at) = '2026-07-14'
-- 好:范围
WHERE created_at >= '2026-07-14' AND created_at < '2026-07-15'

-- 坏:varchar 列与数字比较 → 隐式转换
WHERE campaign_code = 10086
-- 好:
WHERE campaign_code = '10086'

最左前缀忘了,就会出现「有索引却 type=ALL」:idx(a,b)WHERE b=? 爱莫能助(除非 ICP 等特例且仍受限)。选错索引时,可短期 FORCE INDEX,长期应修统计或索引设计。

4. JOIN:算法与原则

Simple/Index/Block Nested-Loop 与 Hash Join,以及深分页坏写法与 keyset/延迟关联对照。

小表(小结果)驱动大表;被驱动表条件必须能走索引。

多表关联配置与计费维表时,确保驱动顺序合理:先过滤出小 campaign 集合,再关联创意,而不是先笛卡儿再 where。

STRAIGHT_JOIN 能强制顺序,只适合救急;长期应靠正确统计与索引让优化器自己选对。子查询在老版本易物化成烂计划,8.0 有不少改善,但仍建议对 Top SQL 用 EXPLAIN ANALYZE 看实际时间落在哪一步。

IN 列表过长(成百上千个 campaign_id)会让优化器成本估计失真,甚至宁愿全表;广告定向圈选这类场景更适合:临时表 / JOIN 一张 id 表,或打散批次,而不是把巨大 IN 糊进 SQL。

5. 深分页与慢日志治理

5.1 OFFSET 的代价

LIMIT 100000, 20 意味着引擎可能拿走 100020 行再丢 100000——越往后越慢。运营后台「跳页到末尾」能直接打穿数据库。

对策:

-- keyset / seek 分页
SELECT * FROM creative
WHERE advertiser_id = ? AND id > ?
ORDER BY id
LIMIT 20;

-- 延迟关联:先索引取 id,再回表
SELECT c.*
FROM (
  SELECT id FROM creative WHERE advertiser_id=? ORDER BY id LIMIT 100000, 20
) t
JOIN creative c ON c.id = t.id;

5.2 慢查询流水线

  1. 打开慢日志(或观察 Performance Schema);
  2. pt-query-digest / 自研聚合找出 Top SQL;
  3. EXPLAIN / EXPLAIN ANALYZE
  4. 改 SQL / 索引 / 归档;
  5. 回归看扫描行与锁等待,而非只看「感觉快了」。

配置变更走主库、重报表走从库或 OLAP,是 读写分离列存 OLAP 的分界——别让分析扫库和决策点查抢同一块 Buffer Pool。

慢日志阈值建议按业务定:决策库可以 100ms 就记;报表库也许 1s。关键是持续看 Top N,而不是只在雪崩时翻日志。把「新增慢 SQL」接进发布门禁(变更后自动对比 digest)能拦住一大批回归。

6. 一张「改之前先自问」清单

对着可疑 SQL,按顺序问:

  1. 有没有合适索引?谓词列、顺序、覆盖?
  2. 谓词有没有破坏索引?函数、隐式转换、前导 %
  3. 扫描行是否合理rowsEXPLAIN ANALYZE 实际行差多少?
  4. 要不要排序/临时表?能否用索引顺序消掉 filesort?
  5. JOIN 驱动顺序?小结果是否在前、被驱动是否 ref/eq_ref?
  6. 分页是否在深翻?能否 keyset?
  7. 是否不该打 OLTP 库?聚合报表是否该去从库 / ClickHouse / Doris?

示例:创意列表慢查询对照。

-- 慢:大 OFFSET + 非覆盖排序
SELECT * FROM creative
WHERE advertiser_id = 42
ORDER BY updated_at DESC
LIMIT 50000, 50;

-- 快:游标分页 + 需要的列
SELECT id, title, status, bid, updated_at
FROM creative
WHERE advertiser_id = 42
  AND (updated_at, id) < (?, ?)   -- 上一页最后一条
ORDER BY updated_at DESC, id DESC
LIMIT 50;
-- 索引例:KEY(advertiser_id, updated_at, id, title, status, bid)

可选武器:optimizer_trace 看为何选/弃某索引;直方图(8.0)改善倾斜列(如大量 status=0)的代价估计。短期 FORCE INDEX 只能当止血,不是长期方案。

计数类查询也要注意:SELECT COUNT(*) 在 InnoDB 不是「免费元数据」,大表精确计数很贵——广告报表的「大概有多少创意」可用近似、汇总表或 OLAP,别拿 OLTP 硬扛实时精确 count。

参考


views
Share this post on:

Previous Post
数据预处理:洗菜择菜的三步——观察、清洗、整理
Next Post
特征工程系统化框架:能用哪些料、料从哪进、料坏了怎么报警