读懂体检报告:EXPLAIN ANALYZE

老板问「到底慢在哪」。你不能再说「感觉是索引问题」--要拿执行计划说话。

学 50 min
练 55 min
盘 15 min
共 120 分钟

学 · 50 min

  1. 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 事务。

  2. 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。

  3. 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=实际

    易错排错性能问题的第一步永远是找「估算和实际差最远」的节点,它常常就是病根。

  4. 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;大数字节点先检查它是不是被循环了。

  5. 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

  1. 挑 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;
  2. 找出一条估算 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=实际
  3. 找一个 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 = '上海';
  4. 对同一条 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
  5. FORMAT JSON 输出一次,观察结构
    参考答案

    输出是棵 JSON 树:Plan 数组嵌 Plan,字段和文本版一一对应,适合程序做自动化分析。psql 里可以 \o plan.json 把输出存成文件再看。

    explain (analyze, format json)
    select count(*) from orders where status = 2;
过关标准 拿到一份陌生执行计划,能在 2 分钟内指出瓶颈在哪个节点、依据是什么。