# 规划与执行

> 跟踪查询从解析到执行的过程，阅读 EXPLAIN 输出，并理解基数、代价估计、预备语句和游标。

---

LLMS 索引： [llms.txt](/llms.txt)

---

<span id="qpt-chapter-2" class="pdf-anchor"></span><span id="qpt-page-16" class="pdf-anchor"></span>

## 1. 简单查询协议 {#qpt-section-2-1}

<span id="qpt-item-1" class="pdf-anchor"></span>

先从最简单的情况开始：当你在 `psql` 中输入一条 `SELECT` 命令时会发生什么？查询处理会经历以下几个阶段：

<span id="qpt-item-2" class="pdf-anchor"></span>

1. 解析

<span id="qpt-item-3" class="pdf-anchor"></span>

2. 重写

<span id="qpt-item-4" class="pdf-anchor"></span>

3. 规划（优化）

4. 执行

<span id="qpt-subsection-4" class="pdf-anchor"></span><span id="qpt-section-3" class="pdf-anchor"></span>

### 解析 {#qpt-p16-parsing}

<span id="qpt-subsection-6" class="pdf-anchor"></span><span id="qpt-section-5" class="pdf-anchor"></span>

### 语法分析 {#qpt-p16-syntax-analysis}

第一步是解析：PostgreSQL 将查询文本拆分为标识符、数字、字符串字面量等词法单元，并检查这些词法单元能否组成语法有效的结构。

以一条简单查询为例：

<span id="qpt-page-17" class="pdf-anchor"></span>

```sql
SELECT  schemaname, tablename
FROM pg_tables
WHERE tableowner = 'postgres'
ORDER BY tablename;
```

