EXPLAIN · 读懂 PostgreSQL 的执行计划

EXPLAIN 让优化器摊开它打算怎么执行一条 SQL:走哪个索引、怎么连接、估算多少行;EXPLAIN ANALYZE 真跑一遍给出实测行数与耗时。这是排查慢查询的第一手段——看懂扫描类型、连接方式、估算与实测的偏差,你才知道慢在哪、该加什么索引。

作者 David更新于 第 12 篇(共 17 篇)

EXPLAIN · 读懂 PostgreSQL 的执行计划

前面几章讲了索引、分区、物化视图这些「让查询变快」的手段。但用哪个、有没有生效,不能靠猜——得让数据库自己告诉你。EXPLAIN 就是那扇窗:它让优化器把「我打算怎么执行这条 SQL」摊开给你看。看懂执行计划,是排查慢查询的基本功。

EXPLAIN 家族:从「看计划」到「真跑一遍」

EXPLAIN                      SELECT ...;  -- 只看计划,不执行
EXPLAIN ANALYZE             SELECT ...;  -- 真跑一遍,给出实测行数与耗时
EXPLAIN (ANALYZE, BUFFERS)  SELECT ...;  -- 再加上缓存命中情况
EXPLAIN (FORMAT JSON)       SELECT ...;  -- 机器可读,给工具用

EXPLAIN ANALYZE 会真正执行语句。对 SELECT 无妨,但对 UPDATE / DELETE / INSERT,它是真的改数据!要看写语句的计划又不想改数据,把它包在事务里最后回滚:BEGIN; EXPLAIN ANALYZE UPDATE ...; ROLLBACK;。

一个索引的立竿见影

最能说明问题的,是「加索引前后」同一条查询的计划对比。实测灌了 20000 行订单,查 WHERE user_id = 1。

没有索引时——全表扫:

Seq Scan on ea_orders  (cost=0.00..398.00 rows=19 width=26)
                       (actual time=0.023..0.997 rows=25 loops=1)
  Filter: (user_id = 1)
  Rows Removed by Filter: 19975      ← 为了找 25 行,读了 20000 行,扔掉 19975 行
Execution Time: 1.013 ms

建了 user_id 的 B-tree 索引后:

Bitmap Heap Scan on ea_orders  (cost=4.43..57.84 rows=19 width=26)
                               (actual time=0.038..0.067 rows=25 loops=1)
  -> Bitmap Index Scan on ea_orders_user_idx  (actual rows=25)
       Index Cond: (user_id = 1)
Execution Time: 0.086 ms           ← 1.013ms → 0.086ms,快了十倍

关键信号是那句 Rows Removed by Filter: 19975:数据库为了返回 25 行,白读了近两万行。看到它,基本就是「这里缺个索引」的铁证。加索引后,Bitmap Index Scan 先从索引拿到匹配行的位置,Bitmap Heap Scan 再只去读那 22 个相关数据块——不再全表扫。

读计划:三个必看的数字

执行计划是一棵树,从最内层缩进往外读(内层先执行)。每个节点盯三样:

① 扫描 / 连接类型(节点名) —— 数据是怎么取的、表是怎么连的:

扫描类型含义
Seq Scan全表顺序扫(小表 OK,大表 + 高过滤 = 危险信号)
Index Scan走索引取行,再回表
Index Only Scan只读索引就够(覆盖索引,不回表,最快)
Bitmap Heap Scan索引找到一批行,批量按块读(中等选择度)
连接类型适合
Nested Loop一侧行很少 + 另一侧有索引
Hash Join两侧都较大、等值连接(建哈希表)
Merge Join两侧都已按连接键有序

② rows 估算 vs actual rows 实测 —— 优化器的「预判」准不准:

(cost=... rows=19 ...) (actual ... rows=25 ...)   -- 估 19,实 25,很准

估算靠 ANALYZE 收集的统计信息。如果 rows 和 actual rows 差了几个数量级(估 10 行实际 100 万),说明统计信息陈旧——优化器基于错误预判选了烂计划。解法:ANALYZE 表名 重新收集统计。这是慢查询排查里最常见的一类病根。

③ cost(成本) —— 优化器的选择依据:

cost=0.00..398.00   -- 启动成本..总成本

cost 是优化器内部的相对单位,不是毫秒——它以「顺序读一个页 = 1.0」为基准折算 I/O 与 CPU。两个数是「启动成本..总成本」:启动成本是「吐出第一行前的花费」(比如排序要先读完全部才能出第一行,启动成本就高)。优化器的工作,就是在所有候选计划里挑总成本最低的那个。你要真实耗时,得看 EXPLAIN ANALYZE 里的 actual time 和 Execution Time。

加上 BUFFERS 还能看到 shared hit=148(命中缓存的页)vs read=2(从磁盘读的页)——read 高说明数据没在内存里,可能需要更大的 shared_buffers 或更好的索引。

估算从哪来:统计信息

优化器不是瞎猜。ANALYZE 会采样每张表,把「某列有多少不同值、最常见的值是哪些、数据怎么分布」记进系统目录(pg_statistic)。查 WHERE user_id = 1 时,它据此估「大概匹配多少行」,再决定走索引还是全表扫。

日常里 autovacuum 会自动触发 ANALYZE,多数时候不用管。但大批量导入 / 删除之后统计会瞬间过时——这时手动跑一次 ANALYZE,能避免优化器拿着旧地图选错路。排查「昨天还好好的、今天突然变慢」的查询,先看 EXPLAIN ANALYZE 里估算和实测差多少,往往就是统计过时。

横向对比

能力PostgreSQLMySQLOracleSQL Server
看计划EXPLAINEXPLAINEXPLAIN PLAN执行计划(图形 / 文本)
实测执行EXPLAIN ANALYZEEXPLAIN ANALYZE(8.0.18+)DBMS_XPLAN + 实测Actual Execution Plan
缓存 / IO 明细BUFFERS部分强强
机器可读FORMAT JSON/YAMLFORMAT=JSONXMLXML / 图形

各家都有执行计划工具,思路一致。PostgreSQL 的 EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) 信息密度很高,配合 explain.dalibo.com 这类可视化工具,读起来相当顺手。

应用场景

  • 慢查询定位:线上某接口慢,把它的 SQL 拿去 EXPLAIN ANALYZE,看是全表扫、缺索引,还是估算跑偏。
  • 验证索引是否生效:建了索引却没变快?EXPLAIN 看它到底走没走索引——很多时候是查询写法(函数包裹列、类型不匹配)让索引失效。
  • JOIN 顺序 / 方式调优:大表连接慢,看是不是选了糟糕的 Nested Loop,或统计过时导致连接顺序错。
  • 上线前体检:核心查询上线前先 EXPLAIN 过一遍,别等生产环境全表扫才发现。

小结与下一章

EXPLAIN 是数据库调优的听诊器:Rows Removed by Filter 大 → 缺索引;rows 与 actual 差远 → 统计过时;Seq Scan 大表 → 警惕。学会读这三样,你就从「盲目加索引」进阶到「对症下药」。

规模与性能这一站到此结束。下一站转向另一个维度——把逻辑放进数据库里:用 PL/pgSQL 写函数与触发器,让数据库不只是存数据,还能主动执行逻辑。👉 PL/pgSQL 与触发器:让数据库自己干活。

参考文献

本页目录