PostgreSQL 的 UNLOGGED 表保留正常 SQL 事务行为,但放弃其内容的崩溃恢复能力。它适合能够重复生成的暂存数据,不适合把唯一副本托付给一次提交。判断边界是服务器是否经历了非正常关闭后的恢复,不能简单地把“重启”两个字等同于数据丢失,也不能因为一次正常重启后行还在,就认为它具备持久性保证。

哪些内容跳过 WAL

非日志表的数据变化及其索引变化不写入预写日志。系统目录的变化仍可能产生 WAL,同一个事务还可能修改普通表,所以不能把整个工作负载描述为“完全没有 WAL”。表的数据页仍然占据存储空间,仍要经过缓冲区管理和后续写回;UNLOGGED 并不是内存表,也不是不需要磁盘的缓存。写入收益来自减少特定日志工作,实际改善幅度需要测量,不能承诺固定倍数。

服务器正常运行时,事务、快照、约束和回滚仍然生效。语句被拒绝或应用断开连接,并不意味着此前提交的全部行都会消失。但是后端进程异常退出可能导致服务器重启其他进程并进入恢复,因此也不能声称“应用事件绝不影响非日志表”。排查时应查看 PostgreSQL 日志和恢复过程,而不是仅凭事故最初发生在哪一层就推断数据状态。

重启与复制的边界

干净关闭后,非日志表内容会保留到下次启动。pg_ctl 的 fast 模式会回滚正在执行的事务并正常关闭,immediate 模式则直接中止进程,不完成干净关闭。后者需要崩溃恢复,恢复期间非日志关系会被重置:表定义仍在,但此前提交的行不再存在。主机维护若完成了干净关闭,并不会仅因为“重启”就清空这类表;反过来,不能把正常关闭的测试当成抗崩溃证明。

物理备库不会收到非日志表的数据内容。备库仍处于恢复模式时,读取这类关系会报错,而不是成功返回零行。提升为主库后可以读取它,但没有已经复制过来的行可以恢复。因此故障切换流程必须先重建或重新填充,再允许依赖这些数据的消费者进入服务。主备拥有同一个表定义,只说明目录信息存在,并不代表表中业务数据已经复制。

普通日志表不能用外键引用非日志表。PostgreSQL 会拒绝这样的建表语句,因为持久化的子表引用不能安全依赖可能在恢复中消失的父表行。下面的实验直接检查这一限制,不会把它描述成“外键还在、父表行没了、数据库默默允许不一致”。设计暂存区时,要把关系约束放到符合持久性边界的位置,而不是事后修补孤立记录。

坏掉的样子

一个常见事故是导入程序把文件装入非日志暂存表后就确认成功,并删除唯一的原始文件。随后服务器崩溃,暂存行消失,而输入已无处重取,这就变成不可恢复的数据丢失。原始输入应该至少保留到持久化目标提交成功,重建流程也必须知道需要哪一个输入版本。去重记录和导入检查点要属于持久化契约,不能和暂存内容一起丢掉。

另一个问题出现在报表服务改读备库,或刚完成提升就立即对外服务。只检测行数为零会漏掉恢复模式下的读取错误;无限重试也无法生成不存在的数据。应明确标记“正在重建”,在重建完成且结果验证通过前控制请求准入。演练要测量重建耗时、上游是否可用、记录数及内容是否符合预期;只建回一张空表不算恢复,能运行的脚本也不等于已经证明恢复时间满足要求。

快速验证

把 Bash 代码保存为 unlogged.sh。它要求以非 root 用户执行 PostgreSQL 18 的命令行工具。下面的容器命令提供工具,不发布网络端口,也不挂载宿主机数据;两个实验数据库都位于容器内的新临时目录。

docker run --rm --network none --user postgres --entrypoint bash -i \
  postgres:18-alpine < unlogged.sh
