Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Database System Concepts (Seventh Edition) - Chapter 32 - PostgreSQL #5

Open
mrdrivingduck opened this issue Aug 22, 2021 · 8 comments
Assignees
Labels
area/database-management-system DBMS source/ebook Online book topic/postgresql PostgreSQL: The World's Most Advanced Open Source Relational Database

Comments

@mrdrivingduck
Copy link
Owner

mrdrivingduck commented Aug 22, 2021

Chapter 32 为 这本书 的网上章节,详细介绍了 PostgreSQL 的技术细节,甚至还介绍了一些内核代码 😁。对这个章节进行学习能够很快建立 PostgreSQL 的世界观。

PostgreSQL 由 UC BerkeleyMichael StoneBraker 教授开发的 POSTGRES 系统发展而来。目前,PostgreSQL 支持:

  • 复杂查询
  • 外键
  • 触发器
  • 视图
  • 事务完整性
  • 全文搜索
  • 有限数据冗余

用户可以使用新的数据类型、函数、运算符、索引来扩展 PostgreSQL。

PostgreSQL 支持大量编程语言 (接口),也支持 JDBC/ODBC 等数据库接口。PostgreSQL 能够运行在所有类 Unix 操作系统下。

@mrdrivingduck mrdrivingduck added area/database-management-system DBMS status/unfinished Status of reading the paper topic/postgresql PostgreSQL: The World's Most Advanced Open Source Relational Database source/ebook Online book labels Aug 22, 2021
@mrdrivingduck mrdrivingduck self-assigned this Aug 22, 2021
@mrdrivingduck
Copy link
Owner Author

32.1 Interacting with PostgreSQL

32.1.1 Interactive Terminal Interfaces

PostgreSQL 内置提供了 psql 作为命令行工具,是用户能够与 PostgreSQL 进行交互:

  • 与 shell 类似,支持变量替换
  • SQL 中可以使用冒号 dereference psql 变量
  • psql 使用 GNU 的 readline 库处理输入,包括 tab 补全的支持

32.1.2 Graphical Interfaces

PostgreSQL 自己本身不带图形接口,但是有很多开源或商业的图形界面。

32.1.3 Programming Language Interfaces

PostgreSQL 提供两套客户端接口:

  • libpq:提供 PostgreSQL 的 C API,是大部分编程语言 API 的底层引擎;支持同步/异步执行 SQL 以及 prepared statement
  • ECPG:C 语言的嵌入式 SQL 预处理器 (什么🐔8⃣️)

@mrdrivingduck
Copy link
Owner Author

32.2 System Architecture

多进程 架构。PostMaster 是中央协调进程,负责系统初始化:

  • 共享内存分配
  • 后台进程启动

也负责系统关闭。另外,还管理与客户端的连接:为每一个新连接的客户端分配一个 backend server 进程。因此 postmaster 进程需要不断监听端口。PostgreSQL 是一个 process-per-connection 模型。

除了 postmaster 和负责对接客户端的 backend server 进程以外,PostgreSQL 还有一些后台工作进程:

  • Background writter:周期性地将 shared buffer 中的脏页写盘
  • Statistic collector:收集统计信息,表的访问次数,表的行数
  • WAL writter:周期性地将 WAL 数据写盘
  • Checkpointer:执行 checkpoint,加快数据库的恢复过程

在内存管理上,分为进程 local memory 和 shared memory。内存中的 buffer pool 被放置在共享内存中,同时需要被多个进程共享的 lock 等信息也会放在共享内存里。由于使用共享内存,因此 PostgreSQL 只能运行在单机上,如果没有第三方的支持。但是现在很多 PostgreSQL 都被构建为 shared-nothing 的并行数据库。

@mrdrivingduck
Copy link
Owner Author

mrdrivingduck commented Aug 22, 2021

32.3 Storage and Indexing

PostgreSQL 依赖于文件系统的文件作为存储,而不是直接使用物理磁盘分区上的 raw 数据。PostgreSQL 维护了一堆 目录 用于存放文件,这些目录被称为 tablespace。每个 PostgreSQL 安装完都会被初始化一个 default tablespace。

这种存储系统的设计导致了一些性能局限 - double buffering。文件块首先被操作系统读取到内核空间的文件系统缓存中,然后再被拷贝到 PostgreSQL 共享内存的 buffer pool 中。另一个局限是 PostgreSQL 的 block 是 8KB 的,与 OS 内核的 block 大小不一致。

