PostgreSQL 教程: EXPLAIN 查询计划字段术语表

八月 29, 2025

摘要:在本教程中,你将了解在执行计划中可能出现的各类基础的字段。

目录

EXPLAIN 术语

PostgreSQL 生成的查询执行计划中包含了大量实用信息,但其可读性和理解难度往往较高。

以下是一个简单示例:

explain select * from t order by c;
                          QUERY PLAN
------------------------------------------------------------
 Sort  (cost=813.32..837.48 rows=9664 width=32)
   Sort Key: c
   ->  Seq Scan on t  (cost=0.00..173.64 rows=9664 width=32)

该执行计划表明,PostgreSQL 会先对表 “t” 执行顺序扫描(Seq Scan),然后根据列 “c” 对结果进行排序(Sort)。计划中的数值分别代表每个执行阶段的启动成本(Startup Cost)、总成本(Total Cost)、计划行数(Plan Rows) 和计划行宽(Plan Width)。

我们将为您呈现一份术语表,其中包含查询计划中各操作节点常见的核心字段,以及每个字段含义的详细说明。本术语表会根据生成执行计划时需使用的EXPLAIN命令参数(不同参数对应显示不同字段),划分成多个部分:

查询结构字段

这些字段描述了执行计划的实际执行逻辑:数据库将如何处理数据并返回查询结果。只要适用,无论使用何种参数生成执行计划,这些字段都会显示。

节点类型

表示当前节点执行的操作类型。关于不同操作类型的具体功能,请参考 PostgreSQL 优化关于各类操作的系列教程,以及 PostgreSQL 源代码中的注释,包括节点执行相关代码规划器节点相关代码

子计划

为当前操作提供输入数据的子操作集合。

父子关系

用于说明当前操作为何需要执行,以配合父操作完成任务。有六种可能的父子关系:

  • Outer(外层):最常见的取值,意为 “接收当前操作的输出行作为输入,处理后传递给父操作”。
  • Inner(内层):仅出现在连接操作的第二个子节点中,且该位置的子节点必然显示此值。它代表循环的 “内层” 部分,即 “对于外层操作返回的每一行,通过当前内层操作查找匹配的行”。
  • Member(成员):适用于Append节点、ModifyTable节点的所有子节点,以及 BitmapAnd(位图与)、BitmapOr(位图或)等位图处理节点的子节点。
  • InitPlan(初始计划):用于查询开始前需预先计算的操作,例如查询中引用的常量或公共表表达式(CTE)扫描的结果。
  • Subquery(子查询):表示当前子节点是父操作的子查询。由于 PostgreSQL 始终通过子查询扫描(Subquery Scan)将子查询数据传递给父查询,因此该值仅出现在子查询扫描节点的子节点中。
  • SubPlan(子计划):与Subquery类似,代表一个独立的子查询,但适用于无需通过子查询扫描即可传递数据的场景。

筛选条件(Filter)

若该字段存在,则表示当前操作会通过筛选条件过滤掉部分行。

需要重点注意的是,这里的 “Filter” 是传统意义上的筛选:先读取行(无论是从数据源还是从执行计划中的其他操作),检查是否满足条件,再决定保留或丢弃该行。

虽然它与索引扫描(Index Scan)或仅索引扫描(Index Only Scan)中的 “Index Cond(索引条件)” 功能相似,但实现逻辑完全不同。“Index Cond” 是利用索引根据索引值筛选行,无需检查行数据本身;而 “Filter” 需要读取行数据后再判断。在某些情况下,同一操作节点可能同时包含 “Index Cond” 和 “Filter” 字段。关于索引条件与筛选条件的区别,可参考关于索引效率的教程(聚焦多列索引)。

并行感知

表示当前操作是否会以支持并行执行的特殊模式运行。有些操作需要感知自身处于并行环境中(例如顺序扫描需知道只需扫描表的一部分数据),而另一些操作可在多个工作进程中独立运行,无需感知其他进程的存在。

关系名称(Relation Name)

当前操作读写数据的数据源名称。绝大多数情况下是表名(即使通过索引访问数据也是如此),也可能是物化视图或外部数据源。

别名(Alias)

用于引用关系名称(Relation Name)所对应对象的别名。

估算值字段

启用COSTS参数(默认启用,可手动关闭)后,执行计划的节点会显示这些字段。

总成本(Total Cost)

