PostgreSQL 教程: EXPLAIN 控制节点

八月 26, 2025

摘要:在本教程中,你将了解在执行计划中可能会看到的各种控制 “节点” 或操作。

目录

Limit

PostgreSQL 用户都会使用 LIMIT,因为它非常简单,但让我们来全面描述一下它。LIMIT 操作会执行其下属操作,并仅返回该操作所返回结果中的前 N 行。通常情况下,在返回结果后它会停止下属操作,但在某些情况下(例如在 PL/PgSQL 函数中),当下属操作返回第一行时,该操作可能已经完成了。

简单示例:

explain analyze select * from pg_class;
                                               QUERY PLAN
---------------------------------------------------------------------------------------------------------
 Seq Scan on pg_class  (cost=0.00..10.92 rows=292 width=203) (actual time=0.008..0.047 rows=295 loops=1)
 Total runtime: 0.096 ms
(2 rows)
 
explain analyze select * from pg_class limit 2;
                                                 QUERY PLAN
-------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.00..0.07 rows=2 width=203) (actual time=0.009..0.010 rows=2 loops=1)
   ->  Seq Scan on pg_class  (cost=0.00..10.92 rows=292 width=203) (actual time=0.008..0.009 rows=2 loops=1)
 Total runtime: 0.045 ms
(3 rows)

正如你所看到的,在第二个示例中使用 LIMIT 使得底层的顺序扫描(Seq Scan)在找到两行数据后立即停止了工作。

Append

该计划只是执行多个下属操作,并将所有返回的行合并为一个结果集返回。

它用于 UNION/UNION ALL 查询:

explain select oid from pg_class
  union all
  select oid from pg_proc
  union all
  select oid from pg_database;
                           QUERY PLAN
-----------------------------------------------------------------
 Append  (cost=0.00..104.43 rows=2943 width=4)
   ->  Seq Scan on pg_class  (cost=0.00..10.92 rows=292 width=4)
   ->  Seq Scan on pg_proc  (cost=0.00..92.49 rows=2649 width=4)
   ->  Seq Scan on pg_database  (cost=0.00..1.02 rows=2 width=4)
(4 rows)

在这里你可以看到,Append 操作对三个表执行了三次扫描,并将所有行合并后返回。

请注意,上面使用的是 UNION ALL。如果我们使用 UNION,结果会是这样:

explain select oid from pg_class
  union
  select oid from pg_proc
  union
  select oid from pg_database;
                              QUERY PLAN
-----------------------------------------------------------------------
 HashAggregate  (cost=141.22..170.65 rows=2943 width=4)
   ->  Append  (cost=0.00..133.86 rows=2943 width=4)
         ->  Seq Scan on pg_class  (cost=0.00..10.92 rows=292 width=4)
         ->  Seq Scan on pg_proc  (cost=0.00..92.49 rows=2649 width=4)
         ->  Seq Scan on pg_database  (cost=0.00..1.02 rows=2 width=4)
(5 rows)

这是因为 UNION 会去除重复的行:在这种情况下,这是通过哈希聚合(HashAggregate)操作来实现的。

InitPlan

当查询中的某部分可以(或必须)在其他所有操作之前计算,并且它不依赖于查询其余部分的任何内容时,就会出现这种计划。

例如,假设你有这样一个查询:

explain select * from pg_class
  where relkind =
    (select relkind from pg_class order by random() limit 1);
                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Seq Scan on pg_class  (cost=13.11..24.76 rows=73 width=203)
   Filter: (relkind = $0)
   InitPlan 1 (returns $0)
     ->  Limit  (cost=13.11..13.11 rows=1 width=1)
           ->  Sort  (cost=13.11..13.84 rows=292 width=1)
                 Sort Key: (random())
                 ->  Seq Scan on pg_class pg_class_1  (cost=0.00..11.65 rows=292 width=1)
(7 rows)

在这种情况下,需要先执行 LIMIT / 排序 / 顺序扫描操作,然后再对 pg_class 执行常规的顺序扫描,因为 PostgreSQL 必须将 relkind 值与子查询返回的值进行比较。

另一方面,如果我这样写:

explain select *, (select length('redrock')) from pg_class;
                         QUERY PLAN
-------------------------------------------------------------
 Seq Scan on pg_class  (cost=0.01..10.93 rows=292 width=203)
   InitPlan 1 (returns $0)
     ->  Result  (cost=0.00..0.01 rows=1 width=0)
(3 rows)

PostgreSQL 会正确地发现子查询的列不依赖于 pg_class 表中的任何数据,因此它只需运行一次,不必为每一行重新计算长度。

当然,你可以有多个初始计划,如下所示:

explain select *, (select length('redrock')) from pg_class
  where relkind =
    (select relkind from pg_class order by random() limit 1);
                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Seq Scan on pg_class  (cost=13.12..24.77 rows=73 width=203)
   Filter: (relkind = $1)
   InitPlan 1 (returns $0)
     ->  Result  (cost=0.00..0.01 rows=1 width=0)
   InitPlan 2 (returns $1)
     ->  Limit  (cost=13.11..13.11 rows=1 width=1)
           ->  Sort  (cost=13.11..13.84 rows=292 width=1)
                 Sort Key: (random())
                 ->  Seq Scan on pg_class pg_class_1  (cost=0.00..11.65 rows=292 width=1)
(9 rows)

不过,有一个重要的点是:单个查询中初始计划的编号是 “全局的”,而不是 “按操作的”。

SubPlan

子计划(SubPlan)与嵌套循环(NestedLoop)有点相似,因为它们都可能被多次调用。

调用子计划是为了计算来自子查询的数据,而该子查询实际上依赖于当前行。

例如:

explain analyze select c.relname, c.relkind,
  (select count(*) from pg_class x where c.relkind = x.relkind)
  from pg_class c;
                                                      QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
 Seq Scan on pg_class c  (cost=0.00..3468.93 rows=292 width=65) (actual time=0.135..26.717 rows=295 loops=1)
   SubPlan 1
     ->  Aggregate  (cost=11.83..11.84 rows=1 width=0) (actual time=0.090..0.090 rows=1 loops=295)
           ->  Seq Scan on pg_class x  (cost=0.00..11.65 rows=73 width=0) (actual time=0.010..0.081 rows=93 loops=295)
                 Filter: (c.relkind = relkind)
                 Rows Removed by Filter: 202
 Total runtime: 26.783 ms
(7 rows)

对于 “pg_class as c” 的扫描所返回的每一行,PostgreSQL 都必须运行子计划,该子计划会检查 pg_class 中有多少行在 relkind 列上具有与当前处理行相同的值。

请注意,“Seq Scan on pg_class x” 行中的 “loops=295”,与前面 “Seq Scan on pg_class c” 节点中的 “rows=295” 是相匹配的。

了解更多

PostgreSQL 优化