但是现在很多企业都使用了外部存储系统,这种存储系统的性能优化独立于数据库,因此 PostgreSQL 可以直接享受这些系统带来的性能提升。

32.3.1 Tables

在 PG 中,表内的元组被存放在所谓 heap file 中。其格式是一个 page header,后续跟上一个数组的 line pointer。Line pointer 中包含元组在 page 内的 offset 和长度。实际的元组从 page 的结尾处开始倒序存放:

image

Heap file 中的每一个记录都由 tuple ID (TID) 标识:

  • 4-byte 的 block ID
  • 2-byte 的 slot ID (line pointer array 的下标)

由于 MVCC 的存在,表中可能会有同一个 tuple 的多个版本,每个元组都有其有效的开始时间和结束时间。删除操作不会立刻删除元组,而是更新元组的结束时间;更新操作不会修改一个元组,而是将旧版本的元组复制一份,旧版本的结束时间被设置为小于新版本的开始时间。无法被任何事务可见的旧元组会在之后被彻底删除,从而引发 page 中有洞。可以对 tuple 进行压实,从而腾出一些空间,同时不影响元组的 TID 值。

物理元组的长度由 page 的大小决定,PG 是无法跨页存储元组的。对于大元组,PG 提供 TOAST 机制,对元组进行压缩,并切分存储到专门的位置。如果压缩后还是存不下,存储在 toast 属性中的数据将会变为只想外部关联 toast 表的指针;在 toast 表中,元组将会被切分为小块 (一页至少能放四块),每块都是独立的一行。

32.3.2 Indices

PG 中的所有索引类型也是用 slotted-page 格式。

32.3.2.1 Index Types

  • B-Tree:默认索引类型,基于 LehmanYao 的 B-Link Tree,一种支持高并发操作的索引结构,能够高效进行等值或范围查询
  • Hash:对简单的等值查询有效
  • Generalized Search Tree (GiST):基于平衡树结构,实现了 WAL 和并发控制,但是关键操作并没有被实现,而是依赖于 access method;也就是说这只是一套模板,用户可以实现特定的 access method 来构建特定功能的索引;比如 R-Tree 索引
  • Space-partitioned GiST (SP-GiST):利用平衡树模板实现一些 in-memory 的非平衡树结构,比如 kd-tree、radix tree 等
  • Generalized Inverted Index (GIN):加快多值元素的查找,比如文本、JSON、数组;其中存放 <key, posting list> 的 pair,posting list 是出现 key 的 row ID 数组;GIN 也可以对特定的数据类型提供特定实现
  • Block Range Index (BRIN):为某些属性天然聚簇的大数据集设计

32.3.2.2 Other Index Variations

  • Multicolumn indices:B-tree、GiST、GIN、BRIN 都支持,最多可以指定 32 个索引列
  • Unique indices:只有 B-tree 可以被定义为唯一索引;PG 自动为主键、唯一约束添加唯一索引
  • Covering indices:包含不属于 search key 的其它列的索引,使用 include 关键字实现,只有 B-tree 支持
  • Indices on expressions:在创建索引时计算表达式,在维护索引时也需要额外计算
  • Operator classes:在索引构建、维护时指定比较函数;可以为特殊的数据类型执行特殊的比较函数
  • Partial indices:只为部分 (选择率高的) 数据建立索引

32.3.2.3 Index Construction

  1. 扫描目标 relation 目前可见的元组
  2. 根据索引列排序
  3. 构建索引结构

这个过程中可能会有并发的元组更新。因此创建索引的过程中会上锁防止一切对元组的更新,这会导致表无法并发更新。create index concurrently 支持创建索引时允许表被并发更新,它将会扫描基表两次:

  1. 第一次扫描,构建初始版本的索引
  2. 第二次扫描,插入所有依旧需要被索引的元组,同时与其它事务同步,保证并发更新的元组也被插入到了索引中

32.3.2.4 Index-Only Scans

只使用索引就可以返回结果,不需要回表。因为 PG 中的索引都是 二级索引,与堆表存放在不同的文件中。如果想要执行 index-only scan,那么查询中只能引用索引中存在的列,且索引类型需要能够支持 index-only scan;或者可以用到 covering indices。

