# 表访问方法

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

---

<span id="internal-ru-chapter-18" class="pdf-anchor"></span><span id="internal-ru-page-372" class="pdf-anchor"></span>

<span id="internal-ru-dummy-129" class="pdf-anchor"></span>

## 18.1. 可插拔存储引擎 {#internal-ru-section-18-1}

PostgreSQL 中采用的磁盘数据组织方式并非唯一可能的方式，也并非适用于所有负载类型的最佳方式。遵循可扩展性理念，PostgreSQL 允许创建和连接不同的 *表访问方法*（数据存储引擎），尽管目前“开箱即用”仅提供一种：（v. 12）

```text {label="psql"}
=> SELECT amname, amhandler FROM pg_am WHERE amtype = 't';
 amname |      amhandler
--------+----------------------
 heap   | heap_tableam_handler
(1 row)
```

创建表时可以指定引擎名称（`CREATE TABLE ... USING`）；默认使用由参数 *default\_table\_access\_method* 的值决定的引擎。之后可以通过（`ALTER TABLE ... SET ACCESS METHOD`）更改引擎，但代价是重写所有数据。（*默认值*：`heap`）（v. 15）

为了使内核能够以统一方式与不同引擎交互，表访问方法必须实现特定接口[^internal-ru-372-1]。列 `amhandler` 中指定的函数返回一个接口结构[^internal-ru-372-2]，其中包含内核所需的所有信息。

<span id="internal-ru-page-373" class="pdf-anchor"></span>

内核的大部分组件对任何表访问方法都是通用的：

- 事务管理器，包括对 ACID 和基于快照的隔离的支持；

- 缓冲管理器；

- 日志系统，包括通过统一日志记录机制 [^internal-ru-373-1] 或用户自定义资源管理器 [^internal-ru-373-2] 支持特定操作；（v. 15）

- 输入/输出子系统；

- TOAST；

- 查询优化器和执行器；

- 索引支持。

并非所有这些组件都必须被引擎使用，但它们的可用性得以保留。

反过来，引擎定义了：

- 行版本格式和数据结构；

- 表扫描的实现及其成本估算；

- 插入、删除、更新和锁的实现；

- 行版本可见性规则；

- 清理和分析过程。

历史上，PostgreSQL 仅使用一种内置内核的数据存储系统，且没有明确的编程接口。由于这一历史原因，如今很难设计出一个既能充分考虑标准 `heap` 引擎的全部特性，又不阻碍其他方法发展的理想接口。

<span id="internal-ru-page-374" class="pdf-anchor"></span>

因此，我也并未试图严格区分内核与表访问方法。前几章中提到的许多内容，形式上并不属于内核功能，而是 `heap` 方法的特性。很可能该方法将永远作为 PostgreSQL 的通用标准引擎存在，而其他方法则占据特定领域，以更高效地应对特定类型的负载。

表访问方法机制的开发始于以下几种引擎：

**Zheap** 的目标是解决表膨胀问题[^internal-ru-374-1]。为此，它实现了行版本的原地更新，并将构建快照所需的历史数据移至独立的 undo 存储中。该引擎在包含大量数据更新的负载下尤为有用。

