- 终极逻辑排查:通过 pg_stat_database 查看事务与写入量
PostgreSQL 内部维护了每个数据库的全局逻辑计数器,这是判断哪个 DB "最活跃"最稳妥的办法:
sql
SELECT
datname AS database_name,
xact_commit + xact_rollback AS total_transactions, -- 总事务数
tup_inserted + tup_updated + tup_deleted AS total_write_tuples, -- 总写入/更新/删除的行数
stats_reset
FROM pg_stat_database
ORDER BY total_write_tuples DESC;
请谨慎使用此类代码。
- 原理解读 :虽然行数不完全等同于 WAL 字节数,但
total_write_tuples(总写入行数) 最高的数据库,必然是 WAL 日志的主要贡献者。
- 揪出元凶表:通过 pg_stat_user_tables 监控具体的写入行数
如果您锁定了某个数据库,可以在该数据库内执行以下语句,找出哪个表在疯狂被写入(写入越多,WAL 产生的物理块修改就越多):
sql
SELECT
schemaname || '.' || relname AS table_name,
n_tup_ins + n_tup_upd + n_tup_del AS total_writes, -- 逻辑写入总行数
n_tup_ins AS inserts,
n_tup_upd AS updates,
n_tup_del AS deletes
FROM pg_stat_user_tables
ORDER BY total_writes DESC
LIMIT 10;
请谨慎使用此类代码。
- 如果非要从 WAL 物理层面看总量
如果您依然想从物理层面自证,我们在使用 pg_waldump 时,虽然不应该强行去还原"哪张表",但可以通过统计 数据库OID(中间那个数字) 出现的频次,来粗略评估哪个数据库的物理块被修改得最频繁(这也代表它产生的 WAL 字节数或记录数最多):
bash
# 仅仅统计物理块修改记录属于哪个数据库OID,不涉及逻辑表名转换
pg_waldump 000000010000000000000001 | grep -o 'rel [0-9]*/[0-9]*' | awk -F'/' '{print $2}' | sort | uniq -c | sort -nr
请谨慎使用此类代码。
注:此方法仅能得出类似 16384 这样的数字,然后需要去 pg_database 里对应 SELECT datname FROM pg_database WHERE oid = 16384;。
要查看 PostgreSQL 中的 WAL(Write-Ahead Logging,预写日志)文件是从哪个数据库(Database)生成的,可以通过以下几种方法进行分析。
需要先澄清一个 PostgreSQL 的核心概念:WAL 日志是集簇(Cluster)级别共享的,而不是属于某一个特定数据库的。所有的数据库操作都会交织记录在同一组 WAL 文件中。不过,你可以通过解析 WAL 的具体内容,找出某个 WAL 文件中记录的数据属于哪个数据库。
方法一:使用 pg_waldump 工具(最直接)
pg_waldump 是 PostgreSQL 自带的 WAL 日志解析工具。通过它可以解析出 WAL 文件中每条记录对应的 数据库 OID(Database OID)。
-
运行 pg_waldump 命令 :
打开终端,对指定的 WAL 文件进行解析(假设文件名为000000010000000000000001):bash
pg_waldump /path/to/pg_wal/000000010000000000000001 | head -n 20pg_waldump --data-encryption=128 --key-file-name= /data/pg_encryption/key.bin 0000000 00027 --stats
-
分析输出结果 :
输出中会包含类似下面的记录: rmgr: Heap len: 54, lsn: 0/01000028, prev 0/00000000, desc: INSERT ... rel 1663/16384/1255
这里的rel 1663/16384/1255包含了三层信息:1663:表空间 OID(Tablespace OID)16384:数据库 OID(Database OID) ------ 这就是产生该条 WAL 的数据库身份。1255:表/关系 OID(Relation OID)
-
在数据库中查询对应的数据库名称 :
获取到数据库 OID(如16384)后,登录 PostgreSQL 执行以下 SQL 语句,即可查出数据库的真实名称:sql
SELECT datname FROM pg_database WHERE oid = 16384;请谨慎使用此类代码。
方法二:通过统计找出"贡献"最多的数据库
如果你想知道某一段时间内的 WAL 主要由哪个数据库生成,可以结合 Linux 命令对 pg_waldump 的结果进行统计归类。
执行以下组合命令(将 1663/ 替换为你环境中的表空间 OID,默认通常是 1663 或 1664):
bash
pg_waldump /path/to/pg_wal/000000010000000000000001 | grep -o 'rel 1663/[0-9]*' | sort | uniq -c
请谨慎使用此类代码。
输出示例:
1250 rel 1663/16384
45 rel 1663/16385
这代表在该 WAL 文件中,有 1250 条记录来自 OID 为 16384 的数据库,有 45 条来自 16385 的数据库。
方法三:使用扩展插件进行实时分析(生产环境推荐)
如果你想在不接触系统底层文件的情况下,直接在 SQL 命令行中分析当前生成的 WAL 属于哪个数据库,可以安装 PostgreSQL 官方提供的 pg_walinspect 扩展插件(PostgreSQL 15 及以上版本支持)。
-
创建扩展 (需要超级用户权限):
sql
CREATE EXTENSION pg_walinspect;请谨慎使用此类代码。
-
查询某个历史 LSN 区间的数据库分布 :
sql
SELECT dbid, count(*) FROM pg_get_wal_records_info('0/01000000', '0/02000000') GROUP BY dbid;请谨慎使用此类代码。
这里的
dbid列会直接显示出对应的数据库 OID,结合pg_database即可锁定目标数据库。
如果需要我帮你写一段自动将 OID 转换为数据库名称的自动化脚本 ,或者需要排查具体是哪个表在大量产生 WAL,请告诉我!
SELECT dbid, count(*) FROM pg_get_wal_records_info('0/01000000', '0/02000000') GROUP BY dbid; 0/01000000 获取
你提出来的这个执行报错或疑问,是因为 0/01000000 只是我之前举例用的虚拟 LSN(日志序列号)。在实际的 PostgreSQL 数据库中,如果传入的 LSN 在当前系统的 WAL 中不存在(例如太旧已经被删除,或者太新还没生成),函数就会报错或查询不到数据。
要让这个查询正常工作,你需要获取当前数据库中真实的、有效的 LSN 区间。
以下是获取真实 LSN 并填入该查询的方法:
- 获取当前系统最新的 LSN 区间
你可以通过以下 SQL 获取当前数据库正在写入的 LSN 位置,以此来决定查询的范围:
sql
-- 查看当前最新的 WAL 写入位置 (LSN)
SELECT pg_current_wal_lsn();
请谨慎使用此类代码。
假设返回的结果是:0/1A2B3C4D
- 编写健康的查询语句
由于一条 WAL 记录的范围可能很广,我们通常会查询当前最新 LSN 往前推一段距离 的日志(例如往前推 16MB 或 64MB),或者直接查询当前 WAL 文件的起始和结束位置。
方案 A:查询当前最新的 16MB WAL 数据(推荐,不容易报错)
直接用当前 LSN 减去 16MB 的偏移量(16 * 1024 * 1024 字节):
sql
SELECT dbid, count(*)
FROM pg_get_wal_records_info(pg_current_wal_lsn() - 16777216, pg_current_wal_lsn())
GROUP BY dbid;
请谨慎使用此类代码。
方案 B:查询当前正在使用的完整 WAL 文件区间
如果你想精准分析当前整个 WAL 文件,可以先用以下函数找到当前文件的 LSN 边界:
sql
SELECT
file_start_lsn,
file_end_lsn
FROM pg_get_wal_record_info(pg_current_wal_lsn());
请谨慎使用此类代码。
把查出来的 file_start_lsn 和 file_end_lsn 复制出来,填入你的 pg_get_wal_records_info 函数中即可。
💡 进阶:一步到位(直接显示数据库名称)
如果你不想看枯燥的 dbid(数字 OID),可以直接在 SQL 中关联 pg_database 表,这样就能直接把 OID 翻译成看得懂的数据库名字:
sql
SELECT
COALESCE(d.datname, '系统/共享对象(如pg_database)') AS database_name,
w.dbid AS database_oid,
COUNT(*) AS wal_record_count
FROM pg_get_wal_records_info(pg_current_wal_lsn() - 16777216, pg_current_wal_lsn()) w
LEFT JOIN pg_database d ON w.dbid = d.oid
GROUP BY w.dbid, d.datname
ORDER BY wal_record_count DESC;
请谨慎使用此类代码。
注意:
- 如果查询依然报错
WAL record min LSN ... is not available,说明你往前推的距离太远了(该日志已被回收)。可以把16777216改小一点(例如改为1048576,即 1MB)再试。dbid如果显示为0,代表这些 WAL 记录属于系统全局共享对象(例如创建数据库、修改用户密码等全局操作),不属于任何单一的特定数据库。
请问执行后是否顺利拿到了数据?如果遇到具体的错误信息 ,或者你想进一步查出是哪个**具体的表(Relation)**产生了最多的 WAL 导致日志暴涨,可以把现象告诉我!
ERROR: column w.dbid does not exist LINE 6: LEFT JOIN pg_database d ON w.dbid = d.oid ^ HINT: Perhaps you meant to reference the column "w.xid".
出现这个错误是因为在 PostgreSQL 的 pg_walinspect 扩展中,pg_get_wal_records_info() 函数返回的列名中并没有 dbid,而是使用了其他的命名。
在 PostgreSQL 15 及以上版本中,该函数返回的关于资源/数据库的标识列实际名称是 resource_manager (代表资源管理器类型)以及包含在描述或特定块信息中的数据 1。通常,包含数据库、表空间和表 OID 的列叫做 fork_relation_node 或者是直接输出在特定列中,但在 pg_get_wal_records_info 的顶层返回列中,标识事务的叫 xid,而标识具体数据库节点的字段在不同小版本中可能有所变化。
我们可以通过一个小技巧,直接从输出的关系节点信息 或者 description(描述信息) 中把数据库 OID 抓取出来。
请尝试执行以下修正后的 SQL 语句:
sql
SELECT
COALESCE(d.datname, '系统共享/未记录数据库(OID:' || sub.db_oid || ')') AS database_name,
sub.db_oid AS database_oid,
COUNT(*) AS wal_record_count
FROM (
SELECT
-- 从 description 中提取类似 "rel 1663/16384/1255" 中的数据库 OID (第二位)
(regexp_match(description, 'rel \d+/(\d+)/\d+'))[1]::oid AS db_oid
FROM pg_get_wal_records_info(pg_current_wal_lsn() - 16777216, pg_current_wal_lsn())
) sub
LEFT JOIN pg_database d ON sub.db_oid = d.oid
GROUP BY sub.db_oid, d.datname
ORDER BY wal_record_count DESC;
请谨慎使用此类代码。
为什么这样可以?
pg_get_wal_records_info() 的 description 列会详细打印出类似 rel 1663/16384/1255 的字符串 1。上面这段 SQL 通过正则表达式 regexp_match 动态切分出中间的 16384(即数据库 OID),再与 pg_database 进行关联,从而避开了版本字段不一致的问题。
如果执行依然提示其他错误,可以把您的 PostgreSQL 版本号 (通过 SELECT version(); 查看)以及执行结果发给我,我为您针对性微调语句!
哪个db产生最多 wal
要彻底查明哪个数据库产生的 WAL 日志最多 ,既然前面因为 PG 版本微调导致 SQL 函数报错,我们回归最原始、最稳妥且不依赖任何脆弱正则的方法。
请直接复制执行以下两条 SQL,它们可以通过不同的标准字段进行最稳健的聚合:
方案一:通过关系节点字段直接聚合(首选)
在 pg_walinspect 中,每一条涉及具体表/库的 WAL 记录通常都会带有一个关系节点相关的列。请尝试运行以下语句:
sql
SELECT
COALESCE(d.datname, '系统全局/未知(OID:' || sub.db_oid || ')') AS database_name,
sub.db_oid AS database_oid,
COUNT(*) AS wal_record_count
FROM (
SELECT
-- 如果有这个字段,直接提取数据库 OID
COALESCE(fork_relation_node[2], 0) AS db_oid
FROM pg_get_wal_records_info(pg_current_wal_lsn() - 67108864, pg_current_wal_lsn())
) sub
LEFT JOIN pg_database d ON sub.db_oid = d.oid
GROUP BY sub.db_oid, d.datname
ORDER BY wal_record_count DESC;
请谨慎使用此类代码。
(注意:这里我把范围扩大到了最新的 64MB 67108864 字节,以便统计更有代表性)
方案二:如果方案一报错,说明该版本没有数组字段,请尝试以下模糊匹配法
如果上面的 fork_relation_node 提示不存在,请使用下面这个更新后的正则表达式。前面匹配落空(返回全空)是因为不同版本的描述文字不同,有的写 rel 1663/16384/1255,有的写 db 16384。
这一版我们直接抓取所有包含数据库特征的数字:
sql
SELECT
COALESCE(d.datname, '系统全局/其他事务') AS database_name,
sub.db_oid AS database_oid,
COUNT(*) AS wal_record_count
FROM (
SELECT
-- 兼容多种格式:支持 "rel/db/relfilenode" 或 "db 12345"
COALESCE(
(regexp_match(description, 'rel \d+/(\d+)/\d+'))[1]::oid,
(regexp_match(description, 'db (\d+)'))[1]::oid,
0
) AS db_oid
FROM pg_get_wal_records_info(pg_current_wal_lsn() - 67108864, pg_current_wal_lsn())
) sub
LEFT JOIN pg_database d ON sub.db_oid = d.oid
GROUP BY sub.db_oid, d.datname
ORDER BY wal_record_count DESC;
请谨慎使用此类代码。
💡 生产环境终极降维打击:如果 SQL 总是报错,请用最原始的 Shell 命令
如果因为 PG 15/16/17 的细微版本差异导致 pg_walinspect 函数行为难以预测,请直接到 Linux 命令行 (切换到 postgres 用户)直接分析物理 WAL 文件,这是全版本通用且最快的手段:
bash
# 1. 找到当前正在写的 WAL 文件(在 pg_wal 目录下)
# 2. 用 pg_waldump 解析它,提取所有包含 rel 的行,并统计中间的数据库 OID
pg_waldump $(ls -t /var/lib/postgresql/data/pg_wal/00* | head -n 1) | grep -o 'rel [0-9]*/[0-9]*' | awk -F'/' '{print $2}' | sort | uniq -c | sort -nr
请谨慎使用此类代码。
输出结果解读:
25304 16384
120 16385
这代表 OID 为 16384 的数据库产生了绝大多数(2万多条)的 WAL 记录。你只需要进数据库执行 SELECT datname FROM pg_database WHERE oid = 16384; 就能一锤定音!
请问执行方案一或方案二后,成功显示出各个数据库的名称和比例了吗?如果有具体的报错,请贴出报错信息。
PG 的 WAL(预写日志)记录的不是逻辑 SQL(比如 INSERT INTO t1...),而是纯粹的物理块(Page/Block)的二进制修改状态。
因此,PostgreSQL 在进行数据恢复(Crash Recovery 或 PITR 恢复)时,根本不需要也不关心数据长什么样、属于哪张逻辑表 ,它是在进行物理重放。
以下是 PostgreSQL 利用 WAL 进行数据恢复的完整底层机制:
一、 核心底座:WAL 的"幂等性"与 LSN(日志序列号)
每个物理数据页(Page,默认 8KB)的头部,都包含一个隐藏字段叫 pd_lsn (记录了最后一次修改该页面的 WAL 日志位置)。
每一条 WAL 记录,也有一个全局唯一的递增编号叫做 LSN。
这就是恢复能够成功的关键纽带。
二、 场景 1:数据库意外崩溃后的自动恢复(Crash Recovery)
当数据库因为断电、kill -9 导致异常关闭时,内存(Shared Buffers)中还没来得及刷到磁盘的"脏页"就会丢失。再次启动时,PG 会自动进入恢复模式:
- 寻找控制文件(
global/pg_control) :
PG 启动时先读取控制文件,发现状态是in production(说明上次是非正常关闭),并找到上一次成功的 检查点(Checkpoint) 位置。 - 从检查点开始扫描 WAL :
PG 从检查点记录的 LSN 开始,向后(向未来)扫描所有的 WAL 日志。 - 物理重放(Redo Phase) :
当 PG 读到一条 WAL 记录时,它会根据记录中的物理地址(表空间OID/数据库OID/RelFileNode/块号)去磁盘上把那个 8KB 的数据页读入内存,然后进行对比 :- 如果
WAL_Record.LSN > Page.pd_lsn:说明崩溃前,这条日志对应的修改在内存中发生了,但没来得及刷到磁盘上。PG 会直接重放(Redo) ,把二进制修改应用到这个数据页上。------整页面替换或者页偏移替换 - 如果
WAL_Record.LSN <= Page.pd_lsn:说明在崩溃前,这个修改其实已经成功刷写到磁盘上了。PG 会直接跳过这条记录。
- 如果
- 无需 Undo,直接标记结束 :
与 Oracle 或 MySQL 不同,PG 采用 MVCC 机制(多版本并发控制),更新和删除也是插入新行,旧数据并不会被覆盖。因此,PG 恢复时不需要 Undo(回滚)阶段。未提交事务产生的数据直接留在磁盘上,后面正常的事务死锁或可见性判断(Hint Bits/Commit Log)会自然视其为不可见。
三、 场景 2:基于备份的物理恢复(PITR - 归档恢复)
当发生人为误删、磁盘损坏,需要通过基础备份(Base Backup)和归档 WAL 恢复到特定时间点时,流程如下:
[基础备份] ➔ [应用归档 WAL] ➔ [持续重放] ➔ [到达指定时间点/LSN] ➔ [切断并打开数据库]
- 解压基础备份:将之前的物理备份(全量数据文件)解压到新的目录下。此时的数据文件是旧的。
- 配置
signal文件和postgresql.conf:
在数据目录下创建一个名为recovery.signal(PG 12+ 版本)的空文件,告诉系统:"启动时请进入恢复模式,不要对外提供服务"。同时配置restore_command告诉 PG 去哪里拿归档的 WAL 文件。 - 应用归档的物理块日志 :
PG 启动后开始源源不断地读取归档的 WAL 物理文件,用同样的方法(对比 LSN)将所有 8KB 数据页"往前推进",就像放电影快进一样。 - 时间点切断(Target Stop) :
如果你指定了recovery_target_time(比如恢复到昨晚 8 点),PG 在解析 WAL 时,一旦读到某条事务提交记录的时间戳 大于昨晚 8 点,或者到达了指定的 LSN,就会立刻停止重放。 - 数据库初始化(Timeline 提升) :
PG 会把recovery.signal文件自动删除,生成一个新的时间线(Timeline),将数据库状态改为可用,允许用户连接。此时,数据就完美回滚到了你指定的那个瞬间。
总结:为什么 PG 恢复不需要逻辑元数据?
正如您所说,PG 在恢复时,它眼里的世界没有"用户、表名、索引名",只有"文件、块号、偏移量和二进制字节"。
它只需要把 WAL 里的二进制字节,强行覆盖到磁盘上对应文件的对应位置即可。只要磁盘文件的物理结构在重放后是完好、一致的,等恢复结束、数据库正常打开后,用户通过逻辑 SQL 去查 pg_class 等元数据时,表和数据自然就都整整齐齐地展现出来了。这种"物理重放"绕过了复杂的逻辑解析,不仅速度极快 ,而且绝对准确。
您是在日常学习 PostgreSQL 的内核与运维原理 ,还是目前正在着手准备搭建一套基于 pg_backrest 或 barman 的 PITR 自动化备份恢复方案呢?我们可以聊聊具体工具在物理恢复时的最佳实践!
在 PostgreSQL 12 及以上版本中,完成基于 recovery.signal 的物理恢复并到达指定的恢复目标(Target)后,如果数据库配置了暂停(默认行为),它会停在那个瞬间。此时,你需要通过提升(Promote) 命令来让它删除 recovery.signal、提升时间线(Timeline)并切换为可读写状态。
以下是实现这一步的常用命令和操作方法:
方法一:使用 pg_ctl 命令行工具(最常用)
在操作系统的 shell 终端下(以 postgres 用户身份),直接执行 promote 指令。这是最直接的运维手段:
bash
# 执行 promote 命令
pg_ctl promote -D /path/to/your/data_directory
# 示例(假设数据目录为 /var/lib/postgresql/data)
pg_ctl promote -D /var/lib/postgresql/data
请谨慎使用此类代码。
执行后的系统行为:
- 信号文件
recovery.signal会被自动删除。 - 产生一条类似
archive recovery complete的日志。 - 数据库将 Timeline 自增(例如从
1提升到2),并生成一个新的.history文件(如00000002.history)。 - 数据库从只读/恢复状态转为正常的读写状态。
方法二:使用 SQL 语句(适合远程或在 SQL 客户端操作)
如果你已经连接到了这个处于恢复状态(通常此时是只读的 hot_standby)的数据库,可以直接通过 SQL 函数来触发提升:
sql
-- 执行此函数来结束恢复模式并提升时间线
SELECT pg_promote();
请谨慎使用此类代码。
注:该函数在 PostgreSQL 12+ 中可用,默认需要超级用户(superuser)权限执行。
💡 自动化进阶:如何让它"不需要输入命令"自动完成提升?
如果你在进行 PITR(基于时间点的恢复)时,希望它到达指定时间点后不需要人工干预、自动打开数据库 ,你可以在恢复前的 postgresql.conf(或 postgresql.auto.conf)中修改以下参数:
ini
# 默认值是 pause(暂停,等待你执行上述 promote 命令)
# 将其改为 promote,则到达目标时间点后会自动删除 recovery.signal 并打开数据库
recovery_target_action = 'promote'
请谨慎使用此类代码。
当 recovery_target_action = 'promote' 时,只要 PG 物理重放的 LSN 到达了你指定的 recovery_target_time 或 recovery_target_lsn,它就会在后台自动完成你提到的所有初始化动作,直接允许用户连接并写入。
请问您当前的数据库目前正停在 paused(暂停)状态 等待提升,还是在执行 pg_ctl promote 时遇到了什么阻碍或报错 ?如果需要,我可以帮您分析当前的 postgresql.conf 恢复参数配置!