在 PG 中,使用 index-only scan 并不能保证不需要回表,因为 PG 中的元组是多版本的,索引中包含的数据可能对于执行扫描的事务来说不可见。元组的可见性信息存放在堆表中,因此需要进行表扫描得到元组的可见信息。PG 提供了一种优化:对于每一个堆表,维护一个 visiability map (每个 page 只占两 bit,I/O 很小),追踪每一个页中的元组是否对所有事务可见:

  • Vacuum 后,page 被 set
  • Page 内的任一元组被更新后,被 reset

如果页内的元组对所有事务都可见,那么 index-only scan 就不必访问堆表来判断元组是否可见了;否则 index-only scan 需要访问一下堆表,从而退化为 index scan。

32.3.3 Partitioning

分区表能够在查询包含分区列时提高性能,能够只扫描一个或一小部分分区,同时也可以减少批量加载和删除的开销。分区表也使 vacuum 或 reindex 等维护性操作更快。分区表上的索引也比全局索引更小,从而能够 fit in memory。分区类型:

  • Range:范围连续不重叠
  • List:枚举
  • Hash:适合没有天然分区列或数据分布的数据

分区表可以通过 表继承显式分区声明 实现。不支持外键引用、不支持将一个已有的表转换为分区表,以及反向操作。

@mrdrivingduck mrdrivingduck changed the title Database System Concepts (Seventh Edition) Database System Concepts (Seventh Edition) - Chapter 32 - PostgreSQL Aug 23, 2021
@mrdrivingduck
Copy link
Owner Author

32.4 Query Processing and Optimization

32.4.1 Query Rewrite

规则重写。比如视图,将会根据视图定义把视图查询转换为 select 查询。规则全部被保存在 catalog 中。重写首先处理 update/delete/insert,最终处理 select。可能需要重复或递归处理,直到没有任何规则可以被应用为止。

32.4.2 Query Planning and Optimization

每一个 block 都被视为一个隔离的部分产生 plan,顺序为从底向上,从重写后的计划的最里面的子查询开始进行。PG 的优化器大部分是基于代价的,代价模型的输入为 I/O 开销和 CPU 开销。查询的优化可以通过以下两种途径进行:

  • 标准 planner:使用自底向上的动态规划算法进行 join 顺序优化 - System R 优化算法
  • 遗传算法:当表数量较多时,动态规划的代价较高,使用遗传算法来解决旅行商问题

优化的结果是一个 query plan,是一棵关系代数运算符节点的树,节点可以是一元/二元/n 元的。

代价模型的核心是准确估算每一个运算符节点需要处理的元组数量,估算基于每一个 relation 上的统计信息来进行:

  • 表内元组总数
  • 平均元组大小
  • 列基数 (列中独立值的个数)
  • 最常见的列值
  • 行顺序

DBA 需要周期性地运行 analyze 来保持统计信息的有效性。

32.4.3 Query Executor

执行器基于 demand-driven 的流水线模型,每个运算符节点都需要实现迭代器接口:

  • open
  • next
  • rescan
  • close

一些重要概念:

  • Access method:从 disk 中获取数据的方式
    • 顺序扫描:从第一个 block 扫到最后一个 block,返回所有可见元组
    • 索引扫描:给定搜索条件,从堆表中返回匹配元组 - 最坏情况下可能造成堆表的随机访问
    • Bitmap 索引扫描:减少堆表的随机访问,还可以与其它 bitmap 进行位运算
      • 构造一个 bitmap,每位对应一个堆表页,将索引扫描的结果用于 set bitmap 中的 bit
      • 将 bitmap 中被置位的页扫出来,使用索引取出页内所有的匹配元组 - 保证了每个页只会被 fetch 一次
  • Join method:连接方式
    • 有序归并连接
    • 嵌套循环连接
    • Hash 连接
  • 排序
    • 对于较小的表,直接在内存中 quick sort
    • 大表:外部排序,切分为小块后进行 heap sort 并存放在临时文件中,然后 merge sort
  • 聚合
    • 基于排序的聚合
    • 基于 hash 的聚合

32.4.4 Parallel Query Support

利用多 CPU 核心完成查询。由优化器负责决定是否使用并行,以及并行进程的数量。在 PG 11 中,只有 只读查询 可以使用并行。

当使用并行时,backend 进程负责协调:

  • 产生所需数量的 worker
  • 执行非并行的逻辑
  • (可能) 参与并行工作

