首页 > 数据库 > PostgreSQL > PG预写式日志解码的艺术与应用
本文整理自 IvorySQL 2025 生态大会暨 PostgreSQL 高峰论坛的演讲分享,演讲嘉宾:李传成,walminer 作者。
本文内容主要包括:

当数据库执行 INSERT操作时,WAL 日志会记录以下关键信息,用于物理层面的数据回放:
流程逻辑为:通过
RelfileNode找到数据文件 → 由
PageNo定位到文件内的页 → 借助
offSet确定页内数据位置 → 最终对
Data进行
二进制拷贝,完成物理层面的日志回放(如备库同步、数据库重启恢复时的底层数据还原)。
若需将 WAL 日志解析为人类可读的逻辑数据,流程如下:
RelfileNode关联数据库
数据字典(如 PostgreSQL 的系统表),查询其对应的模式(Schema)和表名。pg_attribute)中获取该表的字段定义(字段名、类型等)。Data中存储的二进制数据,还原出 INSERT 操作的具体数据内容(如“插入了哪些列、对应什么值”)。wal2json、
test_decoding的格式化输出),将解析结果以可读形式呈现(如 JSON、文本格式)。

DELETE 操作的 WAL 记录由以下核心元素构成:
物理层面的 WAL 回放用于实现数据的底层删除标记,流程如下:
RelfileNode确定对应的物理数据文件。PageNo找到文件内的目标数据页。offSet定位页内待删除的行。xmax(事务标识),完成物理层面的删除标记(该流程用于数据库恢复、备库同步等场景,保障底层数据结构的一致性)。若需将 DELETE 日志解析为人类可读的逻辑操作,流程如下:
RelfileNode关联数据字典(如 PostgreSQL 系统表),确定操作的目标表。wal2json)将结果格式化为可读形式(例如“DELETE FROM 表名 WHERE 主键=XXX”)。

UPDATE 操作的 WAL 记录包含以下核心元素,体现“旧行标记删除、新行插入”的底层逻辑:
物理层面的 WAL 回放严格遵循“旧行标记删除 + 新行二进制插入”的步骤:
PageNo Old和
offSet Old定位旧行,设置其
xmax(事务标识),完成逻辑删除标记。PageNo New和
offSet New定位新行位置,将
New Data/Delta Data以二进制形式直接拷贝写入;若为 Delta Data(增量数据),则先提取旧行基础数据,覆盖变更字段后再完成新行写入。逻辑层面需将 UPDATE 日志解析为可读的更新操作,同时需关注 逻辑日志级别下的 WAL 膨胀问题:
RelfileNode关联数据字典确定表名,再从系统表中获取该表的
全数字段列表(逻辑日志级别下,无论实际更新字段多少,均会存储全字段数据)。New Data:逻辑日志级别下,即使仅更新 1 个字段,也会存储表中所有字段的新值(这是 WAL 日志膨胀的核心诱因)。Identity Data:独立存储用于条件判断的标识字段(如主键);若表的标识级别配置为
full,还会额外存储
所有旧字段值,进一步放大日志体积。

根据是否包含 FPI,日志结构分为两种:
INSERT标识、
RelfileNode(数据文件标识)、
PageNo(页号)、
offSet(页内偏移)、
Data(新插入的二进制数据)。该结构用于常规插入场景,依赖 WAL 的增量记录保障一致性。INSERT标识、
RelfileNode、
PageNo、
offSet、
FPI(整页数据镜像)。此时不存储
Data,恢复时直接通过 FPI 覆盖整页,适用于“页面撕裂风险高”的场景(如 checkpoint 间隔大时)。逻辑级别下的 INSERT 日志存在 数据冗余设计:
FPI(整页数据)和
Data(新插入数据)。从设计合理性看,FPI 本身已包含页面数据,本可直接用于逻辑解码的信息提取,却额外存储
Data,造成 WAL 日志膨胀。checkpoint配置强相关:若
checkpoint配置密集(触发 FPI 的场景少),则性能影响微乎其微;若
checkpoint间隔大(FPI 频繁触发),则可能因冗余加剧 WAL 写入压力,此时优化该设计(复用 FPI 进行逻辑解码)可带来一定性能提升。