![查询树将目标列表、FROM 子句、排序子句以及 pg_tables 上的 tableowner 过滤条件分开。](/images/qpt/p017-01.svg)
{#qpt-fig-2-1 num="2.1" caption="语法分析后的查询树" .book-original-figure .d-flex .flex-column .align-items-center width=417 height=161}

在这个阶段，后端会在内存中构建一棵语法解析树（parse tree）。图中展示了这棵树的简化版本。颜色表示查询文本各部分与树中节点之间的大致对应关系。

`RTE` 是范围表项（Range Table Entry）的缩写，这个名称从外部看并不直观。PostgreSQL 在内部使用 RTE 表示表、子查询、连接结果以及 SQL 查询中可能出现的其他行集。

<span id="qpt-subsection-8" class="pdf-anchor"></span><span id="qpt-section-7" class="pdf-anchor"></span>

### 语义分析 {#qpt-p17-semantic-analysis}

接下来是语义分析，在 PostgreSQL 中称为解析分析（parse analysis）。在这个阶段，PostgreSQL 会确定查询引用了哪些数据库对象，以及用户是否有权访问这些对象。

```sql
SELECT  schemaname, tablename
FROM pg_tables
WHERE tableowner = 'postgres'
ORDER BY tablename;
```

<span id="qpt-page-18" class="pdf-anchor"></span>

![pg_tables 的范围表项被解析为对象标识符，并检查访问权限。](/images/qpt/p018-02.svg)
{#qpt-fig-2-2 num="2.2" caption="语义分析与访问控制" .book-original-figure .d-flex .flex-column .align-items-center width=410 height=294}

在解析分析期间，PostgreSQL 会查询系统目录。在这个例子中，它把名称 `pg_tables` 解析为目录中由 `oid` 标识的某个具体视图，同时也会检查该视图上的访问权限。

<span id="qpt-subsection-10" class="pdf-anchor"></span><span id="qpt-section-9" class="pdf-anchor"></span>

### 重写 {#qpt-p18-rewriting}

然后，查询会根据规则进行重写。

其中一个重要的特殊情况是视图：对视图的引用会展开为定义该视图的查询。严格来说，处理过程并不像这个简化序列所暗示的那样泾渭分明，因为视图定义本身也必须经过解析和分析。

对于我们的示例，可以近似地把结果画成下面这样：

<span id="qpt-page-19" class="pdf-anchor"></span>

```sql
SELECT  schemaname, tablename
FROM (
  SELECT ...
  FROM pg_class c
    LEFT JOIN pg_namespace n
      ON n.oid = c.relnamespace
    LEFT JOIN pg_tablespace t
      ON t.oid = c.reltablespace
  WHERE c.relkind = 'r'::"char"
)
WHERE tableowner = 'postgres'
ORDER BY tablename;
```

![pg_tables 视图展开为对 pg_class、pg_namespace 和 pg_tablespace 的查询。](/images/qpt/p019-03.svg)
{#qpt-fig-2-3 num="2.3" caption="查询重写期间展开视图" .book-original-figure .d-flex .flex-column .align-items-center width=410 height=464}

<span id="qpt-page-20" class="pdf-anchor"></span>

这里的 SQL 仅用于说明。重写操作的对象是查询树，而不是替换 SQL 文本。在树中，与视图查询对应的节点会挂接到 `pg_tables` 的 `RTE` 下方。

<span id="qpt-subsection-12" class="pdf-anchor"></span><span id="qpt-section-11" class="pdf-anchor"></span>

### 规划 {#qpt-p20-planning}

接下来 PostgreSQL 会规划查询。

规划器（planner）会考虑执行查询的不同方式，并通过数学模型估计它们的代价。这个模型使用有关数据的信息，称为**统计信息**（statistics）。估计代价最低的候选方案会表示为一棵计划树。

我们的示例对应的计划片段如下：

```text
Sort
  Sort Key: c.relname
  ->  Nested Loop Left Join
        Join Filter: (n.oid = c.relnamespace)
        ->  Seq Scan on pg_class c
              Filter: ((relkind = 'r'::"char") AND pg_ge...
        ->  Seq Scan on pg_namespace n
```

![一个规划好的语句对 pg_class 和 pg_namespace 进行顺序扫描，通过嵌套循环连接两者，再对输出排序。](/images/qpt/p020-04.svg)
{#qpt-fig-2-4 num="2.4" caption="从查询树到执行计划" .book-original-figure .d-flex .flex-column .align-items-center width=417 height=226}

<span id="qpt-page-21" class="pdf-anchor"></span>

`Seq Scan` 从表中读取行，而 `Nested Loop` 连接两个行集。图中的文本表示形式就是 `EXPLAIN` 命令生成的形式。后续章节会详细讨论访问方法和连接方法。

注意，原来的三张表中现在只剩下两张：规划器已经确定其中一张表不是生成结果所必需的，因此将它从计划中移除了。

<span id="qpt-subsection-14" class="pdf-anchor"></span><span id="qpt-section-13" class="pdf-anchor"></span>

### 执行 {#qpt-p21-execution}

最后，执行器运行选定的计划，并将结果返回给客户端。

计划树的工作方式类似一条流水线。下图重新绘制了计划树，只保留其基本结构。

![顺序扫描访问目录数据，嵌套循环连接两者的行，排序节点再按关系名排列结果。](/images/qpt/p021-05.svg)
{#qpt-fig-2-5 num="2.5" caption="执行计划节点" .book-original-figure .d-flex .flex-column .align-items-center width=418 height=184}

执行从树的根节点开始。在这个例子中，根节点是 `Sort`，它向子节点请求数据。获得所需数据后，根节点执行自己的操作并将行向上返回，最终送达客户端。

`Nested Loop` 这样的节点会组合来自不同来源的数据。连接节点向两个子节点请求行，根据连接条件比较这些行，然后把匹配的行对返回给父节点。下方的 `Seq Scan` 节点从对应的表中读取<span id="qpt-page-22" class="pdf-anchor"></span>行，并将行向上交给连接节点。

有些节点必须读取完所需的全部输入后才能返回任何结果。普通排序就是一个例子：它必须先看到输入，才能确定排序后的顺序。

另一些节点可以随着行到达而返回行。例如，表扫描通常可以在读取表的同时产生行。这样就有可能迅速返回结果的前一部分，例如应用程序逐页显示数据时。PostgreSQL 文档把这种执行器机制称为**按需拉取的流水线**（demand-pull pipeline）：父节点向子节点请求下一行，这个请求沿着计划树向下传播。

![查询文本依次经过解析、重写、规划和执行；目录、规则和统计信息为这些阶段提供信息。](/images/qpt/p022-06.svg)
{#qpt-fig-2-6 num="2.6" caption="查询处理阶段" .book-original-figure .d-flex .flex-column .align-items-center width=415 height=208}

## 2. 扩展查询协议 {#qpt-section-2-2}

默认情况下，客户端使用简单查询协议：它向服务器发送一条 SQL 语句，然后对于 `SELECT` 或带有 `RETURNING` 的命令接收结果行。

<span id="qpt-page-23" class="pdf-anchor"></span>

例如：

```sql
SELECT model FROM airplanes WHERE airplane_code = '77W';
      model
------------------
 Boeing 777-300ER
(1 row)
```

使用简单查询协议时，即使同一条查询被反复执行，每次查询仍然会经历上面描述的全部处理阶段。

PostgreSQL 还提供扩展查询协议，把查询处理分成更明确的步骤。在实践中，这使得准备语句和使用游标成为可能。

<span id="qpt-subsection-16" class="pdf-anchor"></span><span id="qpt-section-15" class="pdf-anchor"></span>

### 预备语句 {#qpt-p23-prepared-statements}

为了准备一条语句，客户端向服务器发送一个查询，其中可以包含参数。服务器会解析、分析并重写查询，然后在后端进程中保留预备语句状态。

要执行预备语句，客户端为它的参数提供值。然后，PostgreSQL 可以构建一份考虑了这些具体值的**自定义计划**（custom plan）并执行它。

准备语句可以避免在同一会话中反复执行同一条语句时重复解析、分析和重写。

如果语句没有参数，那么没有必要为每次执行重新规划，因此 PostgreSQL 可以复用一份**通用计划**（generic plan）。

<span id="qpt-page-24" class="pdf-anchor"></span>

如果语句确实有参数，PostgreSQL 起初可以使用自定义计划；当它估计通用计划的平均表现不会显著变差时，之后也可能切换到通用计划。我们会在 [<span class="text-wrap">第六章“统计信息”</span>](/06-statistics/#qpt-chapter-6)中回到这个决策。

预备语句也是避免 SQL 注入的重要手段：来自不可信来源的值可以作为参数单独发送，而不必拼接进 SQL 文本。这种保护适用于作为参数传递的值；对于表名、列名等动态 SQL 结构，仍然必须采用安全方式构造。

准备一条参数化语句：

```sql
PREPARE model(varchar) AS
  SELECT model
  FROM airplanes
  WHERE airplane_code = $1;
PREPARE
```

按名称执行它：

```text
EXECUTE model('CR7');
       model
-------------------
 Bombardier CRJ700
(1 row)
```

在执行时，参数值会被绑定，语句按需进行规划，然后执行。

可以通过 `pg_prepared_statements` 视图检查预备语句：

<span id="qpt-page-25" class="pdf-anchor"></span>

```sql
SELECT * FROM pg_prepared_statements \gx
-[ RECORD 1 ]---+------------------------------
name            | model
statement       | PREPARE model(varchar) AS    +
                |   SELECT model               +
                |   FROM airplanes             +
                |   WHERE airplane_code = $1;
prepare_time    | 2026-09-25 17:14:48.990998+03
parameter_types | {"character varying"}
result_types    | {text}
from_sql        | t
```

使用通用计划的执行次数

```text
generic_plans   | 0
custom_plans    | 1
```

使用自定义计划的执行次数

最后两列显示该语句使用通用计划和自定义计划的次数。当预备语句不再需要时，可以使用 `DEALLOCATE` 将其移除。

`PREPARE`、`EXECUTE` 和 `DEALLOCATE` 是 SQL 命令。其他编程语言以不同方式提供相同的功能，但数据库驱动最终都会使用相同的客户端/服务器协议。

<span id="qpt-subsection-18" class="pdf-anchor"></span><span id="qpt-section-17" class="pdf-anchor"></span>

### 游标 {#qpt-p25-cursors}

游标可以让我们逐步获取查询结果，而不是一次接收所有行。游标很常见，因为应用程序通常使用命令式语言编写，并且经常迭代处理结果集。

打开游标的信息保存在后端进程中。

在 SQL 中声明游标并获取第一行。游标通常存在于事务内部：

<span id="qpt-page-26" class="pdf-anchor"></span>

```sql
BEGIN;
BEGIN
DECLARE c CURSOR FOR SELECT * FROM airplanes;
DECLARE CURSOR
FETCH c;
 airplane_code |      model      | range | speed
---------------+-----------------+-------+-------
 32N           | Aerobus A320neo |  6500 |   830
(1 row)
```

获取第二行（事务结束时游标会自动关闭）：

```sql
FETCH c;
 airplane_code |        model        | range | speed
---------------+---------------------+-------+-------
 339           | Aerobus A330-900neo | 13300 |   870
(1 row)
COMMIT;
COMMIT
```

数据库 API 提供了判断是否还有结果数据的方法，因此应用程序可以在循环中从游标获取数据。

![PREPARE 解析并重写参数化查询；EXECUTE 绑定参数，使用通用计划或自定义计划，并通过执行产生部分结果。](/images/qpt/p026-07.svg)
{#qpt-fig-2-7 num="2.7" caption="预备语句与游标执行" .book-original-figure .d-flex .flex-column .align-items-center width=415 height=202}

<span id="qpt-page-27" class="pdf-anchor"></span>

## 3. 深入了解规划 {#qpt-section-2-3}

规划值得进一步了解，因为它既是查询性能的核心，也是查询处理过程中较为复杂的阶段之一。

<span id="qpt-subsection-20" class="pdf-anchor"></span><span id="qpt-section-19" class="pdf-anchor"></span>

### 规划过程 {#qpt-p27-the-planning-process}

规划器会探索不同的执行计划，估计它们的代价，并从中选择找到的代价最低者。

计划节点的代价取决于正在执行的操作以及它所处理的数据的性质。显然，按顺序从表中读取行与通过索引获取行使用的是不同的代价模型，而正在处理的数据量也非常重要。

估计数据量时，有两个概念尤其重要：

- **基数**（cardinality）：节点产生的行数估计值

- **选择率**（selectivity）：满足某个条件或谓词的输入行所占的比例

估计选择率和基数需要有关数据的信息：表的大小、列中的值分布等等。在实践中，这会把我们引向**统计信息**。PostgreSQL 通过 autovacuum 的自动分析工作以及 `ANALYZE` 命令收集和刷新统计信息。基数错误是造成糟糕计划的最重要原因之一。

基数错误可能来自统计信息不足，可能来自现有统计信息没有描述的相关性，可能来自没有可用有效统计信息的表达式，也可能来自<span id="qpt-page-28" class="pdf-anchor"></span>规划器估计模型的局限。即使基数估计正确，也不能保证代价估计完美——代价模型及其参数都是近似值——但基数是后续几乎每项规划决策的重要输入。

<span id="qpt-subsection-22" class="pdf-anchor"></span><span id="qpt-section-21" class="pdf-anchor"></span>

### 基数估计 {#qpt-p28-cardinality-estimation}

基数估计是递归进行的。为了估计某个节点的基数，PostgreSQL 会先估计其子节点的基数，然后根据节点类型和节点的谓词估计该节点本身会产生多少行。

![这个计划结合两个目录扫描的行数估计和选择率估计、它们的嵌套循环连接以及最后的排序。](/images/qpt/p028-08.svg)
{#qpt-fig-2-8 num="2.8" caption="基数和选择率估计" .book-original-figure .d-flex .flex-column .align-items-center width=415 height=173}

这个过程从叶节点开始，访问方法在叶节点读取数据。估计叶节点需要知道关系的大小以及其谓词的选择率。后续章节会说明这些估计来自哪里。

暂且假设我们可以估计简单谓词的选择率。这样，就可以使用如下公式近似计算复合谓词的选择率：

$$
\begin{aligned}
\operatorname{sel}_{x\ \mathrm{and}\ y} = \operatorname{sel}_x\operatorname{sel}_y
\\
\operatorname{sel}_{x\ \mathrm{or}\ y} = 1 - (1 - \operatorname{sel}_x)(1 - \operatorname{sel}_y)
\end{aligned}
$$

这些公式假定条件 `x` 和 `y` 相互独立。如果谓词之间存在相关性，估计就可能<span id="qpt-page-29" class="pdf-anchor"></span>出错。PostgreSQL 的扩展统计信息可以改善其中一些情况。

接下来是连接。我们已经有了两个输入行集的基数估计，因此还需要估计连接条件的选择率。具体细节暂且留到后面，之后会回到具体示例。

其他节点也可以用类似方式处理，例如聚合。排序通常保持基数不变，而其他操作可能会改变基数。

计划树底部引入的估计误差会向上传播。随着更多操作符依赖错误估计，误差可能会被放大，最终导致规划器选择一个糟糕的计划。

<span id="qpt-subsection-24" class="pdf-anchor"></span><span id="qpt-section-23" class="pdf-anchor"></span>

### 代价估计 {#qpt-p29-cost-estimation}

代价估计同样是递归进行的。为了估计计划子树的代价，PostgreSQL 会从子节点的代价开始，再加上当前节点执行操作的代价。

![同一计划展示每次扫描、嵌套循环和排序的启动代价与总代价。](/images/qpt/p029-09.svg)
{#qpt-fig-2-9 num="2.9" caption="启动代价与总代价估计" .book-original-figure .d-flex .flex-column .align-items-center width=410 height=157}

节点的代价由规划器的数学模型计算得出，该模型使用了为节点估计的基数和其他属性。

<span id="qpt-page-30" class="pdf-anchor"></span>

`EXPLAIN` 展示两个代价组成部分：**启动代价**定义了节点开始产生行之前必须完成的工作量估计值；**总代价**定义了产生全部行的代价估计值。

有些操作几乎不需要启动工作。另一些操作则必须在返回第一行之前完成大量工作。

例如，普通排序必须先读取要排序的输入，才能输出排好序的结果。这样的节点具有非零启动代价，即使消费者只需要最终结果中的一小部分，也必须支付这部分代价。

必须理解，代价反映的是规划器的估计，而且可能与实际执行时间没有关联。可以把代价看作用任意单位表示的量，这些单位本身没有意义。代价的用途只是让规划器能够比较同一查询的不同计划。

<span id="qpt-subsection-26" class="pdf-anchor"></span><span id="qpt-section-25" class="pdf-anchor"></span>

### 选择最佳计划 {#qpt-p30-choosing-the-best-plan}

优化器会尝试在不过度耗费规划时间的前提下找到最佳计划。

对于连接，PostgreSQL 通常使用动态规划：从较小的连接关系构建更大的连接关系，同时丢弃那些按照规划器标准不可能胜出的替代方案。随着关系数量增加，穷举考虑所有连接顺序的代价会变得过高。PostgreSQL 可以通过只考虑部分成对连接方案，或切换到遗传查询优化器（Genetic Query Optimizer，GEQO），来减少需要考虑的计划数量。这又带来了计划不佳的另一种可能来源：问题不在于代价估计错误，而在于最佳计划从未被考虑。在这种情况下，<span id="qpt-page-31" class="pdf-anchor"></span>查询作者有时可以通过约束连接顺序来提供帮助。

对于普通查询，什么样的计划算是“最佳”计划？PostgreSQL 通常会优化获取完整结果的过程，因此总代价占主导地位。

然而，使用游标时，快速获得前几行可能更加重要。参数 `cursor_tuple_fraction`（默认值为 `0.1`）告诉规划器预计会获取游标结果的多少比例。这个值越小，启动代价相对于总代价的影响就越大。

<span id="qpt-subsection-28" class="pdf-anchor"></span><span id="qpt-section-27" class="pdf-anchor"></span>

### 查看查询计划 {#qpt-p31-viewing-a-query-plan}

`EXPLAIN` 会显示查询计划。它是查询性能工作不可或缺的工具，学会正确阅读它十分重要。下面是上面使用过的查询对应的计划：

```sql
EXPLAIN SELECT schemaname, tablename
FROM pg_tables
WHERE tableowner = 'postgres'
ORDER BY tablename;
                         QUERY PLAN
------------------------------------------------------------
 Sort  (cost=22.36..22.37 rows=1 width=128)
   Sort Key: c.relname
   -> Nested Loop Left Join
       (cost=0.00..22.35 rows=1 width=128)
       Join Filter: (n.oid = c.relnamespace)
       -> Seq Scan on pg_class c
           (cost=0.00..21.26 rows=1 width=72)
           Filter: ((relkind = ANY ('{r,p}'::"char"[])) ...
       -> Seq Scan on pg_namespace n
           (cost=0.00..1.04 rows=4 width=68)
(10 rows)
```

<span id="qpt-page-32" class="pdf-anchor"></span>

加入 `analyze` 后，PostgreSQL 会执行查询，并显示实际执行统计信息以及估计值：

```sql
EXPLAIN (analyze, buffers off) SELECT schemaname, tablename
FROM pg_tables
WHERE tableowner = 'postgres'
ORDER BY tablename;
                         QUERY PLAN
------------------------------------------------------------
 Sort  (cost=22.36..22.37 rows=1 width=128)
   (actual time=0.101..0.103 rows=77.00 loops=1)
   Sort Key: c.relname
   Sort Method: quicksort  Memory: 35kB
   -> Nested Loop Left Join
       (cost=0.00..22.35 rows=1 width=128)
       (actual time=0.009..0.080 rows=77.00 loops=1)
       Join Filter: (n.oid = c.relnamespace)
       Rows Removed by Join Filter: 112
       -> Seq Scan on pg_class c
           (cost=0.00..21.26 rows=1 width=72)
           (actual time=0.006..0.039 rows=77.00 loops=1)
           Filter: ((relkind = ANY ('{r,p}'::"char"[])) ...
           Rows Removed by Filter: 382
       -> Seq Scan on pg_namespace n
           (cost=0.00..1.04 rows=4 width=68)
           (actual time=0.000..0.000 rows=2.45 loops=77)
 Planning Time: 0.080 ms
 Execution Time: 0.113 ms
(19 rows)
```

对会修改数据的语句使用 `EXPLAIN ANALYZE` 时要小心：该语句确实会被执行。

在某些平台上，为每个计划节点收集计时信息可能会明显拖慢查询。如果不需要每个节点的计时信息，可以使用 `timing off` 将其禁用。

<span id="qpt-page-33" class="pdf-anchor"></span>

## 参考资源 {#referenced-resources}

- [www.postgresql.org/docs/18/protocol-flow](<https://www.postgresql.org/docs/18/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY>)
- [www.postgresql.org/docs/18/query-path](<https://www.postgresql.org/docs/18/query-path.html>)
- [www.postgresql.org/docs/18/using-explain](<https://www.postgresql.org/docs/18/using-explain.html>)