#!/usr/bin/env bash
set -euo pipefail
pg_lab=$(mktemp -d /tmp/unlogged-lab.XXXXXX)
cleanup() {
  for cluster in standby primary; do
    if [ -f "$pg_lab/$cluster/postmaster.pid" ]; then
      pg_ctl -D "$pg_lab/$cluster" -m immediate -w stop >/dev/null
    fi
  done
  rm -rf -- "$pg_lab"
}
trap cleanup EXIT
primary() { psql -X -h "$pg_lab" -p 55432 -d postgres -v ON_ERROR_STOP=1 "$@"; }
standby() { psql -X -h "$pg_lab" -p 55433 -d postgres -v ON_ERROR_STOP=1 "$@"; }
start_primary() {
  pg_ctl -D "$pg_lab/primary" -l "$pg_lab/primary.log" \
    -o "-k $pg_lab -p 55432 -c listen_addresses=''" -w start >/dev/null
}
initdb -D "$pg_lab/primary" --no-locale --encoding=UTF8 --auth=trust >/dev/null
start_primary
primary <<'SQL'
CREATE TABLE durable (id integer PRIMARY KEY);
CREATE UNLOGGED TABLE scratch (id integer PRIMARY KEY);
INSERT INTO durable VALUES (1);
INSERT INTO scratch VALUES (1);
BEGIN;
INSERT INTO scratch VALUES (2);
ROLLBACK;
DO $$
BEGIN
  EXECUTE 'CREATE TABLE durable_ref (id integer REFERENCES scratch(id))';
  RAISE EXCEPTION 'a logged table unexpectedly referenced an unlogged table';
EXCEPTION WHEN invalid_table_definition THEN
  RAISE NOTICE 'logged-to-unlogged foreign key rejected';
END $$;
SQL
counts='SELECT (SELECT count(*) FROM durable), (SELECT count(*) FROM scratch)'
test "$(primary -Atc "$counts")" = '1|1'
pg_ctl -D "$pg_lab/primary" -m fast -w stop >/dev/null
start_primary
test "$(primary -Atc "$counts")" = '1|1'
printf 'clean restart: 1|1\n'
pg_basebackup -h "$pg_lab" -p 55432 -D "$pg_lab/standby" -R -X stream -c fast
pg_ctl -D "$pg_lab/standby" -l "$pg_lab/standby.log" \
  -o "-k $pg_lab -p 55433 -c listen_addresses=''" -w start >/dev/null
test "$(standby -Atc 'SELECT pg_is_in_recovery()')" = t
test "$(standby -Atc 'SELECT count(*) FROM durable')" = 1
if standby -Atc 'SELECT count(*) FROM scratch' 2>"$pg_lab/read-error"; then
  echo 'unlogged read unexpectedly succeeded on standby' >&2
  exit 1
fi
grep -F 'cannot access temporary or unlogged relations during recovery' "$pg_lab/read-error"
pg_ctl -D "$pg_lab/standby" -w promote >/dev/null
test "$(standby -Atc "$counts")" = '1|0'
printf 'promoted standby: 1|0\n'
pg_ctl -D "$pg_lab/primary" -m immediate -w stop >/dev/null
start_primary
test "$(primary -Atc "$counts")" = '1|0'
printf 'crash recovery: 1|0\n'
primary -Atc 'SHOW server_version'
echo 'PASS: rollback, foreign key restriction, clean restart, standby, promotion, crash'

主库与备库使用同一个临时目录中的不同 Unix 套接字端口,退出时的 trap 只关闭这两个实验集群并移除其文件。实验故意对临时主库执行 immediate 关闭,不能把这一步改成已有数据库目录。PostgreSQL 18.6 上,回滚和正常重启后的计数都是 1|1;仍处于恢复模式的备库拒绝读取 scratch。备库提升和主库崩溃恢复后分别得到 1|0,表明持久化行还在而暂存内容不在。错误文本断言使用集群的 C locale,避免本地化消息造成误判。

这些结果验证生命周期和约束语义,不代表已经测出吞吐量改善,也不证明所有文件系统在真实掉电后的表现。性能评估还应计入输入保留和重建成本。持久化目标必须保留正常 WAL 与持久性配置;如果同时关闭这些保护,就改变了对照条件,无法再用本实验说明普通表与非日志表的区别。

来源