并行查询计划包含以下两种节点,它们都只会有一个孩子节点:

  • Gather:从 worker 进程收集元组
  • Gather Merge:从 worker 进程 有序 收集元组

协调进程和工作进程通过共享内存区来进行通信。

并行支持的操作:

  • 并行 seq scan / 并行 index/index only scan / 并行 bitmap scan
  • 并行 nestloop join / 并行 hash join / 并行 merge join
  • 并行聚合:每个 worker 产生部分结果,交给 master 进程计算最终结果

32.4.5 Triggers and Constraints

触发器和约束不是在 rewrite 阶段实现,而是在执行器中实现的。

32.4.6 Just-In-Time (JIT) Compilation

在对元组进行选择运算和投影元算时,实际上是通过解释执行的方式实现的。对于 JIT 来说,可以根据运行时的元组数据类型,直接把函数编译为 native code。在 PG 11 中,使用 JIT 来处理 表达式验证tuple deforming (元组的磁盘表示转换为内存表示) 的工作,因为它们都是 per-tuple 的操作,会很频繁。

JIT 对于长时间占用 CPU 的查询较有优势。在优化器中,根据代价估算以及一些阈值决定是否启用 JIT。PG 使用 LLVM 实现 JIT,以动态链接的方式按需加载。PG 11 中默认不开启。

@mrdrivingduck
Copy link
Owner Author

mrdrivingduck commented Aug 25, 2021

32.5 Transaction Management in PostgreSQL

PostgreSQL 的事务管理同时使用以下两种协议:

  • 对于 DML 使用 快照隔离 (snapshot isolation)
  • 对于 DDL 使用 两阶段锁定 (two-phase locking)

32.5.1 Concurrency Control

32.5.1.1 Isolation Levels

SQL 标准定义了三种等级的弱一致性,以允许更高的并发。应用可以根据需要选择一些非最强保证。SQL 标准根据违反串行化的现象来定义 隔离级别

  • 脏读:事务读到另外一个事务未提交的数据,对应隔离级别为 Read Uncommited
  • 不可重复读:一个事务读同一个对象两次,第二次得到的值与第一次不一样,对应隔离级别为 Read Committed
  • 幻读:一个事务重复使用同一个查询条件对集合进行查询,满足条件的行数发生变化,对应隔离级别为 Repeatable Read
  • 串行不一致:一组已提交的事务的状态与依次串行执行事务的状态不一致,对应隔离级别为 Serilizable

PG 用户可以使用四种隔离级别中的任意一种,但是 PG 实际上只实现了三种隔离级别 (后三种),默认隔离级别为 Read Committed。

32.5.1.2 Concurrency Control for DML Commands

快照隔离的具体实现:多版本并发控制 (MVCC)。核心思想:在同一时间维护每一行的不同版本。在执行命令之前,事务只会选择:

  • 位于快照中的行版本
  • 由相同事务的早先命令创建的行版本

MVCC 带来的特性是:读不阻塞写,写不阻塞读。冲突只有可能发生在两个 writer 更新同一行时。光有快照隔离并不能满足串行化。使用 SSI (Serilizable Snapshot Isolation) 可以在保留快照隔离的性能优势的同时,保证串行化。在运行时,SSI 会检测并发事务之间的冲突,并在错误发生时回滚事务。

32.5.1.3 Implementation of MVCC

MVCC 的核心是 元组可见性。对于事务 T 来说,元组可见的必要条件为:

  1. 在事务 T 获取其快照前,创建元组的事务已经 commit
  2. 对元组进行更新的事务 (如果有) 回滚 (修改撤销) / 在 T 获取快照之后开始运行 (未开始) / 当 T 获取快照时处于 active 状态 (未提交)

这两个条件确保每个事务看到的是一致的数据。PostgreSQL 维护了以下状态信息来高效检查元组的可见性:

  • Transaction ID:事务开始时的时间戳,事务 ID
  • pg_xact:记录当前每个事务状态的日志文件 (in progress / abort / committed)
  • 每个 tuple header 内维护的信息
    • xmin:创建元组的事务的 id
    • xmax:更新或删除元组的事务 id,为 null 表示无操作
    • forward-link:指向更新版本的相同逻辑行
  • SnapshotData 结构,记录快照创建时的事务状态:
    • 活跃事务列表
    • xmax:值为当前开启的事务中 id 最大值 + 1