该引擎的结构对 Oracle 用户来说可能较为熟悉，但存在一些差异（例如，索引方法接口不允许创建具有自身版本控制的索引）。（*参见* [394](/zh18/19-index-access-methods/#internal-ru-page-395)）

**Zedstore** 实现了列式存储 [^internal-ru-374-2]，应能高效处理 OLAP 查询。

数据以行版本标识符的主 B-树组织，每个列存储在与主树关联的独立 B-树中（未来有望将多个列合并至同一棵树中，以实现混合存储）。

遗憾的是，这些引擎尚未达到工业可用状态；相关工作实际上已停止。目前，其他项目正在积极开发，例如：

**OrioleDB** 是一种针对现代硬件特性和云服务特点设计的引擎[^internal-ru-374-3]。该访问方法将数据存储在以树结构组织的表中，并通过使用回滚日志消除表膨胀。

<span id="internal-ru-page-375" class="pdf-anchor"></span>

由于表访问方法接口尚未充分完善，安装 OrioleDB 需要对 PostgreSQL 内核进行特定修改。

**Citus Columnar** 实现了经典列式存储并支持压缩[^internal-ru-375-1]。该访问方法专为分析型负载和数据仓库设计，不支持更新，并存在其他限制。

## 18.2. 顺序扫描 {#internal-ru-section-18-2}

<span id="internal-ru-dummy-130" class="pdf-anchor"></span>

存储引擎负责表数据的物理组织，并提供访问方式——顺序扫描，即完全读取表主层的文件（或文件集合）。在每一页读取后，检查每一行版本的可见性；不满足查询条件的版本将被丢弃。（*参见* [97](/zh18/04-snapshots/#internal-ru-page-98)）

![顺序扫描：表；页面；行版本；不满足；条件](/images/internal/ru/p375-01.svg)
{#internal-ru-fig-18-1 num="18.1" caption="顺序扫描" .book-original-figure width=659 height=267}

读取通过缓冲区缓存进行；为避免大表驱逐有用数据，顺序扫描使用小尺寸的缓冲环。其他同时扫描同一张表的进程可加入该环，从而节省磁盘读取操作。因此，一般情况下扫描可能并非从文件开头启动。（*参见* [198](/zh18/09-buffer-cache/#internal-ru-page-199)）

<span id="internal-ru-page-376" class="pdf-anchor"></span>

顺序扫描是读取整个表或其大部分内容的最高效方式。换句话说，顺序扫描在低选择性场景下表现良好。（当选择性较高，即仅需表中少量行时，使用索引更为合适。）（*参见* [394](/zh18/19-index-access-methods/#internal-ru-page-395)）

<span id="internal-ru-section-star-239" class="pdf-anchor"></span>

### 成本估算 {#internal-ru-subsection-star-240}

在查询执行计划中，顺序扫描表现为 Seq Scan 节点：

```text {label="psql" hl_lines="4"}
=> EXPLAIN SELECT * FROM flights;
                           QUERY PLAN
----------------------------------------------------------------
 Seq Scan on flights  (cost=0.00..4772.67 rows=214867 width=63)
(1 row)
```

行数估算属于基础统计信息的一部分：

```text {label="psql"}
=> SELECT reltuples FROM pg_class WHERE relname = 'flights';
 reltuples
-----------
    214867
(1 row)
```

优化器在成本估算中考虑两个因素：磁盘 I/O 和 CPU 资源[^internal-ru-376-1]。

<span id="internal-ru-page-377" class="pdf-anchor"></span>

**I/O 成本** 计算为表中页数乘以单页读取成本，*前提是页按顺序读取*。当缓冲管理器向操作系统请求页时，物理上一次从磁盘读取的数据量大于单页，因此极大概率后续几页已进入操作系统缓存。因此，顺序读取单页的成本（对规划器而言由参数 *seq\_page\_cost* 定义）低于随机访问成本（由参数 *random\_page\_cost* 定义）。（*默认值*：`1`）（*默认值*：`4`）

默认值适用于 HDD；对于 SSD 存储，建议显著降低 *random\_page\_cost*（通常不调整 *seq\_page\_cost*，保持其为 1 作为基准值）。由于该比例依赖于硬件特性，这些参数通常在表空间级别设置（`ALTER TABLESPACE ... SET`）。

```text {label="psql"}
=> SELECT relpages,
  current_setting('seq_page_cost') AS seq_page_cost,
  relpages * current_setting('seq_page_cost')::real AS total
FROM pg_class WHERE relname = 'flights';
 relpages | seq_page_cost | total
----------+---------------+-------
     2624 | 1             |  2624
(1 row)
```

该公式清晰地揭示了因未及时清理导致表膨胀的后果：表主层占用空间越大，无论其中实际有效行版本数量多少，都需要扫描的页数就越多。（*参见* [170](/zh18/08-rebuilding-tables-and-indexes/#internal-ru-page-171)）

**CPU 资源估算** 考虑了处理每一行版本的成本（对规划器而言由参数 *cpu\_tuple\_cost* 定义）：（*默认值*：`0.01`）

```text {label="psql"}
=> SELECT reltuples,
  current_setting('cpu_tuple_cost') AS cpu_tuple_cost,
  reltuples * current_setting('cpu_tuple_cost')::real AS total
FROM pg_class WHERE relname = 'flights';
 reltuples | cpu_tuple_cost |  total
-----------+----------------+---------
    214867 | 0.01           | 2148.67
(1 row)
```

上述两项估算之和即为计划的总成本。初始成本为零，因为顺序扫描无需执行预处理操作。

<span id="internal-ru-page-378" class="pdf-anchor"></span>

若对扫描表施加了条件，这些条件将在查询计划的 Seq Scan 节点的 Filter 部分显示。行数估算将考虑这些条件的选择性，成本估算将包含其计算开销。（*参见* [346](/zh18/17-statistics/#internal-ru-page-347)）

命令 `EXPLAIN ANALYZE` 将输出实际获取的行数以及被条件过滤掉的行数：

```text {label="psql" hl_lines="7 8 10"}
=> EXPLAIN (analyze, timing off, summary off, buffers off)
SELECT * FROM flights
WHERE status = 'Scheduled';
                   QUERY PLAN
------------------------------------------------
 Seq Scan on flights
   (cost=0.00..5309.84 rows=15383 width=63)
   (actual rows=15383.00 loops=1)
   Filter: ((status)::text = 'Scheduled'::text)
   Rows Removed by Filter: 199484
(5 rows)
```

考虑一个稍复杂的带聚合的执行计划示例：

```text {label="psql" hl_lines="4"}
=> EXPLAIN SELECT count(*) FROM seats;
                          QUERY PLAN
--------------------------------------------------------------
 Aggregate  (cost=28.74..28.75 rows=1 width=8)
   -> Seq Scan on seats  (cost=0.00..25.39 rows=1339 width=0)
(2 rows)
```

该计划包含两个节点：上层的 Aggregate 节点负责计算 `count` 函数，其数据来自下层的 Seq Scan 节点，后者执行表扫描。

Aggregate 节点的初始成本包括聚合本身：无法在未获取所有下层节点数据的情况下输出第一（也是唯一）行结果。该成本基于对每条输入行执行条件操作的成本 *cpu\_operator\_cost* 进行估算 [^internal-ru-378-1]：（*默认值*：`0.0025`）

<span id="internal-ru-page-379" class="pdf-anchor"></span>

```text {label="psql"}
=> SELECT reltuples,
  current_setting('cpu_operator_cost') AS cpu_operator_cost,
  round((
    reltuples * current_setting('cpu_operator_cost')::real
  )::numeric, 2) AS cpu_cost
FROM pg_class WHERE relname = 'seats';
 reltuples | cpu_operator_cost | cpu_cost
-----------+-------------------+----------
      1339 | 0.0025            |     3.35
(1 row)
```

该估算成本将累加至 Seq Scan 节点的总成本中。

Aggregate 节点的总成本额外包含了处理一行结果的成本 *cpu\_tuple\_cost*：（默认值：`0.01`）

```text {label="psql"}
=> WITH t(cpu_cost) AS (
  SELECT round((
    reltuples * current_setting('cpu_operator_cost')::real
  )::numeric, 2)
  FROM pg_class WHERE relname = 'seats'
)
SELECT 21.39 + t.cpu_cost AS startup_cost,
  round((
    21.39 + t.cpu_cost +
    1 * current_setting('cpu_tuple_cost')::real
  )::numeric, 2) AS total_cost
FROM t;
 startup_cost | total_cost
--------------+------------
        24.74 |      24.75
(1 row)
```

在此示例中，成本估算之间的依赖关系可以这样理解：

```text
                 QUERY PLAN
--------------------------------------------
 Aggregate
   (cost=28.74..28.75 rows=1 width=8)
   -> Seq Scan on seats
       (cost=0.00..25.39 rows=1339 width=0)
(4 rows)
```

<span id="internal-ru-page-380" class="pdf-anchor"></span>

## 18.3. 并行计划 {#internal-ru-section-18-3}

<span id="internal-ru-dummy-131" class="pdf-anchor"></span>

PostgreSQL 支持查询的并行执行 [^internal-ru-380-1]。其核心思想是：执行查询的领导者进程通过 postmaster 启动多个工作进程，这些工作进程同时执行计划的并行部分。这些执行结果被传送到领导者进程，由其在 Gather 节点 [^internal-ru-380-2] 汇总。在空闲接收数据的时间段内，领导者进程也可以执行计划的并行部分。

如需可选地禁用领导者进程执行计划的并行部分，可使用参数 *parallel\_leader\_participation*。 （默认值：`on`）

![并行执行计划：顺序部分；计划的并行部分；Gather；并行部分；并行部分；并行部分；计划的并行部分；计划的并行部分；计划的并行部分；工作进程；领导者进程；工作进程](/images/internal/ru/p380-02.svg)
{#internal-ru-fig-18-2 num="18.2" caption="并行执行计划" .book-original-figure width=687 height=266}

显然，进程启动和数据传输需要一定的资源，因此并非每个查询都适合并行执行。

此外，即使在并行执行中，也并非计划中的所有步骤都能同时进行。部分操作仍需由领导者进程单独、顺序地执行。

> [!NOTE]
> PostgreSQL 并未实现另一种理论上可能的并行化模式，即多个进程构成流水线来处理数据（粗略地说，计划中的各个节点由不同的进程执行）。PostgreSQL 开发者认为这种模式效率低下。

<span id="internal-ru-page-381" class="pdf-anchor"></span>

## 18.4. 并行顺序扫描 {#internal-ru-section-18-4}

<span id="internal-ru-dummy-132" class="pdf-anchor"></span>

一个专为并行执行设计的节点示例是 Parallel Seq Scan —— “并行顺序扫描”。

名称听起来有些矛盾（到底是并行还是顺序？），但确实反映了操作的本质。从对表页面文件的访问角度来看，页面是按顺序读取的，顺序与普通顺序扫描相同。然而，读取由多个并行运行的进程完成。这些进程通过专门分配的共享内存区域进行同步，以避免重复读取同一页面。

关键点在于，操作系统看到的是多个进程执行随机读取，而非单一顺序读取。因此，通常能加速顺序读取的预读机制效果不佳。为此，每个进程被分配连续的多个页面进行读取[^internal-ru-381-1]。（v. 14）

单独的并行扫描操作本身意义不大，因为除了常规的页面读取成本外，还增加了进程间数据传输的开销。但如果工作进程对读取的行执行某些处理（例如聚合），则查询的总执行时间可能显著减少。

<span id="internal-ru-section-star-241" class="pdf-anchor"></span>

### 成本估算 {#internal-ru-subsection-star-242}

考虑一个对大表执行聚合的简单查询。执行计划将使用并行化：

<span id="internal-ru-page-382" class="pdf-anchor"></span>

```text {label="psql" hl_lines="6 8 10"}
=> EXPLAIN SELECT count(*) FROM bookings;
                          QUERY PLAN
--------------------------------------------------------------
 Finalize Aggregate  (cost=25483.58..25483.59 rows=1 width=8)
   -> Gather  (cost=25483.36..25483.57 rows=2 width=8)
       Workers Planned: 2
       -> Partial Aggregate
           (cost=24483.36..24483.37 rows=1 width=8)
           -> Parallel Seq Scan on bookings
               (cost=0.00..22284.29 rows=879629 width=0)
(7 rows)
```

Gather 节点以下的节点构成计划的并行部分。该部分在每个工作进程中执行（本例中计划了两个工作进程），也可能在领导者进程中执行（如果未通过参数 *parallel\_leader\_participation* 禁用）。Gather 节点及其上方的节点构成计划的顺序部分，仅在领导者进程中执行。

Parallel Seq Scan 节点表示以并行模式扫描表。rows 字段显示的是 *一个* 进程平均将输出的行数。总共需要三个进程协作（领导者和两个工作进程），但领导者进程不会完全被利用：其贡献随工作进程数量增加而减少[^internal-ru-382-1]。本例中使用了 2.4 的系数。

```text {label="psql"}
=> SELECT reltuples::numeric, round(reltuples / 2.4) AS per_process
FROM pg_class WHERE relname = 'bookings';
 reltuples | per_process
-----------+-------------
   2111110 |      879629
(1 row)
```

Parallel Seq Scan 节点的成本估算方式几乎与顺序扫描相同。收益来自于每个进程处理的行数更少；而 I/O 成本仍被完全计入，因为无论如何都必须逐页读取整个表：

<span id="internal-ru-page-383" class="pdf-anchor"></span>

```text {label="psql"}
=> SELECT round((
  relpages        * current_setting('seq_page_cost')::real +
  reltuples / 2.4 * current_setting('cpu_tuple_cost')::real
)::numeric, 2)
FROM pg_class WHERE relname = 'bookings';
  round
----------
 22284.29
(1 row)
```

下一个节点是 Partial Aggregate —— 它对工作进程获取的数据执行聚合，即本例中统计行数。

聚合成本的估算方式已知，会累加到表扫描成本上：

```text {label="psql"}
=> WITH t(startup_cost) AS (
  SELECT 22284.29 + round((
    reltuples / 2.4 * current_setting('cpu_operator_cost')::real
  )::numeric, 2)
  FROM pg_class WHERE relname = 'bookings'
)
SELECT startup_cost,
  startup_cost + round((
    1 * current_setting('cpu_tuple_cost')::real
  )::numeric, 2) AS total_cost
FROM t;
 startup_cost | total_cost
--------------+------------
     24483.36 |   24483.37
(1 row)
```

<span id="internal-ru-dummy-133" class="pdf-anchor"></span>

下一个节点 Gather 在领导者进程中执行。该节点负责启动工作进程并接收它们的数据。

进程启动成本（与进程数量无关）由参数 *parallel\_setup\_cost* 定义，而每个数据元组在进程间传输的成本由 *parallel\_tuple\_cost* 定义。（默认值：`1000`）（默认值：`0.1`）

<span id="internal-ru-page-384" class="pdf-anchor"></span>

本例中，初始成本（进程启动）占主导，该值被加到 Partial Aggregate 节点的初始成本上。总成本考虑了两行数据的传输，该值与 Partial Aggregate 节点的总成本相加 [^internal-ru-384-1]：

```text {label="psql"}
=> SELECT
  24483.36 + round(
    current_setting('parallel_setup_cost')::numeric,
  2) AS setup_cost,
  24483.37 + round(
    current_setting('parallel_setup_cost')::numeric +
    2 * current_setting('parallel_tuple_cost')::numeric,
  2) AS total_cost;
 setup_cost | total_cost
------------+------------
   25483.36 |   25483.57
(1 row)
```

最后一个节点 Finalize Aggregate 对 Gather 节点从并行进程获取的局部聚合结果进行最终聚合。

最终聚合的初始成本考虑了三行的计算；该值与 Gather 节点的总成本相加（因为结果计算需要所有行）。总成本还增加了输出一行结果的成本。

```text {label="psql"}
=> WITH t(startup_cost) AS (
  SELECT 25483.57 + round((
    3 * current_setting('cpu_operator_cost')::real
  )::numeric, 2)
  FROM pg_class WHERE relname = 'bookings'
)
SELECT startup_cost,
  startup_cost + round((
    1 * current_setting('cpu_tuple_cost')::real
  )::numeric, 2) AS total_cost
FROM t;
 startup_cost | total_cost
--------------+------------
     25483.58 |   25483.59
(1 row)
```

<span id="internal-ru-page-385" class="pdf-anchor"></span>

成本估算之间的依赖关系取决于节点是否在向上传递结果前累积数据。聚合操作必须在收到所有输入行后才能返回结果。因此，聚合的 *初始* 成本基于其下层节点的 *总* 成本。而 Gather 节点在接收到数据后立即向上输出，因此其初始成本取决于下层节点的初始成本，而总成本则取决于下层节点的总成本。

可以这样示意性地理解：

```text
                       QUERY PLAN
---------------------------------------------------------
 Finalize Aggregate
   (cost=25483.58..25483.59 rows=1 width=8)
   -> Gather
       (cost=25483.36..25483.57 rows=2 width=8)
       Workers Planned: 2
       -> Partial Aggregate
           (cost=24483.36..24483.37 rows=1 width=8)
           -> Parallel Seq Scan on bookings
               (cost=0.00..22284.29 rows=879629 width=0)
(9 rows)
```

## 18.5. 并行执行限制 {#internal-ru-section-18-5}

<span id="internal-ru-section-star-243" class="pdf-anchor"></span>

### 后台工作进程数量 {#internal-ru-subsection-star-244}

<span id="internal-ru-dummy-134" class="pdf-anchor"></span>

进程数量受三个参数限制，形成层级结构。同时执行的工作进程最大数量由参数 *max\_worker\_processes* 决定。（默认值：`8`）

但工作进程机制不仅用于查询的并行执行。例如，后台工作进程用于逻辑复制，扩展也可能使用它们。专门用于并行计划的工作进程数量由参数 *max\_parallel\_workers* 限制。（默认值：`8`）

其中，最多有 *max\_parallel\_workers\_per\_gather* 个进程可为一个领导者进程服务。（默认值：`2`）

<span id="internal-ru-page-386" class="pdf-anchor"></span>

选择这些参数值时需考虑：

- 硬件能力 —— 系统必须拥有未被其他任务占用的空闲核心；

- 数据库中存在大表；

- 负载情况 —— 必须执行可能从并行执行中获益的查询。

大多数情况下，这类条件更适合 OLAP 系统，而非 OLTP 系统。

如果规划器估算从表中读取的数据量未超过 *min\_parallel\_table\_scan\_size* 的值，将不会考虑并行扫描。（默认值：`8MB`）

如果未在表的存储参数 *parallel\_workers* 中显式指定进程数量，则按以下公式计算：

$$
1+\left\lfloor\log_3\left(\frac{\text{表大小}}{\mathit{min\_parallel\_table\_scan\_size}}\right)\right\rfloor.
$$

这意味着表大小每增加三倍，就增加一个并行进程。例如，使用默认参数可得以下数值：

| 表，MB | 进程数 |
| --- | --- |
| 8 | 1 |
| 24 | 2 |
| 72 | 3 |
| 216 | 4 |
| 648 | 5 |
| 1944 | 6 |
{#internal-ru-table-18-1 num="18.1" caption="表大小与进程数量" class="wide"}

无论如何，进程数量不能超过 *max\_parallel\_workers\_per\_gather* 的值。

如果查询 19MB 的小表，将计划并启动一个工作进程：

<span id="internal-ru-page-387" class="pdf-anchor"></span>

```text {label="psql" hl_lines="7 8"}
=> EXPLAIN (analyze, costs off, timing off, summary off, buffers off)
SELECT count(*) FROM flights;
                             QUERY PLAN
---------------------------------------------------------------------
 Finalize Aggregate (actual rows=1.00 loops=1)
   -> Gather (actual rows=2.00 loops=1)
       Workers Planned: 1
       Workers Launched: 1
       -> Partial Aggregate (actual rows=1.00 loops=2)
           -> Parallel Seq Scan on flights (actual rows=107433.50...
(6 rows)
```

查询 105MB 的表时，由于 *max\_parallel\_workers\_per\_gather* 限制（默认值：`2`），仅计划两个进程：

```text {label="psql" hl_lines="7 8"}
=> EXPLAIN (analyze, costs off, timing off, summary off, buffers off)
SELECT count(*) FROM bookings;
                             QUERY PLAN
---------------------------------------------------------------------
 Finalize Aggregate (actual rows=1.00 loops=1)
   -> Gather (actual rows=3.00 loops=1)
       Workers Planned: 2
       Workers Launched: 2
       -> Partial Aggregate (actual rows=1.00 loops=3)
           -> Parallel Seq Scan on bookings (actual rows=703703.3...
(6 rows)
```

解除限制后，可计算出三个进程：

```text {label="psql" hl_lines="9 10"}
=> ALTER SYSTEM SET max_parallel_workers_per_gather = 4;
=> SELECT pg_reload_conf();
=> EXPLAIN (analyze, costs off, timing off, summary off, buffers off)
SELECT count(*) FROM bookings;
                             QUERY PLAN
---------------------------------------------------------------------
 Finalize Aggregate (actual rows=1.00 loops=1)
   -> Gather (actual rows=4.00 loops=1)
       Workers Planned: 3
       Workers Launched: 3
       -> Partial Aggregate (actual rows=1.00 loops=4)
           -> Parallel Seq Scan on bookings (actual rows=527777.5...
(6 rows)
```

<span id="internal-ru-page-388" class="pdf-anchor"></span>

如果在执行查询时可用的进程槽位少于计划数量，将仅启动可用数量的工作进程。

将并行进程总数限制为五个，并同时执行两个查询：

```text {label="psql"}
=> ALTER SYSTEM SET max_parallel_workers = 5;
=> SELECT pg_reload_conf();
=> EXPLAIN (analyze, costs off, timing off, summary off, buffers off)
SELECT count(*) FROM bookings;
```

```text {label="psql" hl_lines="8"}
=> EXPLAIN (analyze, costs off, timing off, summary off, buffers off)
SELECT count(*) FROM bookings;
                           QUERY PLAN
-----------------------------------------------------------------
 Finalize Aggregate (actual rows=1.00 loops=1)
   -> Gather (actual rows=3.00 loops=1)
       Workers Planned: 3
       Workers Launched: 2
       -> Partial Aggregate (actual rows=1.00 loops=3)
           -> Parallel Seq Scan on bookings (actual rows=7037...
(6 rows)
```

```text {hl_lines="6"}
                             QUERY PLAN
---------------------------------------------------------------------
 Finalize Aggregate (actual rows=1.00 loops=1)
   -> Gather (actual rows=4.00 loops=1)
       Workers Planned: 3
       Workers Launched: 3
       -> Partial Aggregate (actual rows=1.00 loops=4)
           -> Parallel Seq Scan on bookings (actual rows=527777.5...
(6 rows)
```

尽管每个查询计划了三个工作进程，但其中一个查询仅获得两个空闲槽位。

恢复参数的默认值：

```text {label="psql"}
=> ALTER SYSTEM RESET ALL;
=> SELECT pg_reload_conf();
```

<span id="internal-ru-page-389" class="pdf-anchor"></span><span id="internal-ru-section-star-245" class="pdf-anchor"></span>

### 无法并行化的查询 {#internal-ru-subsection-star-246}

并非每个查询都可并行执行[^internal-ru-389-1]。以下操作无法并行化：

- 修改或锁定数据的查询（`UPDATE`、`DELETE`、`SELECT FOR UPDATE` 等）。

这不适用于以下命令中使用的查询：

**–** `CREATE TABLE AS`、`SELECT INTO`、`CREATE MATERIALIZED VIEW`；

**–** `REFRESH MATERIALIZED VIEW`。（v. 14）

但在这两种情况下，行的插入仍按顺序执行。

- 可能被暂停执行的查询。这包括游标中的查询，包括 PL/pgSQL 中的 `FOR` 循环中的查询。

- 包含标记为 `PARALLEL UNSAFE` 的 *非安全* 函数调用的查询。默认情况下，所有用户定义函数和一小部分标准函数均属于此类。可通过系统目录获取非安全函数列表：

```sql
SELECT * FROM pg_proc WHERE proparallel = 'u';
```

- 在函数内部的查询，当这些函数从并行查询中被调用时（为防止工作进程数量递归式增长）。

这些限制中的部分将在后续 PostgreSQL 版本中被解除。例如，已实现可串行化隔离级别下查询的并行化。（v. 12）

查询可能因多种原因无法以并行模式执行：

- 该查询在任何情况下都无法并行化；

- <span id="internal-ru-page-390" class="pdf-anchor"></span>并行计划被配置参数值禁止（包括因表大小限制而禁止）；

- 并行计划的成本高于顺序计划。

要检查查询在原则上是否可并行化，可临时启用参数 *debug\_parallel\_query*。此时，规划器将在所有可能的情况下构建并行计划：（v. 16）（默认值：`off`）

```text {label="psql" hl_lines="4"}
=> EXPLAIN SELECT * FROM flights;
                           QUERY PLAN
----------------------------------------------------------------
 Seq Scan on flights  (cost=0.00..4772.67 rows=214867 width=63)
(1 row)
```

```text {label="psql"}
=> SET debug_parallel_query = on;
```

```text {label="psql" hl_lines="4"}
=> EXPLAIN SELECT * FROM flights;
                             QUERY PLAN
---------------------------------------------------------------------
 Gather  (cost=1000.00..27259.37 rows=214867 width=63)
   Workers Planned: 1
   Single Copy: true
   -> Seq Scan on flights  (cost=0.00..4772.67 rows=214867 width=63)
(4 rows)
```

现在我们理解了，规划器考虑了并行计划，但选择了在此情况下更高效的顺序扫描。

<span id="internal-ru-section-star-247" class="pdf-anchor"></span>

### 并行限制的查询 {#internal-ru-subsection-star-248}

计划中越大的部分能以并行方式执行，所能获得的性能提升就越大。然而，存在一些操作，虽然总体上不阻碍并行化，但本身只能在主进程的顺序部分执行[^internal-ru-390-1]。换句话说，它们不能出现在 Gather 节点之下的计划树中。

<span id="internal-ru-page-391" class="pdf-anchor"></span><span id="internal-ru-section-star-249" class="pdf-anchor"></span><span id="internal-ru-paragraph-star-250" class="pdf-anchor"></span>

**通用表表达式。** 通用表表达式的查询结果读取无法并行化[^internal-ru-391-1]（计划节点 CTE Scan）：

```text {label="psql" hl_lines="11"}
=> EXPLAIN (costs off)
WITH t AS MATERIALIZED (
  SELECT * FROM flights
)
SELECT count(*) FROM t;
         QUERY PLAN
----------------------------
 Aggregate
   CTE t
     -> Seq Scan on flights
   -> CTE Scan on t
(4 rows)
```

如果通用表表达式未物化，则计划中不包含 CTE Scan 节点，此限制不适用。（v. 12）

但通用表表达式本身完全可以在并行模式下计算，只要这样做有利可图：

```text {label="psql"}
=> EXPLAIN (costs off)
WITH t AS MATERIALIZED (
  SELECT count(*) FROM flights
)
SELECT * FROM t;
                   QUERY PLAN
-------------------------------------------------
 CTE Scan on t
   CTE t
     -> Finalize Aggregate
         -> Gather
             Workers Planned: 1
             -> Partial Aggregate
                 -> Parallel Seq Scan on flights
(7 rows)
```

<span id="internal-ru-paragraph-star-252" class="pdf-anchor"></span><span id="internal-ru-section-star-251" class="pdf-anchor"></span>

**相关子查询。** 一次性计算且在计划中以 InitPlan 节点表示的非相关子查询，可以参与并行计划：（v. 17）

<span id="internal-ru-page-392" class="pdf-anchor"></span>

```text {label="psql" hl_lines="17 18 21 22"}
=> EXPLAIN (costs off)
SELECT * FROM flights f1
WHERE f1.aircraft_code = (
  SELECT '733' -- InitPlan 1
)
UNION ALL
SELECT * FROM flights f2
WHERE f2.aircraft_code = (
  SELECT '320' -- InitPlan 2
);
                          QUERY PLAN
---------------------------------------------------------------
 Gather
   Workers Planned: 2
   -> Parallel Append
       -> Parallel Seq Scan on flights f2
           Filter: ((aircraft_code)::text = (InitPlan 2).col1)
           InitPlan 2
             -> Result
       -> Parallel Seq Scan on flights f1
           Filter: ((aircraft_code)::text = (InitPlan 1).col1)
           InitPlan 1
             -> Result
(11 rows)
```

Parallel Append 节点在此实现 `UNION ALL` 的并行执行。工作进程各自分配一个子查询（如果工作进程数量多于子查询数量，多个进程可共同处理一个子查询）。当所有子查询的数据均已获取后，空闲的进程将接入下一个子查询，直到所有数据合并完成。

而相关子查询（计划节点 SubPlan）则不参与并行计划：

<span id="internal-ru-page-393" class="pdf-anchor"></span>

```text {label="psql" hl_lines="15 16 20 23"}
=> EXPLAIN (costs off)
SELECT * FROM flights f1
WHERE f1.aircraft_code = (
  SELECT f1.aircraft_code -- SubPlan 1
)
UNION ALL
SELECT * FROM flights f2
WHERE f2.aircraft_code = (
  SELECT '320' -- InitPlan 2
);
                          QUERY PLAN
---------------------------------------------------------------
 Append
   -> Seq Scan on flights f1
       Filter: (aircraft_code = (SubPlan 1))
       SubPlan 1
         -> Result
   -> Gather
       Workers Planned: 1
       InitPlan 2
         -> Result
       -> Parallel Seq Scan on flights f2
           Filter: ((aircraft_code)::text = (InitPlan 2).col1)
(11 rows)
```

计划中出现了 Append 节点而非 Parallel Append，其中 `UNION ALL` 操作顺序执行，而并行部分现在仅覆盖第二个子查询，其中仍保留 InitPlan 节点。

<span id="internal-ru-paragraph-star-254" class="pdf-anchor"></span><span id="internal-ru-section-star-253" class="pdf-anchor"></span>

**临时表。** 临时表无法并行扫描，因为它们仅对创建它们的进程可见。临时表的页面操作在本地缓冲区缓存中进行。若允许多个进程访问，需引入类似共享缓存的锁机制，但这会削弱其他优势。（参见 [209](/zh18/09-buffer-cache/#internal-ru-page-210)）（参见 [305](/zh18/15-locks-on-memory-structures/#internal-ru-page-306)）

```text {label="psql"}
=> CREATE TEMPORARY TABLE flights_tmp
AS SELECT * FROM flights;
=> EXPLAIN (costs off)
SELECT count(*) FROM flights_tmp;
          QUERY PLAN
------------------------------
 Aggregate
   -> Seq Scan on flights_tmp
(2 rows)
```

<span id="internal-ru-section-star-255" class="pdf-anchor"></span><span id="internal-ru-paragraph-star-256" class="pdf-anchor"></span>

**有限并行化的函数。** 标记为 `PARALLEL RESTRICTED` 的函数调用只能在顺序部分执行。

此类函数的列表可通过系统目录查询获得：

<span id="internal-ru-page-394" class="pdf-anchor"></span>

```sql
SELECT * FROM pg_proc WHERE proparallel = 'r';
```

将自定义函数标记为 `PARALLEL RESTRICTED`（更不用说 `PARALLEL SAFE`）需格外谨慎，必须仔细研究现有限制[^internal-ru-394-1]。

[^internal-ru-372-1]: [postgrespro.ru/docs/postgresql/18/tableam](<https://postgrespro.ru/docs/postgresql/18/tableam>).
[^internal-ru-372-2]: [include/access/tableam.h](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/include/access/tableam.h;hb=REL_18_STABLE>).
[^internal-ru-373-1]: [postgrespro.ru/docs/postgresql/18/generic-wal](<https://postgrespro.ru/docs/postgresql/18/generic-wal>).
[^internal-ru-373-2]: [postgrespro.ru/docs/postgresql/18/custom-rmgr](<https://postgrespro.ru/docs/postgresql/18/custom-rmgr>).
[^internal-ru-374-1]: [github.com/EnterpriseDB/zheap](<https://github.com/EnterpriseDB/zheap>).
[^internal-ru-374-2]: [github.com/greenplum-db/postgres/tree/zedstore](<https://github.com/greenplum-db/postgres/tree/zedstore>).
[^internal-ru-374-3]: [github.com/orioledb/orioledb](<https://github.com/orioledb/orioledb>).
[^internal-ru-375-1]: [github.com/citusdata/citus/tree/main/src/backend/columnar](<https://github.com/citusdata/citus/tree/main/src/backend/columnar>).
[^internal-ru-376-1]: [backend/optimizer/path/costsize.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/optimizer/path/costsize.c;hb=REL_18_STABLE>), 函数 cost\_seqscan.
[^internal-ru-378-1]: [backend/optimizer/path/costsize.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/optimizer/path/costsize.c;hb=REL_18_STABLE>), 函数 cost\_agg.
[^internal-ru-380-1]: [postgrespro.ru/docs/postgresql/18/parallel-query](<https://postgrespro.ru/docs/postgresql/18/parallel-query>); [backend/access/transam/README.parallel](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/transam/README.parallel;hb=REL_18_STABLE>).
[^internal-ru-380-2]: [backend/executor/nodeGather.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/executor/nodeGather.c;hb=REL_18_STABLE>).
[^internal-ru-381-1]: [backend/access/heap/heapam.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/heap/heapam.c;hb=REL_18_STABLE>), 函数 table\_block\_parallelscan\_startblock\_init 和 table\_block\_parallelscan\_nextpage.
[^internal-ru-382-1]: [backend/optimizer/path/costsize.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/optimizer/path/costsize.c;hb=REL_18_STABLE>), 函数 get\_parallel\_divisor.
[^internal-ru-384-1]: [backend/optimizer/path/costsize.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/optimizer/path/costsize.c;hb=REL_18_STABLE>), 函数 cost\_gather.
[^internal-ru-389-1]: [postgrespro.ru/docs/postgresql/18/when-can-parallel-query-be-used](<https://postgrespro.ru/docs/postgresql/18/when-can-parallel-query-be-used>).
[^internal-ru-390-1]: [postgrespro.ru/docs/postgresql/18/parallel-safety](<https://postgrespro.ru/docs/postgresql/18/parallel-safety>).
[^internal-ru-391-1]: [backend/optimizer/plan/subselect.c](<https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/optimizer/plan/subselect.c;hb=REL_18_STABLE>).
[^internal-ru-394-1]: [postgrespro.ru/docs/postgresql/18/parallel-safety#PARALLEL-LABELING](<https://postgrespro.ru/docs/postgresql/18/parallel-safety#PARALLEL-LABELING>).
