PostgreSQL码农集散地

PostgreSQL 19 动态并行

本期播客

PostgreSQL 19 preview - 支持动态并行

PG 19 支持动态并行扫描了, 这个特性本来是PolarDB PG epq用到的特性 参见:

  • 《一起学PolarDB - 第13期 - 为什么木桶有短板?》
  • 《一起学PolarDB - 第12期 - 为什么增加只读实例不能提高单条SQL的执行速度?》

如今PG 社区版也有了. 其思想和PolarDB一样, 动态分配数据扫描范围, 多劳多得. 不会因为某个并行任务慢而导致整体变慢.

https://github.com/postgres/postgres/commit/0ca3b16973a8bb1c185f56e65edcadc0d9d2c406

Add parallelism support for TID Range Scans    
In v14, bb437f9 added support for scanning for ranges of TIDs using a    
dedicated executor node for the purpose.  Here, we allow these scans to    
be parallelized.  The range of blocks to scan is divvied up similarly to    
how a Parallel Seq Scans does that, where'chunks' of blocks are    
allocated to each worker and the size of those chunks is slowly reduced    
down to 1 block per worker by the time we're nearing the end of the    
scan.  Doing that means workers finish at roughly the same time.    

Allowing TID Range Scans to be parallelized removes the dilemma from the    
planner as to whether a Parallel Seq Scan will cost less than a    
non-parallel TID Range Scan due to the CPU concurrency of the Seq Scan    
(disk costs are not divided by the number of workers).  It was possible    
the planner could choose the Parallel Seq Scan which would result in    
reading additional blocks during execution than the TID Scan would have.    
Allowing Parallel TID Range Scans removes the trade-off the planner    
makes when choosing between reduced CPU costs due to parallelism vs    
additional I/O from the Parallel Seq Scan due to it scanning blocks from    
outside of the required TID range.  There is also, of course, the    
traditional parallelism performance benefits to be gained as well, which    
likely doesn'
t need to be explained here.    

Author: Cary Huang <[email protected]>    
Author: David Rowley <[email protected]>    
Reviewed-by: Junwang Zhao <[email protected]>    
Reviewed-by: Rafia Sabih <[email protected]>    
Reviewed-by: Steven Niu <[email protected]>    
Discussion: https://postgr.es/m/[email protected]    

详细解读

这个补丁(commit 0ca3b16973a8bb1c185f56e65edcadc0d9d2c406)的核心内容是:为 PostgreSQL 增加了对 TID 范围扫描(TID Range Scans)的并行支持(Parallelism Support) 。

TID 范围扫描是一种通过行标识符(TID,即 (block_number, offset))的范围来扫描表数据的方法。

以下是补丁的详细解读:

1. 核心目标和背景

  • 目标: 允许 TID 范围扫描(在 v14 中引入)以并行方式执行,从而提高性能。
  • 背景: 在 PostgreSQL v14 中,引入了专用的执行器节点来支持 TID 范围扫描,用于根据 TID 范围快速访问数据。
  • 解决的痛点: 以前,规划器(planner)在选择执行策略时,可能需要在“并行顺序扫描(Parallel Seq Scan)”和“非并行 TID 范围扫描(Non-parallel TID Range Scan)”之间进行权衡。
    • 并行顺序扫描提供了 CPU 并行优势,但可能会读取比所需 TID 范围更多的磁盘块(额外的 I/O)。
    • 非并行 TID 范围扫描只读取所需块,但缺乏并行带来的 CPU 优势。
    • 引入并行 TID 范围扫描后, 规划器不再需要这种权衡,可以直接获得并行 CPU 优势,同时确保只扫描需要的块,减少了不必要的 I/O。

2. 并行化机制

  • 范围划分: 待扫描的块范围被划分为多个“块”(chunks),然后分配给各个工作进程(worker)。
  • 动态调整: 这种划分方式类似于并行顺序扫描(Parallel Seq Scan):
    • 初始时,分配给每个 worker 的“块”较大。
    • 随着扫描接近结束,块的大小会逐渐减少到每个 worker 1 个块。
  • 目的: 这种动态调整机制是为了确保所有 worker 大致在同一时间完成工作,实现负载均衡。

3. 主要代码更改(涉及 15 个文件)

这个补丁涉及了规划器、执行器、表访问方法和文档等多个层面:

  • 执行器 (src/backend/executor/nodeTidrangescan.c 和 execParallel.c): 实现了并行 TID 范围扫描的逻辑,包括工作进程数据的设置和共享。
  • 规划器 (src/backend/optimizer/path/tidpath.c 和 costsize.c):
    • 在 tidpath.c 中添加了生成 ParallelTidRangeScanPath 的逻辑。
    • 在 costsize.c 中更新了成本模型,以便规划器能够估算和选择并行 TID 范围扫描路径。
  • 表访问方法 (src/backend/access/table/tableam.c 和 heapam.c):
    • 在 tableam.c 中新增了 table_beginscan_parallel_tidrange 函数,用于开始并行 TID 范围扫描。
    • 处理并行扫描描述符 (ParallelTableScanDesc) 的初始化和同步。
  • 数据结构/头文件 (src/include/nodes/execnodes.h 等):
    • 更新了执行节点数据结构,以包含并行扫描所需的字段。
  • 文档 (doc/src/sgml/parallel.sgml):
    • 在并行操作的文档中新增了对“并行 TID 范围扫描”的描述,解释了它是如何将块范围细分并共享给工作进程的。
  • 测试 (test/regress/sql/tidrangescan.sql 和 expected/tidrangescan.out):
    • 增加了回归测试用例,以验证并行 TID 范围扫描的功能和正确性。

