读懂体检报告:EXPLAIN ANALYZE
老板问「到底慢在哪」。你不能再说「感觉是索引问题」--要拿执行计划说话。
学 · 50 min
01EXPLAIN (ANALYZE, BUFFERS, VERBOSE) 各选项作用
场景本周主力工具,先把每个开关是干什么的记牢。
裸
explain:只估算不执行;analyze:真执行并报告实际耗时行数(注意 UPDATE / DELETE 会真的执行);buffers:显示读了多少页、命中缓存多少(IO 真相);verbose:显示每个节点输出哪些列;format json:机器可读,程序分析用。explain (analyze, buffers) select * from orders where user_id = '...'; -- 对 UPDATE 想安全地 analyze:包在事务里回滚 begin; explain (analyze, buffers) update orders set status = 3 where status = 1; rollback;易错
explain analyze一条 DELETE 把数据真删了,是新手经典事故--DML 先包 rollback 事务。02cost=0.00..123.45 两个数字分别是什么
场景计划第一行的这串数字,多数人看了三年没看懂。
cost = 启动成本..总成本:启动成本 = 吐出第一行之前要干的活(排序节点几乎全部成本都在启动段);总成本 = 整个节点干完的账。单位是成本单位不是秒(一次顺序页读 = 1.0 基准),只能用于计划内部比较,不能换算耗时。explain select * from orders order by created_at limit 10; -- Sort 节点:cost=....大数字....(启动贵:排完才有第一行) -- Limit 把下游成本打了折:只需要排够 10 行的量易错「cost 100 万 = 多少毫秒」没有答案;真实耗时看 analyze 的 actual time。
03rows(估算)vs actual rows(实际)偏差意味着什么
场景优化器是个做决策的会计师,账算错了路就选错。
估算 rows 来自统计信息(pg_stats)。偏差 10 倍以上就是警报:优化器按错误行数选 join 算法、选扫描方式,整棵计划树跟着错。常见原因:统计过期(大更新后没 analyze)、列间相关性强(单列统计不懂「上海的用户爱买手机」这种相关)。
explain (analyze) select * from orders o join users u on u.id = o.user_id where o.status = 2 and u.city = '上海'; -- 对比每个节点的 rows=估算 与 actual rows=实际易错排错性能问题的第一步永远是找「估算和实际差最远」的节点,它常常就是病根。
04loops 的含义:实际耗时要乘以 loops
场景一个 0.2ms 的节点,总耗时却是 2 秒--数字没错,你没乘 loops。
嵌套在循环里的节点(典型:Nested Loop 的内侧)会执行多次,
actual time是每次的平均值,真实总耗时 = actual time × loops。看计划不乘 loops,就像看单价不乘数量。explain (analyze) select * from users u join orders o on o.user_id = u.id where u.city = '上海'; -- Index Scan on orders ... (actual time=0.2..0.3 rows=100 loops=10000) -- 0.3ms × 10000 loops = 3 秒 <- 瓶颈在这,不在顶上那个节点易错loops=1 时才可以直接读 actual time;大数字节点先检查它是不是被循环了。
05计划树从下往上、从内到外的读法
场景一份计划几十行,从哪读起?
数据流是自下而上的:缩进最深的节点最先执行,把行喂给上层。读法:先找最内层(叶子扫描节点,看走没走索引),再往外看连接怎么组织,顶上是排序 / 聚合。找瓶颈的口诀:看 analyze 里 actual time 最大的一枝,顺藤摸到它的叶子。
explain (analyze) select u.city, count(*), sum(o.total_amount) from orders o join users u on u.id = o.user_id where o.created_at >= current_date - 7 group by u.city; -- 读的顺序:最深的 Seq/Index Scan -> Hash -> Hash Join -> GroupAggregate易错顶层节点的成本包含所有子节点(累计值),别把父子的时间相加重复计算。
练 · 55 min
- 挑 5 条查询做
EXPLAIN (ANALYZE, BUFFERS),逐行写注释参考答案
注释从缩进最深的节点往上写:叶子扫描走没走索引 -> 连接怎么组织 -> 顶层排序聚合。每个节点记三样:actual time、rows= vs actual rows、buffers。瓶颈 = actual time 最大的一枝。
explain (analyze, buffers) select * from orders where user_id = '00000000-0000-0000-0000-000000000042'; explain (analyze, buffers) select count(*) from orders where status = 2; explain (analyze, buffers) select * from orders order by created_at desc limit 10; explain (analyze, buffers) select u.city, count(*), sum(o.total_amount) from orders o join users u on u.id = o.user_id group by u.city; explain (analyze, buffers) select * from orders o join payments p on p.order_id = o.id where o.total_amount > 1900; - 找出一条估算 rows 与实际相差 10 倍以上的查询,分析原因
参考答案
本库数据是独立随机分布,偏差通常在几倍以内,找不到 10 倍偏差也正常--真实业务里列相关性强(「上海用户爱买手机」单列统计表达不了)或大更新后没 analyze 时才会爆表。排错第一步永远是找「差最远」的节点,它常是病根。
explain (analyze) select * from orders o join users u on u.id = o.user_id where u.city = '上海' and o.status = 1; -- 逐节点对比 rows=估算 与 actual rows=实际 - 找一个 Nested Loop 节点,用 loops 算出它的真实总耗时
参考答案
外层筛出 ≈1600 个上海用户,内侧对 orders 的 Index Scan 跑 ≈1600 次:actual time 是每次的平均值,真实总耗时 = actual time × loops。若优化器选了 Hash Join,set enable_hashjoin = off 再看,看完记得 reset。
explain (analyze) select count(*) from users u join orders o on o.user_id = u.id where u.city = '上海'; - 对同一条 SQL 跑 EXPLAIN 和 EXPLAIN ANALYZE,说出区别
参考答案
前者是优化器的「预算」,后者是「决算」。血泪警告:analyze 会真的执行 DML,update / delete 必须先包 begin ... rollback。
explain select count(*) from orders where status = 2; -- 秒回:只有 cost 和估算 rows,没有执行 explain (analyze) select count(*) from orders where status = 2; -- 真执行:多出 actual time、actual rows、loops - 用
FORMAT JSON输出一次,观察结构参考答案
输出是棵 JSON 树:Plan 数组嵌 Plan,字段和文本版一一对应,适合程序做自动化分析。psql 里可以 \o plan.json 把输出存成文件再看。
explain (analyze, format json) select count(*) from orders where status = 2;