当前操作及其所有子操作的估算总成本。PostgreSQL 查询规划器通常会为同一查询生成多种可能的执行计划,对每种计划计算一个 “成本值”(该值理论上与实际执行时间正相关),最终选择成本值最小的计划。

需要注意的是,成本值是 “无单位” 的,它并非直接对应时间或磁盘读取次数,仅用于表示操作的相对快慢:成本值越大,操作通常越慢;成本值越小,操作通常越快。若想了解成本计算的具体依据,可参考 PostgreSQL 源代码中与成本计算相关的逻辑

启动成本(Startup Cost)

启动当前操作所需的估算固定开销。与实际启动时间(Actual Startup Time)不同,该值是固定的,不会因返回行数的不同而变化。

计划行数(Plan Rows)

规划器预计当前操作返回的行数,与实际行数(Actual Rows)类似,该值是每轮循环的平均行数。

计划行宽(Plan Width)

规划器预计当前操作返回的每行数据的平均大小,单位为字节。

实际值字段

当启用ANALYZE参数时,PostgreSQL 会实际执行查询并收集真实性能数据,执行计划节点将显示这些字段。

实际循环次数(Actual Loops)

当前操作的执行次数。大多数操作的循环次数为 1,但在以下三种情况下可能大于 1 或等于 0:

  • 部分操作可执行多次,例如嵌套循环(Nested Loop)连接会为外层操作返回的每一行执行一次内层操作。
  • 若某个通常仅执行一次的操作被拆分到多个工作进程中并行执行,每个工作进程的部分操作会被算作一次 “循环”。
  • 当操作无需执行时,循环次数为 0。例如,若计划通过读取某表为内连接侧提供候选行,但外连接侧无匹配行,则该表读取操作可被跳过,循环次数为 0。

实际总时间(Actual Total Time)

当前操作及其所有子操作的实际耗时,单位为毫秒,是每轮循环的平均耗时(保留三位小数)。

该字段可能出现一些看似异常的情况,尤其是在物化(Materialize)节点中。物化操作会将接收的数据存储在内存中,以便多次访问,每次访问都会被算作一次 “循环”;而其数据来源(如某次顺序扫描)通常仅执行一次,示例如下:

{   
  "Node Type": "Materialize",   
  "Actual Loops": 9902,  
  "Actual Total Time": 0.000,  
  "Plans": [{
    "Node Type": "Seq Scan",             
    "Actual Loops": 1,             
    "Actual Total Time": 0.035
  }]
}

从示例可见,物化节点及其子节点顺序扫描 “Seq Scan” 的总耗时(0.035 毫秒)远小于 9902 × 0.0005 = 4.951 毫秒,因此每轮循环的 “实际总时间” 小于 0.0005 毫秒,最终四舍五入为 0.000 毫秒。

这种情况下,物化节点及其子节点的总耗时看似为 0 毫秒,但子节点实际耗时 0.035 毫秒,仿佛物化操作本身耗时为负,这其实是四舍五入导致的误解。由于精度保留到千分位,这类问题通常仅出现在极快的操作中,而非性能瓶颈所在的慢操作。未来若能在执行计划中增加 “总耗时的总和”(而非仅显示每轮平均耗时),可避免此类因循环次数多而导致的误解。

实际启动时间(Actual Startup Time)

当前操作返回第一行数据所需的实际时间,单位为毫秒。

很多用户可能会误以为该值是操作的 “固定启动开销”。例如,若某线性操作的返回行数减半,该值不变,而 “实际总时间” 会减少(总时间与启动时间差值的一半)。但实际情况是,该值取决于操作类型:

  • 对于顺序扫描(需返回表中所有行)等操作,实际启动时间接近 “初始化时间”。
  • 对于某些操作(如对 10000 行数据排序),为返回第一行数据,必须先完成所有数据的排序,因此实际启动时间几乎等于实际总时间,且会随数据量的变化而显著变化。

实际行数(Actual Rows)

当前操作每轮循环返回的实际行数,是所有循环的平均值(四舍五入取整)。

因此,“实际循环次数 × 实际行数” 通常可近似表示操作返回的总行数,但误差可能高达 “循环次数的一半”。多数情况下,这种误差仅会导致操作间的行数出现一两行的差异,但若循环次数极大,可能会造成严重的计算偏差。

被筛选条件移除的行数(Rows Removed by Filter)

通过筛选条件(Filter)移除的行数。与其他 “实际值” 字段类似,该值是每轮循环的平均值,因此也存在精度损失和理解偏差的可能。

了解更多

PostgreSQL 优化