bigywater.tech
PostgreSQL · 13

PostgreSQL 一次 UPDATE 写了多少 WAL:pg_waldump 读记录、整页镜像、full_page_writes 和 unlogged 表实测

“PostgreSQL 一次 UPDATE 到底写了多少 WAL、整页镜像是什么、为什么检查点之后 WAL 暴增” 都可以用 pg_waldump 和 pg_walinspect 直接读出来。这篇在本机自己起一个 PostgreSQL 16.15,逐条读出 INSERT、UPDATE、DELETE 的 WAL 记录和字节数,验证整页镜像、full_page_writes 和 unlogged 表的影响。

18256 → 176 字节
检查点后同一页的第一次 UPDATE 与第二次 UPDATE 写的 WAL(第一次含整页镜像)
480280 → 309056 字节
检查点后第一次更新 1000 行与紧接着第二次更新同样 1000 行的 WAL 量
21.6 MB 对 0.0 MB
插入 10 万行,带主键的普通表和 unlogged / 临时表的 WAL 量

量 WAL

实测 · 本机执行输出
  INSERT 一行(检查点后第一次碰这一页)                          7728 字节
  INSERT 又一行(同一页,不再有整页镜像)                          272 字节
  UPDATE 一行(检查点后第一次碰这一页)                         18256 字节
  UPDATE 同一页的另一行                                   176 字节
  DELETE 一行(检查点后第一次碰这一页)                          8272 字节
  DELETE 同一页的另一行                                   160 字节

用 pg_current_wal_insert_lsn() 前后相减;检查点之后页的第一次修改带整页镜像。

读 WAL 记录

实测 · 本机执行输出
  第一次 UPDATE:
      rmgr: Heap 记录 59 字节(含整页镜像共 8203),事务 <xid>,LOCK xmax: 735, off: 10, infobits: [LOCK_ONLY, EXCL_LOCK], flags: 0x00, blkref rel <库>/<表空间>/<文件> blk 0 FPW
      rmgr: Heap 记录 73 字节(含整页镜像共 2477),事务 <xid>,UPDATE old_xmax: 735, old_off: 10, old_infobits: [], flags: 0x00, new_xmax: 0, new_off: 17, blkref rel <库>/<表空间>/<文件> blk 17 FPW, blkref rel <库>/<表空间>/<文件> bl
      rmgr: Btree 记录 53 字节(含整页镜像共 7453),事务 <xid>,INSERT_LEAF off: 12, blkref rel <库>/<表空间>/<文件> blk 1 FPW
      rmgr: Transaction 记录 34 字节(含整页镜像共 34),事务 <xid>,COMMIT 2026-10-30 11:45:44.340586 CST
  第二次 UPDATE:
      rmgr: Heap2 记录 57 字节(含整页镜像共 57),事务 <xid>,PRUNE snapshotConflictHorizon: 735, nredirected: 0, ndead: 1, nunused: 0, redirected: [], dead: [10], unused: [], blkref rel <库>/<表空间>/<文件> blk 0
      rmgr: Heap 记录 71 字节(含整页镜像共 71),事务 <xid>,HOT_UPDATE old_xmax: 736, old_off: 11, old_infobits: [], flags: 0x60, new_xmax: 0, new_off: 59, blkref rel <库>/<表空间>/<文件> blk 0
      rmgr: Transaction 记录 34 字节(含整页镜像共 34),事务 <xid>,COMMIT 2026-10-30 11:45:44.340978 CST

pg_waldump 能读出 Heap、Btree、Transaction 等记录;第二次 UPDATE 是 HOT_UPDATE,没有索引记录。

整页镜像的代价

实测 · 本机执行输出
  检查点之后第一次更新 1000 行:WAL 480280 字节;紧接着第二次更新同样的 1000 行:WAL 309056 字节
  第一次:Heap/UPDATE 978 条(record_size 178864 字节,fpi_size 2544 字节);Btree/INSERT_LEAF 991 条(record_size 63413 字节,fpi_size 7420 字节);Heap/LOCK 995 条(record_size 53805 字节,fpi_size 122160 字节)
  第二次:Heap/UPDATE 964 条(record_size 176412 字节,fpi_size 0 字节);Btree/INSERT_LEAF 981 条(record_size 62784 字节,fpi_size 0 字节);Heap/LOCK 981 条(record_size 52974 字节,fpi_size 0 字节)

pg_walinspect 统计:检查点后第一遍更新(480280 字节)比第二遍(309056 字节)多写不少 WAL。

full_page_writes

实测 · 本机执行输出
  full_page_writes = on :检查点后更新 1000 行写了 494128 字节 WAL
  full_page_writes = off:检查点后更新 1000 行写了 299400 字节 WAL

关掉能省 WAL,但有 torn page 的风险;更稳妥的是拉长检查点间隔。

不写 WAL 的表

实测 · 本机执行输出
  普通表                 :插入 10 万行,WAL 15.3 MB(表 13.5 MB)
  普通表 + 主键索引          :插入 10 万行,WAL 21.6 MB(表 13.5 MB)
  unlogged 表          :插入 10 万行,WAL 0.0 MB(表 13.5 MB)
  临时表                 :插入 10 万行,WAL 0.0 MB(表 13.5 MB)
  WAL 段文件(每个 16MB): (2, '32 MB')

unlogged 表和临时表不写 WAL,代价是崩溃后清空、不复制。

分步演示

WAL 实测
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / PostgreSQL 16.15(Homebrew);每个实验自己 initdb 一个数据目录,端口 9261 起,只监听 127.0.0.1,不碰本机已有的 5432 上的实例;命令行工具统一用 LC_ALL=C 取英文原文,路径换成 ;计时用 time.perf_counter 或 EXPLAIN ANALYZE 的 Execution Time,数字每次运行会有小幅抖动;实验结束后整个数据目录删除。autovacuum 关掉,checkpoint_timeout = 1h(只有手动 CHECKPOINT 才触发),synchronous_commit = on;WAL 位置用 pg_current_wal_insert_lsn(),WAL 记录用 pg_waldump 和 pg_walinspect 读;字节数是一次运行的结果,不同运行会差几十到几百字节
    • 没有验证:wal_compression 对整页镜像大小的影响;wal_level = logical 时多写的 WAL;并发写入时 WAL 的插入锁争用;WAL 归档(archive_command)和 PITR;pg_waldump 读压缩过的 WAL;PostgreSQL 其他大版本的记录格式
    京ICP备2026067502号-1京公网安备11010502063487号