根据是否包含 FPI,日志结构分为两种:
DELETE标识、
RelfileNode(数据文件标识)、
PageNo(页号)、
offSet(页内偏移)。该结构下,DELETE 操作仅需定位行后设置
xmax(事务标识)即可完成物理层面的删除标记,无需额外数据存储。FPI(全页镜像)。此时通过 FPI 覆盖整页来保障“页面撕裂”场景下的物理一致性,恢复时直接以全页镜像还原数据。逻辑级别下的 DELETE 日志需满足“可读删除条件”的解析需求,结构如下:
DELETE、
RelfileNode、
PageNo、
offSet,并额外加入
Identity Data(标识数据,如主键)。
Identity Data用于逻辑解码时明确“删除哪一行”的条件(如
DELETE FROM 表名 WHERE 主键=XXX)。FPI(保障物理恢复)和
Identity Data(服务逻辑解码)。FPI 满足底层页面一致性,Identity Data 满足逻辑行定位需求,二者结合支撑物理与逻辑的双重场景。

UPDATE 在 replica 级别下的日志可理解为“INSERT + DELETE”的物理层面组合,结构分为两种:
UPDATE标识、
RelfileNode(数据文件标识)、
PageNo New/offSet New(新行定位)、
PageNo Old/offSet Old(旧行定位)、
New Delta(新行的增量数据)。仅记录变更的增量信息,保障物理复制的高效性。New Delta替换为
FPI(全页镜像),通过整页覆盖实现“页面撕裂”场景下的物理一致性恢复,此时不存储增量数据,直接依赖 FPI 完成新行的二进制写入。逻辑级别下的 UPDATE 日志因 标识列是否更新和 是否启用 FPI呈现复杂差异,这也是逻辑解码易踩坑的核心场景:
UPDATE、
RelfileNode、新旧
PageNo/offSet、
New Data(新行全量数据)。因标识列未更新,逻辑解码时可直接从
New Data中提取标识列(如主键),无需额外存储旧数据。Identity Data(标识数据,如更新后的主键)。此时标识列被修改,需单独存储新标识以明确“更新后的数据归属”,逻辑解码时通过
Identity Data定位新行的标识条件。UPDATE、
RelfileNode、新旧
PageNo/offSet、
New Data、
FPI。
FPI保障物理层面的页面一致性,
New Data服务逻辑解码的新行数据解析。Identity Data,同时满足“物理页面恢复(FPI)”“新行数据解析(New Data)”“标识列变更定位(Identity Data)”三重需求。在解析 logical 级别 UPDATE 日志时,需重点关注 标识列是否更新:
New Data中已包含标识信息,无需额外依赖旧数据即可定位行。Identity Data明确新标识,否则易因标识列变更导致数据关联错误。这一设计细节是 PostgreSQL 为平衡“逻辑可读性”与“存储效率”的权衡,也是逻辑解码开发中需重点理解的技术坑点——只有明确标识列的更新状态,才能准确解析“更新了哪一行、更新后的数据是什么”的逻辑语义。
我们常遇到以下三个问题:
如何解决这 3 个问题呢?那么就要依靠高级逻辑解码实现。

在物理复制中,使用磁盘上切实存在的 tuple 作为变更受体完成 delete 或者 update 操作,而在逻辑解码中因为无法确定的在当前 wal 中找到 tuple 变更受体。因而需要额外记录大量的新旧数据来完成逻辑变更数据的确定。
PG 依赖 FPW 体系,这意味着当前 WAL 记录前序相对不远的 WAL 中,一定存在本 WAL 修改的 page 的全页。这将使在 replica wal 级别下做逻辑解码变为可能。

