慢查询排查与 EXPLAIN¶
30 秒版(开场)¶
慢 SQL 治理链路:slow log 发现 → EXPLAIN 看计划 → 索引/改写 SQL → 验证 rows 与 latency。重点字段:
type(访问方式)、key(实际索引)、rows(估算扫描行)、Extra(Using filesort/temporary)。生产关键词:回表、深分页、隐式转换、统计信息过期。
3 分钟版(精讲深度)¶
- 是什么:执行时间超过
long_query_time的 SQL 记入 slow log;EXPLAIN 展示优化器选择的访问路径,不执行真实查询。 - 为什么:单条慢 SQL 可拖垮连接池,引发雪崩;80% 性能问题来自 20% SQL。
- 怎么做:开 slow log +
log_queries_not_using_indexes(谨慎);pt-query-digest聚合;EXPLAIN 后加索引或改写法;8.0 用EXPLAIN ANALYZE看真实耗时。
10 分钟版(原理 + 图示)¶
EXPLAIN 核心列
| 列 | 关注 |
|---|---|
| type | 访问方法线索,不应只按固定排行榜判好坏;结合实际 rows、loops 与耗时 |
| key | NULL 表示未走期望索引 |
| rows | 估算行数,乘 join 数爆炸 |
| filtered | 8.0 过滤百分比 |
| Extra | Using filesort/temporary/index condition 预警 |
flowchart TB
Slow[Slow Log] --> Digest[pt-query-digest TopN]
Digest --> EX[EXPLAIN / ANALYZE]
EX --> Fix{根因}
Fix --> Idx[加/改索引]
Fix --> Rewrite[改写 SQL]
Fix --> Stats[ANALYZE TABLE]
Idx --> Verify[压测 / 对比 rows]
高频慢因:缺索引全表扫;联合索引不符合最左;OFFSET 100000 LIMIT 10 深分页;子查询物化;OR 导致 index merge 差;数据倾斜致优化器选错;Buffer pool 不足致大量磁盘读。
EXPLAIN ANALYZE(8.0.18+):真实执行+计时,适合验证优化效果;注意会在从库或低峰跑。
生产场景¶
- 后台列表分页:
ORDER BY id DESC LIMIT 100000,20扫 10 万行 → 改WHERE id < ? ORDER BY id DESC LIMIT 20游标分页。 - JOIN 三表:中间表无索引
type=ALL→ 在 ON/WHERE 列补索引。 - GORM 生成
IN (?,?,...)上千:分批或临时表 join。
排查与工具¶
| 工具 | 用途 |
|---|---|
| slow_query_log | 原始慢 SQL |
| pt-query-digest | 聚合相似 SQL |
| EXPLAIN FORMAT=JSON/TREE | 详细计划 |
| Performance Schema | statement 汇总 |
| mysqldumpslow | 简易统计 |
路径:P99 飙高 → APM 定位 DB → slow log 匹配 → EXPLAIN → 改索引 → 灰度观察 Threads_running。
架构取舍¶
| 方案 | 适用 | 不适用 |
|---|---|---|
| 索引优化 | 大部分 OLTP 慢查 | 分析型全表扫 |
| 读写分离 | 报表拖主库 | 强一致读 |
| 归档冷数据 | 历史表过大 | 实时查全量 |
| ES/ClickHouse | 复杂检索聚合 | 简单 PK 查 |
| 缓存 | 读重复高 | 一致性敏感 |
深挖问答¶
- type=index 和 ALL? → index 扫整棵二级索引树;ALL 全表,通常更差。
- Using filesort 一定慢? → 不一定;它表示不能按目标索引顺序直接返回,需要额外排序,可能完全在内存完成。看实际行数、
EXPLAIN ANALYZE、Sort_merge_passes和临时空间;max_length_for_sort_data在 MySQL 8.0.20 起已废弃且不再生效。 - rows 很大但很快? → 估算偏差,用 ANALYZE 或 ANALYZE TABLE。
- 如何查正在跑的慢 SQL? →
SHOW PROCESSLIST/sys.session。 - Go database/sql 慢? → 查连接池、N+1、未用 context 超时。
反模式与事故¶
- 只在测试库 EXPLAIN,数据量差 1000 倍——上线仍全表扫。
- 加索引不监控写 RT——插入 TPS 腰斩。
- 打开
log_queries_not_using_indexes磁盘打满。 - 用
FORCE INDEX硬怼——统计变化后又慢。
代码示例¶
-- 深分页改写:游标
SELECT id, title FROM posts
WHERE tenant_id = 1 AND id < 99887766
ORDER BY id DESC
LIMIT 20;
-- 8.0 真实耗时
EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 42 AND status = 1;
// Go:带超时的查询,避免慢 SQL 占满连接
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, sql, args...)