有这些数据后,元组可见的条件可以被改写为:

  1. 元组的 xmin 需要同时满足:
    • pg_xact 中该事务已提交
    • 小于当前事务的 SnapshotData 中的 xmax
    • 不是 SnapshotData 中的活跃事务
  2. 元组的 xmax (如果有) 需要满足以下之一:
    • pg_xact 中该事务已回滚
    • 大于等于 SnapshotData 中的 xmax
    • 是 SnapshotData 中的活跃事务之一

对于 insert 语句,插入一条元组,并将元组的 xmin 初始化为当前事务 id - 这个过程不需要任何并发控制协议,除非 insert 需要检查约束;对于 delete,主要操作时更新元组的 xmax 为当前事务 id;对于 update,除了这一步,还需要创建一个新元组并初始化 xmin 为当前事务 id,并将旧元组的 forward-link 指向新元组。

对于 select/update/delete 来说,需要使用 MVCC 协议,SnapshotData 的获取时机由隔离级别决定:

  • Read committed:在一条语句开始前获取 SnapshotData
  • Serializable:在事务开始时获取 SnapshotData

对于 read committed 下的 update/delete 来说,事务开始时,元组可能已经被其它正在进行的事务更新,那么就需要等待其它事务的结束:

  • 如果其它事务回滚,那么可以直接进行修改
  • 如果其它事务提交,那么需要重新判断条件

对于 serializable 下的 update/delete 来说,如果其它事务提交,那么将无法保证串行化,当前等待事务将会回滚报错。另外,为了解决幻读,SSI 使用了 谓词锁

32.5.1.4 Implications of Using MVCC

存储多版本的行可能导致额外的存储开销。一个行版本可以被删除,当且仅当它无法被任何事务可见。元组的删除是由 vacuum 机制实现的。PG 使用了一个后台进程自动对表进行 vacuum:

  • Vacuum 进程首先扫描表,将所有无法被任何事务可见的行标记为 dead
  • 扫描表的所有索引,移除引用死元组的索引项
  • 重新扫描表,物理删除死元组

可以通过同一个页面的死元组指向活元组的优化,避免修改索引。

Vacuum 的两种模式:

  • Plain:识别死元组,将它们的空间标记为可使用;可与表的读写并发执行
  • Full:把表压实,使其使用尽可能少的 block;需要对表加排他锁

MVCC 更适合读比写多的场合;两阶段锁定更适合写较多的场景。

32.5.1.5 DDL Concurrency Control

MVCC 无法阻拦一些需要独占表的操作。DDL 遵从严格的两阶段锁定协议来对表上锁。DML 一般使用最低级的三种表级锁,这三种锁互相兼容,因为 MVCC 能够实现这些操作的互相保护。

锁被实现在共享内存中的一个 hash 表中,key 为被锁定对象的签名。如果一个事务想要对另一个事务已经上锁的对象上锁,则需要等待。锁等待是由信号量实现的,每个事务与一个信号量相关联。因此锁等待的实现模式是 per-lock-holder 而不是 per-lock-object。每个事务对应一个信号量,而不是每个锁对象对应一个信号量。

32.5.1.6 Locking and Indices

索引使用 page-level 的共享/排他锁,在索引行被读写后立刻释放;另外 hash 索引使用 bucket-level 的共享/排他锁,在整个 bucket 被处理完毕后立刻释放。

32.5.2 Recovery

PostgreSQL 使用基于 WAL 的恢复来保证原子性和持久性。在 PG 中,恢复不需要做 undo,只需要在 pg_xact 中更新事务状态,就可以使这个事务更新的元组不被其它事务可见。唯一可能发生问题的情况是进程发生了 crash,那么它没有机会更新 pg_xact 中的事务状态。PG 的解决方法是在查看 pg_xact 时,判断事务是否在任意一个进程上运行:如果没有,那么说明执行该事务的进程可能已经 crash,将该事务的状态更新为 aborted。

32.5.3 High Availability and Replication

PG 的 HA 集群是通过 WAL 回放实现的。一台机器处于归档模式,另一台机器处于恢复模式。PG 实现了基于文件的日志迁移,虽然性能开销很低,但是可能存在数据丢失的时间窗口。可以通过设置归档周期,强制使归档中的 PG 切换新的 WAL 文件。