基于 WAL 日志的分层存储与内存管理机制,逻辑解码工具可通过以下流程,在 logical 级别下实现 REDO SQL 与 UNDO SQL 的生成:
FPI 内存缓存
当工具解析到
带 FPI(全页镜像)的 WAL 记录(如
PAGE A FPI)时,会将该 FPI 对应的页数据完整缓存至内存,建立“页标识-全页数据”的映射关系,为后续元组(tuple)定位提供基础。
同页操作的旧 tuple 定位
当解析到针对
同一 page(如 PAGE A)的写入类 WAL 记录(如
PAGE A INSERT或
PAGE A UPDATE)时,工具从内存缓存的 FPI 中定位到操作对应的
旧 tuple(元组)(即“寻找 tuple 受体”的过程)。
新旧 tuple 拼装与 SQL 生成

上述操作既能解析普通表,也能解析系统表,那么我们也可以解析出一条语句来。
向
pg_class 中插入一条数据,其 oid 如上图所示,向
pg_attribute 中插入一行数据,其字段如上图所示。那么我们即可拼出上图最下方的 DDL。
基本上所有的 DDL 都可以通过这种方式去找到复原方法,这就是在 replica 级别完成 DDL 解码的过程。

WALMINER 的核心技术突破体现在以下四点:
基于核心技术,WALMINER 衍生出 CDC(变更数据捕获) 和数据恢复两大产品方向:
APP 出现 bug,生成错误的 UPDATE SQL,在操作数据库时导致表 A 更新大量无关数据行。

执行一个命令行工具,指定数据字典的位置,指定用户名。

要利用 WALMINER 的
search命令定位误操作,可按以下流程操作:
(1)
命令参数配置:执行
walminer search时,需指定
-D(第一步生成的数据字典文件)、
-w(WAL 日志的生产目录和归档目录列表)、
-b(误操作涉及的数据库)、
-r(误操作涉及的“模式.表”,如
public.t1)。
(2)
自动化分析输出:工具会遍历指定 WAL 日志,输出每个事务的详细统计,包括事务 ID(xid)、LSN 范围(
start_lsn/
end_lsn)、insert/update/delete 操作次数、提交 LSN 等(如示例中“xid=853 insert=901”“xid=856 update=10”等条目)。
(3) 人工判别误操作:这是流程中唯一需要 DBA 人工介入的环节——需结合操作时间、类型(insert/update/delete)和数据量,从输出的事务列表中识别出误操作对应的事务。

通过 wal2sql 命令,精准的找回 undo 语句,然后人为确定这些语句没有问题,防止出现 bug。

只需通过
psql -f命令执行生成的 SQL 恢复脚本(如
wal2sql_result.sql),再通过常规 SQL 查询验证数据,整个数据恢复流程操作简洁,即使是数据库新手也能轻松完成。

wal2sql 命令 1:
walminer wal2sql -k 4 -m 1 -L 0/9023de8 -s 856 -D ~/wp/dic/walminer.dic -w [/data/pg_datas/pg15/archive_dir],[/data/pg_datas/pg15/data/pg_wal] -F 2 -t 2 -f ~/wal2sql_result.sql
wal2sql 命令 2:
walminer wal2sql -k 4 -m 1 -L 0/9023de8 -s 856 -D ~/wp/dic/walminer.dic -w [/data/pg_datas/pg15/archive_dir],[/data/pg_datas/pg15/data/pg_wal] -t 2 -f ~/wal2sql_result.sql

如果误操作了一个表之后又执行了正常的业务,应该如何去恢复数据?
WALMINER

WALMINER 的解决方案可按以下逻辑落地:
bank_account表的 WAL 日志范围。-c参数执行
wal2sql命令:该参数会触发 WALMINER 的
分层事务分析能力,自动列出:
id=1,2,3设为
balance=1的更新);id=3的
balance+100操作)。用户可基于这些事务明细, 人工判定最终应恢复的正确数据状态(例如区分“误操作前的原始值”“误操作后的值”“后续业务更新后的值”)。后续将集成“自动推荐最优 UNDO SQL”功能,无需人工介入即可输出最可能的回滚语句,进一步简化数据恢复的决策流程。
PGTO 致力于极简的 CDC 部署,目前已实现一键式 CDC 部署,可以完成集簇级的数据同步,同时支持新建 DB 实例同步,新建表同步,支持双向同步。



server 运行后的订阅方案:



来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70043484/viewspace-3101134/ ,如需转载,请注明出处,否则将追究法律责任。