题记与说明
- 日期:2026-11-01
- 形式:主题研究(先检索来源,再逐篇在线精读,共 6 个来源)
- 本次阅读的来源(全部在线精读原文):
- 13.1. Introduction — MVCC(PostgreSQL 18 官方文档)
- 13.2. Transaction Isolation(PostgreSQL 18 官方文档)
- 8.14. JSON Types(PostgreSQL 18 官方文档)
- 11.2. Index Types(PostgreSQL 18 官方文档)
- 25.3. Continuous Archiving and Point-in-Time Recovery(PostgreSQL 18 官方文档)
- Basic Understanding of Bloat and VACUUM in PostgreSQL — Percona 博客(含 pageinspect 实测演示)
- 本地实证:本机未安装 PostgreSQL(
psql不存在),Docker daemon 未运行(docker ps报cannot find the file specified),故本次无本地实证,全部结论来自上述 6 个在线来源的精读与交叉印证。这一点记录于 §5 局限。
0. TL;DR
PostgreSQL 的并发模型一句话可以说完:每条 SQL 看到的是数据在过去某个时刻的快照,写操作不覆盖旧版本而是追加新版本。这就是 MVCC(多版本并发控制)。官方给它的卖点是:
"in MVCC locks acquired for querying (reading) data do not conflict with locks acquired for writing data, and so reading never blocks writing and writing never blocks reading."(§13.1)
收益是读写互不阻塞(甚至在最严格的 Serializable 下依然成立),代价则散落在主题的另外四个角落:隔离级别要理解"四档请求、三档实现"与序列化失败重试;表与索引要为 UPDATE = insert + delete 留下的死元组付 VACUUM 的账;JSONB 之所以能建 GIN 索引,正是因为它把文档分解成了可索引的二进制项;备份恢复的 PITR 体系(基础备份 + WAL 归档)则是把 MVCC 背后的 WAL 从容灾材料再利用了一遍。四块知识共享同一个内核:版本、快照、WAL。
1. 核心问题
这组材料共同回答的问题是:一个多用户 OLTP 数据库,如何在不加读锁的情况下保证事务隔离,并且这个选择的后果如何波及索引设计、JSON 存储与备份策略? 拆成五个子问题:
- MVCC 机制:PostgreSQL 用什么结构保存"多个版本"?读快照如何判定哪个版本对当前事务可见?
- 隔离级别:ANSI 四档隔离级别 PostgreSQL 如何实现?为什么 Read Uncommitted 形同虚设、Repeatable Read 强于标准要求、Serializable 靠 SSI 达成?
- 版本堆积的代价:UPDATE 为什么等价于 insert + delete?死元组、表/索引膨胀、VACUUM 的分工是什么?
- 索引与 JSONB:六种索引类型各自适配什么数据形态?json 与 jsonb 的本质区别是什么?GIN 的两个操作类如何取舍?
- 备份恢复:pg_dump、文件系统快照、PITR 三条路线的能力边界在哪?为什么官方说高可靠场景首选连续归档?
2. 主要主张
2.1 MVCC:把"UNDO 日志"塞进每张表里
- 定义与目的:官方定义是"Internally, data consistency is maintained by using a multiversion model (Multiversion Concurrency Control, MVCC)",效果是"each SQL statement sees a snapshot of data (a database version) as it was some time ago, regardless of the current state of the underlying data"(§13.1)。
- 核心卖点:读锁不与写锁冲突,"reading never blocks writing and writing never blocks reading",且"PostgreSQL maintains this guarantee even when providing the strictest level of transaction isolation through the use of an innovative Serializable Snapshot Isolation (SSI) level"(§13.1)。
- 与 Oracle/MySQL 的关键差异(Percona 实测文):Oracle/MySQL 把旧版本行放到全局 UNDO 段,而 "In simple terms, PostgreSQL maintains both the past image and the latest image of a row in its own Table." 旧版本就躺在原表里。好处是没有 Oracle 那种 ORA-01555 "snapshot too old" 错误——旧版本永远在,多老的快照都能读;坏处是死元组直接膨胀表。
- 版本判定字段(Percona,用
pg_attribute可见):每个元组带隐藏列——xmin:插入该版本的事务 ID(实测:txid_current()返回 646 后,INSERT 得到的行 xmin=647);xmax:删除/更新该版本的事务 ID,0 表示活版本;即使删除事务回滚,xmax 值仍留在元组上;cmin/cmax:事务内命令序号;ctid:版本物理位置,更新或重建表后改变;tableoid:所属表 OID。
- 可见性规则由这些字段 + 快照共同决定:"no transaction ID that has started before the ID 647, can see this row"(Percona 对 xmin 语义的表述)。
2.2 隔离级别:四档请求、三档实现,可串行化靠 SSI
官方总纲(原文):
"In PostgreSQL, you can request any of the four standard transaction isolation levels, but internally only three distinct isolation levels are implemented, i.e., PostgreSQL's Read Uncommitted mode behaves like Read Committed."
理由:"This is because it is the only sensible way to map the standard isolation levels to PostgreSQL's multiversion concurrency control architecture."(§13.2)
Table 13.1(异常 × 级别矩阵,注意两处 "but not in PG"):
| 级别 | 脏读 | 不可重复读 | 幻读 | 序列化异常 |
|---|---|---|---|---|
| Read uncommitted | Allowed, but not in PG | Possible | Possible | Possible |
| Read committed | Not possible | Possible | Possible | Possible |
| Repeatable read | Not possible | Not possible | Allowed, but not in PG | Possible |
| Serializable | Not possible | Not possible | Not possible | Not possible |
- Read Committed(默认):普通 SELECT "sees a snapshot of the database as of the instant the query begins to run"——每条语句一个新快照,同一事务里两条 SELECT 可以看到不同数据。UPDATE/DELETE/SELECT FOR UPDATE 碰到并发更新的行时会重新求值 WHERE(EvalPlanQual 语义),锁定并基于新版本继续或跳过。文档给的例子:一边
UPDATE website SET hits = hits + 1;,一边DELETE FROM website WHERE hits = 10;——hits 从 9 更到 11 就跳过了 10,DELETE 对它无影响。官方评价:RC "unsuitable for commands that involve complex search conditions; however, it is just right for simpler cases"。 - Repeatable Read:首条非事务控制语句时取快照,此后整个事务 "sees a completely stable view",防幻读(强于 SQL 标准,标准只要求防不可重复读)。实现是快照隔离(Snapshot Isolation,引 berenson95):原文 "The Repeatable Read isolation level is implemented using a technique known in academic database literature and in some other database products as Snapshot Isolation."。若并发首更新者先提交,本事务回滚并报:
ERROR: could not serialize access due to concurrent update应用侧义务:"must be prepared to retry transactions due to serialization failures"——只有只读事务永不冲突。作者注:9.1 之前 PostgreSQL 的 Serializable 其实就是这一档。
- Serializable:"provides the strictest transaction isolation",在 RR 基础上额外监测读写依赖以拦截序列化异常,实现为 SSI(Serializable Snapshot Isolation,引 ports12),用谓词锁:谓词锁在
pg_locks中显示为 SIReadLock 模式,不阻塞也不死锁,可从元组级粗化到页级再到关系级。失败报错:
ERROR: could not serialize access due to read/write dependencies among transactions官方示例:事务 A SELECT SUM(value) FROM mytab WHERE class = 1; 然后插入 class=2 的行;事务 B 对 class=2 求和再插入 class=1 的行——两事务单独串行执行都不会产生双方提交后的结果,故一方必须回滚。序列化失败一律返回 SQLSTATE '40001'(应用重试的判据)。SERIALIZABLE READ ONLY DEFERRABLE 是唯一"Serializable 会阻塞而 Repeatable Read 不会"的情形。
- 工程提示(官方清单):尽量声明
READ ONLY;用连接池;避免长事务/空闲事务(idle_in_transaction_session_timeout);少用显式锁与 FOR UPDATE;调max_pred_locks_per_transaction/_relation/_page;鼓励索引扫描——因为顺序扫描要拿关系级谓词锁,把random_page_cost/cpu_tuple_cost调到让规划器偏爱你已有的索引即可减少谓词锁粒度。 - 一个例外:序列(含 serial 计数器)的变更 "are immediately visible to all other transactions and are not rolled back if the transaction that made the changes aborts"——回滚会烧号,这是官方加框强调的行为。
2.3 死元组与 VACUUM:MVCC 每月寄来的账单
- UPDATE = insert + delete(Percona 原文):"An UPDATE in PostgreSQL would perform an insert and a delete." 实测:更新 10 行后表里物理存在 20 个元组——10 个旧版本(xmin=672, xmax=673)+ 10 个新版本(xmin=673, xmax=0)。DELETE 也不移除行:删 10 行中的 5 行,
pageinspect的heap_page_items显示 10 个元组都还在,只是 5 个的 t_xmax 被置上。 - 膨胀:"The space occupied by these dead tuples may be referred to as Bloat."(Percona)死元组保留是必要的——取决于隔离级别,更老的事务可能还需要读它们。
- VACUUM(普通):扫描死元组,把空间记入 Free Space Map(FSM,
<relation_oid>_fsm文件),"Upon VACUUM, this space is not reclaimed to disk but can be re-used by future inserts on this table."——空间还给表自己,不还给操作系统,除非是高水位线之后的空页。实测:1000 行占 6 页(8KB 页);DELETE emp_id > 500+VACUUM ANALYZE后缩到 3 页(删的是尾部连续行);而DELETE emp_id < 500后仍是 6 页(死元组散布在每页中间,空间只能内部复用)。同时 VACUUM 还冻结老行(服务事务 ID 回卷防护,Percona 留到下一篇文章细讲)。
"Running a VACUUM is a non-blocking operation. It never causes exclusive locks on tables."
- VACUUM FULL:重写整表并把空间还给磁盘(6 页 → 3 页),但:
"VACUUM FULL is not an ONLINE operation. It is a blocking operation."
期间读写全停。在线重表的替代工具是 pg_repack(评论区提及,Percona 承诺另文)。
- 索引也膨胀:Percona 用
bt_page_items实测——DELETE 后索引里 3 个 ctid 条目都还在,VACUUM 之后才 "corresponding pointers with same ctid are also removed from Index through a RowExclusiveLock"。所以死元组的 I/O 代价同时落在堆和索引两侧。
2.4 索引:六种类型,按数据形态选型(§11.2)
官方给出六种内建索引类型:B-tree、Hash、GiST、SP-GiST、GIN、BRIN(另有 bloom 扩展)。每种原文定义:
| 类型 | 一句话能力(原文引述) | 适配场景 |
|---|---|---|
| B-tree | "B-trees can handle equality and range queries on data that can be sorted into some ordering." | 等值/范围/排序,< <= = >= >、BETWEEN、IN、IS NULL;锚定前缀的 col LIKE 'foo%'(不是 '%bar');还能按序取数 |
| Hash | "Hash indexes store a 32-bit hash code derived from the value of the indexed column. Hence, such indexes can only handle simple equality comparisons." | 纯等值 |
| GiST | "GiST indexes are not a single kind of index, but rather an infrastructure within which many different indexing strategies can be implemented." | 几何/二维空间、KNN 最近邻(ORDER BY location <-> point '(101,456)' LIMIT 10) |
| SP-GiST | "SP-GiST permits implementation of a wide range of different non-balanced disk-based data structures, such as quadtrees, k-d trees, and radix trees (tries)." | 空间分区树、前缀类结构 |
| GIN | "GIN indexes are 'inverted indexes' which are appropriate for data values that contain multiple component values, such as arrays." | 数组/JSONB/全文检索等复合值 |
| BRIN | "BRIN indexes ... store summaries about the values stored in consecutive physical block ranges of a table","most effective for columns whose values are well-correlated with the physical order of the table rows." | 超大表、值与物理顺序强相关(如时序),索引体积极小 |
选型总纲:"By default, the CREATE INDEX command creates B-tree indexes, which fit the most common situations."——默认 B-tree,特殊数据形态(几何、数组、JSON、时序大表)才换 USING gist/gin/brin/...。
2.5 JSONB:二进制分解换来可索引性(§8.14)
json 与 jsonb 的本质区别(原文整段,值得背下来):
"The json data type stores an exact copy of the input text, which processing functions must reparse on each execution; while jsonb data is stored in a decomposed binary format that makes it slightly slower to input due to added conversion overhead, but significantly faster to process, since no reparsing is needed. jsonb also supports indexing, which can be a significant advantage."
行为差异:json 保留空格、键序、重复键(处理函数视最后一个为有效);jsonb 不保留三者,重复键只留最后值。jsonb 的输出会重排键({"bar": "baz", "balance": 7.77, "active":false} 输出为按序的 {"bar": "baz", "active": false, "balance": 7.77})。jsonb 把 JSON 原始类型映射到 PostgreSQL 原生类型:string→text、number→numeric、boolean(只认小写 true/false),因此拒绝 \u0000、拒绝超出 numeric 范围的数字。
两个杀手级操作符:
- 包含
@>:右侧文档是左侧的子结构即真;数组内元素与顺序无关('[1,2,3]' @> '[3,1]'为 true),但嵌套必须整体匹配('[1, 2, [1, 3]]' @> '[1, 3]'为 false,@> '[[1, 3]]'才为 true)。 - 存在
?:"tests whether a string ... appears as an object key or array element at the top level of the jsonb value"——只查顶层('{"foo": {"bar": "baz"}}' ? 'bar'为 false)。
GIN 的两个操作类:
- 默认
jsonb_ops:支持?、?|、?&、@>、@?、@@,为每个键和值分别建索引项; jsonb_path_ops:只支持@>、@?、@@(无键存在查询),"Each jsonb_path_ops index item is a hash of the value and the key(s) leading to it"(如{"foo": {"bar": "baz"}}只产生一条 foo+bar+baz 的哈希项)。官方权衡:
"A jsonb_path_ops index is usually much smaller than a jsonb_ops index over the same data, and the specificity of searches is better, particularly when queries contain keys that appear frequently in the data."
代价:"it produces no index entries for JSON structures not containing any values, such as {"a": {}}",查空结构要全索引扫描。
表达式索引补位:WHERE jdoc -> 'tags' ? 'qui' 对表达式求值,默认 GIN 索引帮不上忙;官方给的正解是建表达式索引 CREATE INDEX idxgintags ON api USING GIN ((jdoc -> 'tags'));,或者改写成可索引的包含查询 WHERE jdoc @> '{"tags": ["qui"]}'(代价是索引里存下全部键值)。
设计建议(三条官方原话):
- "In general, most applications should prefer to store JSON data as jsonb, unless there are quite specialized needs, such as legacy assumptions about ordering of object keys."
- "even for applications where maximal flexibility is desired, it is still recommended that JSON documents have a somewhat fixed structure"——半固定结构的文档让汇总查询好写,JSON 与关系模型"can co-exist and complement each other"。
- "any update acquires a row-level lock on the whole row. Consider limiting JSON documents to a manageable size in order to decrease lock contention"——理想情况"JSON documents should each represent an atomic datum that business rules dictate cannot reasonably be further subdivided into smaller datums"。
另有实用细节:对象比数组更适合做包含/存在查询("because unlike arrays they are internally optimized for searching");支持下标读写 jsonb_field['key'] = '1';对 NULL 源做下标 UPDATE 会把它变成 {"a": 1}。
2.6 备份恢复:pg_dump 与 PITR 是两个物种(§25.3)
路线盘点:
| 路线 | 能力 | 边界 |
|---|---|---|
| pg_dump 逻辑转储 | 灵活、可单库单表、可跨版本 | "pg_dump and pg_dumpall do not produce file-system-level backups and cannot be used as part of a continuous-archiving solution. Such dumps are logical and do not contain enough information to be used by WAL replay." |
| 文件系统级备份 | 快(冷拷贝) | 只能整簇恢复;需要文件系统快照工具保证一致性 |
| 连续归档 + PITR | 基础备份 + WAL 回放,可恢复到任意时间点 | 只能整簇恢复;恢复目标点必须晚于基础备份结束时间 |
PITR 的四大收益(官方原文):
- "We do not need a perfectly consistent file system backup as the starting point. Any internal inconsistency in the backup will be corrected by log replay… So we do not need a file system snapshot capability, just tar or a similar archiving tool."
- "Since we can combine an indefinitely long sequence of WAL files for replay, continuous backup can be achieved simply by continuing to archive the WAL files. This is particularly valuable for large databases, where it might not be convenient to take a full backup frequently."
- "It is not necessary to replay the WAL entries all the way to the end. We could stop the replay at any point and have a consistent snapshot of the database as it was at that time."——误删表后可回放到删表前一秒,这是 pg_dump 给不了的能力。
- "If we continuously feed the series of WAL files to another machine that has been loaded with the same base backup file, we have a warm standby system."
机制:WAL 段文件"normally 16MB apiece",按序编号(段名前 8 位即 timeline ID,如 0000000100001234000055CD)。归档三件套:wal_level = replica+、archive_mode = on、archive_command(或 archive_library),如 archive_command = 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f'(%p 待归档文件路径、%f 仅文件名)。三条硬规则:
- "It is important that the archive command return zero exit status if and only if it succeeds."——失败会反复重试;且应拒绝覆盖已存在的归档文件。
- pg_wal 写满的后果:"PostgreSQL will do a PANIC shutdown. No committed transactions will be lost, but the database will remain offline until you free some space."
- "The archive command or function is only invoked on completed WAL segments"——低写入系统要设
archive_timeout("a minute or so")或手动pg_switch_wal,否则丢失窗口可能很长。配置文件(postgresql.conf 等)不进归档。
基础备份:"The easiest way to perform a base backup is to use the pg_basebackup tool.";低层 API 是 pg_backup_start(label => 'label', fast => false) → 复制数据目录(用 tar/cpio,不是 pg_dump)→ pg_backup_stop(wait_for_archive => true),并把后者的第 2/3 个返回值逐字节写入 backup_label/tablespace_map("byte for byte without modification")。备份期间 "full_page_writes is effectively forced on"(性能略降但"not necessary to be concerned about the amount of time it takes"),可并发多个备份,也无需停库。当前版本文档还描述了 --incremental 增量备份:依赖 backup manifest 与 pg_wal/summaries 中的 WAL 摘要,恢复时需 pg_combinebackup 合成全量(增量备份能力自 PostgreSQL 17 起提供——此版本号为本笔记基于已有知识的补充,本次提取的文档页未标注引入版本)。
恢复七步(文档顺序):停库 → 把旧数据目录挪走("you should at least save the contents of the cluster's pg_wal subdirectory",未归档段可能救命)→ 清空数据目录与表空间根 → 恢复基础备份(属主要对——"the database system user, not root!";增量备份先过 pg_combinebackup)→ 清空 pg_wal/ 后把保留的未归档段放回 → 在 postgresql.conf 配 restore_command = 'cp /mnt/server/archivedir/%f %p' 并 "create a file recovery.signal in the cluster data directory"(PG 12+ 用信号文件取代 recovery.conf)→ 启动。注意 restore_command 对"找不到文件"应返回非零——"This is not an error condition",正常的恢复流程就是以一次"file not found"收尾的。
恢复目标与 timeline:可按时间(recovery_target_time)、命名还原点或事务 ID(xid)指定停止点;硬约束:"The stop point must be after the ending time of the base backup… You cannot use a base backup to recover to a time when that backup was in progress."。每次归档恢复完成都会开启新 timeline:"Whenever an archive recovery completes, a new timeline is created to identify the series of WAL records generated after that recovery.";.history 文件"cheap and appropriate to keep… around indefinitely";且 "You cannot recover into timelines that branched off earlier than the base backup."——做演练时选错 timeline 会扑空。
3. 数据与证据
- 异级矩阵与"but not in PG"两处例外:Table 13.1(§13.2 精读提取,见 §2.2)。
- xmin/xmax/ctid 实测链(Percona):
txid_current()=646 → INSERT 行 xmin=647;DELETE 5/10 行后 heap_page_items 仍见 10 个元组;UPDATE 10 行 → 20 个元组(10 旧 + 10 新);bt_page_items 显示 DELETE 后索引 3 个 ctid 仍在、VACUUM 后消失。 - 膨胀回收的形态依赖(Percona):1000 行=6 页=49,152 字节;删尾部连续行+VACUUM → 3 页=24,576 字节;删前半散布行+VACUUM → 仍 6 页。这是"VACUUM 不还空间给 OS、只在高水位线后例外"的直接证据。
- 隔离失败报错与 SQLSTATE:
could not serialize access due to concurrent update(RR)、could not serialize access due to read/write dependencies among transactions(Serializable)、SQLSTATE '40001'(§13.2)。 - WAL 数字:段 16MB;pg_wal 满 → PANIC 停机但"no committed transactions will be lost";archive_timeout 建议"a minute or so"(§25.3)。
- jsonb 语义实验(§8.14 内嵌示例):
'[1,2,3]' @> '[3,1]' → true;'[1, 2, [1, 3]]' @> '[1, 3]' → false;'{"foo": {"bar": "baz"}}' ? 'bar' → false(顶层限定);'{"reading": 1.230e-5}'::jsonb → {"reading": 0.00001230}。 - 交叉印证:MVCC"读不阻塞写"由官方 §13.1 提出、§13.2 的 SSI 章节确认其在 Serializable 下依然成立;Percona 的"UPDATE=insert+delete"与官方 §25.3 的 WAL 全页写机制(归档体积大正是因为页快照)互为佐证。
4. 本地实证说明
本次未执行任何本地验证:which psql pg_ctl pg_dump 均未命中(PostgreSQL 未安装);docker images/docker ps 因 daemon 未运行失败(failed to connect to the docker API ... The system cannot find the file specified)。理论上可做的验证(启动 postgres 容器后:双会话演示 RC/RR 差异、xmin/xmax 观察、jsonb @> 语义、pg_basebackup 演练)均未进行,已列入局限。
5. 结论
- MVCC 把并发控制降维成"版本可见性判定":读路径只做快照过滤,不加锁;写路径追加新版本。由此换来读写并存的并发模型,也由此产生 PostgreSQL 特有的两笔开销——表内自持旧版本(没有 UNDO 段,也就没有 snapshot too old)与死元组回收(VACUUM 家族)。
- 隔离级别的"表面四档、内核三档"是理解 PostgreSQL 事务的钥匙:RU≈RC(每语句快照)、RR=快照隔离(每事务快照 + 首更新者获胜)、Serializable=SSI(快照隔离 + 谓词锁 + 读写依赖检测)。RR 比 SQL 标准强(防幻读),RU 比 SQL 标准弱(无脏读也谈不上)。应用层的义务是把 40001 重试写成默认习惯。
- 膨胀治理要看删除形态:VACUUM 只把空间还给表内部复用,只有高水位线后的空页才还给 OS;散布删除的表 VACUUM 后页数不变,要缩表只有 VACUUM FULL(阻塞)或 pg_repack(在线)。索引膨胀与堆膨胀并存。
- 索引选型是"数据形态 → 索引类型"的映射:可排序标量用 B-tree(默认),纯等值 Hash,几何 GiST/SP-GiST,复合值(数组/JSONB/全文)GIN,物理序相关的大表 BRIN。JSONB 场景还要在 jsonb_ops / jsonb_path_ops / 表达式索引之间按查询模式取舍。
- 备份策略的判定线是"要不要 PITR":要"恢复到误操作前一秒"或热备,只有连续归档一条路;pg_dump 是逻辑备份,逻辑上就不可能参与 WAL 回放。PITR 的韧性来自"基础备份不必完全一致 + WAL 回放修正",其纪律来自归档命令的零退出约定与 pg_wal 空间监控。
6. 局限
- 无本地实证:本机无 PostgreSQL、Docker daemon 未运行(命令与输出见 §4),全部 xmin/xmax、隔离级别、JSONB、恢复流程的结论都来自文档与 Percona 一文的实测叙述,本笔记未能独立复现。
- 实现层只到"机制"未到"源码":官方 §13.1 明确不含 xmin/xmax/snapshot 内部(其提取结果自述"the introduction is conceptual only"),实现细节依赖 Percona 一文;该文自定位入门级,不含 autovacuum 阈值(
autovacuum_freeze_max_age等)、事务 ID 回卷细节(作者明说留待后文)、bloat 的量化 I/O 影响与 HOT 更新等内容。 - 性能主张多为定性:jsonb "significantly faster to process"、jsonb_path_ops "usually much smaller"、B-tree 排序读取 "not always faster than a simple scan and sort" 均无数值基准;检索摘要中出现过的"表达式索引提速 600%+"一类数字未在本次精读的原文页面中得到,故不采信。
- 主题切面有遗漏:本笔记未覆盖 TOAST、HOT update、
pg_visibility、逻辑复制 vs 物理复制的取舍、pg_dump 并行选项与pg_upgrade、max_pred_locks_*的具体默认值等周边;Serializable 调优清单只做了摘录。 - 版本时效:来源为 PostgreSQL 18 "current" 文档与一篇日期未明的 Percona 文章;"增量备份自 PG 17 引入"一说是本笔记基于已有知识的补充(提取页未标注),引用时应核对。9.1 之前 Serializable≈RR、PG12 recovery.signal 等历史行为仅在文档转述中出现。
- 网络检索覆盖面:检索聚焦官方文档与 Percona;未读 2ndQuadrant/EDB/Cybertec 的隔离级别专文与
postgresql.org/docs/current/mvcc.html章节目录页全文,可能遗漏更深层的实现文章。
7. 对我的启发
- 和已有的 SQLite WAL 笔记接上:SQLite 的"读者记住 WAL end-mark 就是 MVCC 的最小实现",PostgreSQL 是它的服务器级放大版——xmin/xmax 判可见性、快照即"活跃事务集合"、WAL 从日志升级为备份/复制的原材料。两个系统一起看,"多版本 + 追加日志 + 定期回收"这个三件套就是所有现代存储的公共骨架。
- 把 40001 重试写进默认模板:官方明确要求 RR/Serializable 应用"must be prepared to retry transactions due to serialization failures"。以后写任何涉及 PostgreSQL 事务的代码(包括 Agent 工具里的写操作),冲突重试(指数退避 + 上限)应当是模板的一部分,而不是出了
40001再补。 - 默认 Read Committed ≠ 默认正确:
UPDATE ... SET hits = hits + 1这类"读-改-写"在 RC 下要靠 EvalPlanQual 兜底,复杂条件下("read-modify-write 跨语句")就该上 RR + 重试或SELECT ... FOR UPDATE。选隔离级别的依据是并发异常对业务的代价,不是"默认的最安全"。 - 长事务是 MVCC 系统共同之敌:SQLite 的 checkpoint starvation 与 PostgreSQL 的死元组堆积/谓词锁膨胀是同一个病的两副面孔——旧版本因快照而不可回收。生产 checklist 应有:
idle_in_transaction_session_timeout、监控pg_stat_user_tables.n_dead_tup与 WAL 目录体积。 - JSONB 用于 Agent 记忆/事件存储的边界:jsonb + GIN 很适合"半固定结构 + 偶发按任意键查询"的场景(正是我之前事件溯源/Agent 记忆调研里的需求),但要记住官方警告:整行级锁意味着大文档高频更新是反模式——把"可变状态"和"追加型事实"分表存放,文档只做原子事实的载体。
- 备份恢复接上 321 原则笔记:之前"数据备份与恢复演练-321原则"那篇讲的是通用纪律,PostgreSQL PITR 给出数据库版的落地形态——基础备份(全量层)+ WAL 归档(增量层)+ 恢复演练(recovery.signal 七步)。值得吸收的硬规则:归档命令"零退出当且仅当成功"、恢复目标必须晚于基础备份结束时刻、演练时盯 timeline。
- "读实现之前先读文档的例外框":官方文档里"Allowed, but not in PG"、"sequence 变更不回滚"、"EvalPlanQual"这些加框/加粗的例外,恰恰是二手教程最常省略、生产事故最常发生的地方。精读官方文档的价值密度高于泛读博客。
附:来源链接汇总
- https://www.postgresql.org/docs/current/mvcc-intro.html (§13.1 MVCC 引言:定义与"读不阻塞写")
- https://www.postgresql.org/docs/current/transaction-iso.html (§13.2 事务隔离:四档矩阵、RC/RR/Serializable 行为、SSI 调优清单)
- https://www.postgresql.org/docs/current/datatype-json.html (§8.14 JSON 类型:json vs jsonb、@>/? 语义、GIN 两个操作类)
- https://www.postgresql.org/docs/current/indexes-types.html (§11.2 索引类型:六类索引的定义与适配)
- https://www.postgresql.org/docs/current/continuous-archiving.html (§25.3 连续归档与 PITR:归档配置、基础备份、恢复七步、timeline)
- https://www.percona.com/blog/basic-understanding-bloat-vacuum-postgresql-mvcc (Percona:xmin/xmax 实测、死元组、VACUUM/VACUUM FULL 对比、索引膨胀演示)