流复制能够使备份机器更加 up to date。不需要等待 WAL 文件被写满,就可以异步 (by default) 将操作发送到备份库上。同步备份也是支持的,主库需要等待备库把 WAL 写到磁盘上后,才可以继续下一个操作。这样提升了数据可靠性,但是吞吐量可能会下降。

除了物理备份,PG 还支持逻辑备份。逻辑备份保存逻辑修改操作,而不是物理操作。逻辑备份使用 发布-订阅 模式,主要用于不同平台、不同版本的 PG 数据库之间。

@mrdrivingduck
Copy link
Owner Author

32.6 SQL Variations and Extensions

32.6.1 PostgreSQL Types

PG 具有对一些非标准类型的支持,也支持通过 create type 命令创建类型。

  • Base types:基本类型,也称为 抽象数据类型,内置已经被 C 语言实现
  • Composite types:对应于一个 field 列表以及相应的类型列表,相当于一张表
  • Domains:将基本类型与约束耦合
  • Enumerated types:枚举
  • Pseudotypes
  • Polymorphic types

一些开源贡献的扩展类型:

  • 地理数据类型
  • 全文搜索数据
  • 网络地址 (IP / MAC)
  • Bitmap
  • JSON / XML

32.6.2 Extensibility

PG 将数据库、表、列等信息全部存放在 system catalog 中。其它数据库需要修改硬编码的源代码来扩展新类型,而 PG 只需要向 catalog 中添加行即可。数据类型、函数、access method 等信息也全部存放在 catalog 中。内置类型和扩展类型的唯一区别是:内置类型已经链接在 backend 中,并且被提前注册在了 system catalog 里;而扩展类型则需要被手动注册,动态加载并链接到 backend。

32.6.2.1 Types

组合类型类似表定义的方式,定义好 C 结构体,定义好读写该类型的 C 函数,然后注册到 catalog 中。

32.6.2.2 Functions

PG 支持函数重载。函数可以使用 SQL 实现,或者其它过程语言,或者 C 语言。

32.6.2.3 Index Extensions

添加索引扩展需要提供运算符的定义:

  • Index-method strategies:用于 where 子句中;比如 B-Tree 索引就需要提供 < <= = >= > 五种运算符,而 hash 索引只提供 =
  • Index-method support routines:比如 hash 索引中的 hash 函数

32.6.2.4 Procedural Languages

PL/pgSQL、PL/Tcl、PL/Perl、PL/Python。

@mrdrivingduck
Copy link
Owner Author

32.7 Foreign Data Wrappers

FDW 允许用户连接外部数据源,但可以像数据在数据库内部一样使用。FDW 允许 PG 访问其它关系型数据库、KV 数据库、文件,但大部分扩展都不被 PG 官方支持。PG 官方支持的 FDW 模块有:

  • file_fdw:访问磁盘中的文件,文件格式需要兼容 copy from 命令
  • postgres_fdw:访问其它 PG server - 开启相同的事务隔离级别,并尽可能减少需要进行传输的数据量,比如下推运算符

@mrdrivingduck
Copy link
Owner Author

32.8 PostgreSQL Internals for Developers

32.8.1 Installation From Source Code

32.8.2 Code Organization

  • contrib:移植工具、分析工具、插件
  • src/bin:客户端 (前端) 程序
  • src/pl:对过程语言的支持
  • src/test:回归测试
  • src/backend:PG 后端

后端代码组织:

  • Parser
    • Lexer:词法分析 scan.l
    • Grammar:语法分析 gram.y
    • parse_*.c
    • analyze.c
  • Optimizer
    • /path:决定 join 表的顺序
    • /plan:产生执行计划
    • /geqo:处理大量表连接的遗传优化算法
  • Executor:demand/pull driven 流水线

32.8.3 System Catalogs

扩展功能可以被优雅地实现为向 pg_* 表中插入条目。

32.8.4 The Region-Based Memory Manager

实现了层次化的 memory context,一次调用就可以释放特定 memory context 中所有的对象。这样不仅更加高效,还可以避免内存泄漏的问题。

常见的 memory context 有:

  • CurrentMemoryContext:指向当前 memory context
  • TopMemoryContext:当前 backend 的生命周期
  • CurTransactionContext:当前事务的生命周期
  • PerQueryContext:当前查询的生命周期
  • PerTupleContext:当前元组的生命周期