4. 结论

这个补丁是对 PostgreSQL 性能优化的一个重要改进,它将 TID 范围扫描(一种高效的块访问方式)与 并行处理(一种高效的 CPU 利用方式)结合起来,为依赖 TID 范围查询的工作负载提供了更优的执行路径,消除了规划器在 I/O 效率和 CPU 效率之间的选择难题。

什么是tid range scan?

TID 范围扫描(TID Range Scan)是一种内部的执行策略,它通常不是直接由用户通过一个特定的 SQL 语句来触发的,而是由 规划器(Planner) 根据查询条件自动选择的执行计划。

1. TID 范围扫描的 SQL 例子

TID 范围扫描通常在 SQL 语句中包含对 ctid(行标识符)或 TID 范围 的过滤条件时被规划器选中。

示例 SQL 语句:

假设我们有一个表 my_table。

EXPLAIN (COSTS OFF)    
SELECT *    
FROM my_table    
WHERE ctid BETWEEN'(10, 1)'::tid AND'(20, 50)'::tid;    

解读:

  1. ctid 是 PostgreSQL 中的一个隐藏系统列,它存储了行在物理存储中的位置,格式是 (块号, 行偏移)。
  2. ' (10, 1) '::tid 表示第 10 个数据块的第 1 行。
  3. ' (20, 50) '::tid 表示第 20 个数据块的第 50 行。
  4. WHERE ctid BETWEEN ... AND ... 明确指定了一个物理位置的范围。

执行计划输出(在支持该功能的版本上):

当运行上述 EXPLAIN 时,如果规划器认为这是最高效的方式,它会选择 Tid Range Scan:

                          QUERY PLAN    
----------------------------------------------------------------    
 Tid Range Scan on my_table    
   Tid Cond: (ctid >= '(10,1)'::tid) AND (ctid <= '(20,50)'::tid)    
(2 rows)    

如果您使用的 PostgreSQL 版本支持并行 TID 范围扫描(如您之前询问的补丁所做),并且查询环境允许并行,执行计划可能会显示为:

                          QUERY PLAN    
----------------------------------------------------------------    
 Parallel Tid Range Scan on my_table    
   ...    

2. TID 范围扫描的实际使用场景

虽然直接使用 ctid 在应用代码中并不常见,但 TID 范围扫描在以下两个主要的内部场景中发挥着关键作用:

场景一:分区表的快速修剪和维护(最重要和最常见的用途)

在处理大型分区表(Partitioned Tables)时,TID 范围扫描非常有用。

  • 问题背景: 当您想要将一个大的分区表进行维护操作(例如,移动数据、逻辑删除、迁移)时,您通常只需要操作表中的一个逻辑子集。如果这个子集对应于存储上的一个或几个连续的块范围,使用常规索引或全表扫描效率很低。
  • 如何应用: 一些高级工具或内部维护任务(如 Logical Decoding 或某些自定义的数据迁移工具)可以通过预先计算或查找得到要操作数据的 TID 范围。然后,它们可以生成一个使用 ctid BETWEEN 的查询来精确且高效地定位和操作这些行,避免扫描不需要的块。

例如,一个自定义的归档脚本:

  1. 脚本确定了需要归档的数据的逻辑范围。
  2. 它通过某种方式(例如,一个临时的、仅包含 ctid 的索引,或者从一个辅助表中读取)快速获取到这些数据对应的 起始 TID 和 结束 TID,例如 (100, 1) 到 (500, 10)。
  3. 脚本执行一个查询:
    DELETEFROM large_partition    
    WHERE ctid BETWEEN'(100, 1)'::tid AND'(500, 10)'::tid;    
    规划器会选择 TID Range Scan,它会直接跳转到块 100 并扫描到块 500,这是极其高效的。

场景二:增量备份和复制(Logical Decoding)

逻辑解码(Logical Decoding)是 PostgreSQL 捕获数据库变更用于复制和审计的关键技术。

  • 在某些实现中,为了精确且有效地定位并提取自上次检查以来发生变化的行,后台进程可能会利用 TID 或 TID 范围来快速定位这些行,而不是依赖其他更慢的扫描方式。

简而言之,TID 范围扫描是一种 物理寻址 优化。它牺牲了逻辑上的抽象性,以换取对数据的最快物理访问速度,主要用于数据库的 内部优化、维护任务和高级工具。