Memory context 的基本操作:

  • MemoryContextCreate():创建
  • MemoryContextSwitchTo():切换
  • MemoryContextAlloc():在上下文内分配一大块内存
  • MemoryContextDelete():释放并删除当前 memory context 中的所有对象
  • MemoryContextReset():释放所有已分配的对象,但不删除 memory context 自身

其中 reset/delete 将会自动 reset/delete 所有 children memory context。

32.8.5 Node Structure and Node Functions

PG 使用了支持 单继承 的对象系统。所有节点的基类:

typedef struct {
    NodeTag type;
} Node;

所有节点都需要把 Node 放在结构体定义的第一位,这样就有了类似继承的效果。

  • makeNode():创建节点
  • IsA():运行时类型判定
  • equal():判断两个节点是否相等
  • copyObject():深拷贝一个节点
  • nodeToString():序列化一个节点
  • stringToNode():反序列化一个节点

任何节点类型都可以通过指针类型强制转换得到一个 Node * 指针,并能够访问到节点类型。而将基类指针转换为子类指针则需要经过运行时判断 - castNode 会对 NodeTag 进行判断决定是否可以转换;或者在调用 IsA() 之后直接进行转换。

32.8.6 Datum

Datum 是用来存放内部表示的通用数据类型,可以存放值,也可以存放指针。使用 Datum 的代码必须知道它的数据类型,因为 Datum 本身只存放值。内核里提供了很多基本数据类型和 Datum 相互转换的宏。

32.8.7 Tuple

一个 tuple 包含一系列的 Datum。HeapTupleData 是指向元组的内存数据结构,其中包含了元组的长度,以及指向 tuple header 的指针。该指针有如下几种指向元组的方式:

  • 指向一个 pinned 的 buffer page
  • 指向 NULL
  • 被 palloc 的 chunk:指向 HeapTupleData 结构体之后的位置
  • 指向与 HeapTupleData 结构体不相邻的 palloc chunk
  • 指向 minimal tuple 的偏移 (?)

MinimalTupleHeapTuple 的一种替代表示,用于在执行器内部转移元组,其中的一些事务状态信息已经不需要了。这样可以为每个元组省下一些空间。

PG 开发者推荐使用 TupleTableSlot 来访问各种元组,它屏蔽了 tuple 指针指向哪里的细节。

32.8.8 Query Execution Stack

  • Parser:SQL 转换为 raw parse trees,每个 parse trees 被独立地分析和重写,返回一系列 Query 节点
  • Rewriter
  • Optimizer:调用 planner 创建最佳执行计划
  • Executor

32.8.8.1 Memory Management and Context Switches

ExecutorStart()
    CreateExecutorState():创建 per-query 的 memory context
    (swith to per-query memory context)
    InitPlan()
        ExecInitNode()
            CreateExprContext():创建 per-tuple 的 memory context
            ExecInitExpr()
ExecutorRun()
    ExecutePlan()
        ExecProcNode()
            ExecEvalExpr():在 per-tuple 的 memory context 中调用
            ResetExprContext():释放 per-tuple 的 memory context
ExecutorFinish()
ExecutorEnd()
    ExecEndPlan()
        ExecEndNode()
    FreeExecutorState():释放 per-query 的 memory context 及其 child contexts

32.8.9 Error Handling

  • ereport():用于用户可见的错误,与 C++ 的异常处理机制类似,但是使用很多 C 的宏实现的
  • elog():追溯调用栈至最近的错误处理块,处理块可以选择处理异常或抛出异常;最顶层的错误处理块将会中止当前事务,并 reset 事务的 memory context

32.8.10 Tips For Adding New Functionality

如果想要添加新功能,最好的方法是看看类似的系统函数是如何实现的。鼓励使用内核中已经实现的 API 和数据结构:

  • 单链表
  • 内联单向/双向链表
  • 二叉堆
  • 布隆过滤器
  • 基于动态共享内存的 hash table
  • 背包问题 solver
  • Pairing Heap
  • 红黑树
  • 字符串处理

@mrdrivingduck mrdrivingduck removed the status/unfinished Status of reading the paper label Aug 26, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/database-management-system DBMS source/ebook Online book topic/postgresql PostgreSQL: The World's Most Advanced Open Source Relational Database
Projects
None yet
Development

No branches or pull requests

1 participant