pg_waldump --key-file-name status 查看wal 产生的db

  1. 终极逻辑排查:通过 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 日志的主要贡献者。
  1. 揪出元凶表:通过 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;

请谨慎使用此类代码。

  1. 如果非要从 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)。

  1. 运行 pg_waldump 命令 :
    打开终端,对指定的 WAL 文件进行解析(假设文件名为 000000010000000000000001):

    bash

    复制代码
    pg_waldump /path/to/pg_wal/000000010000000000000001 | head -n 20

    pg_waldump --data-encryption=128 --key-file-name= /data/pg_encryption/key.bin 0000000 00027 --stats

  2. 分析输出结果 :
    输出中会包含类似下面的记录: 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)
  3. 在数据库中查询对应的数据库名称 :
    获取到数据库 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 及以上版本支持)。

  1. 创建扩展 (需要超级用户权限):

    sql

    复制代码
    CREATE EXTENSION pg_walinspect;

    请谨慎使用此类代码。

  2. 查询某个历史 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 并填入该查询的方法:

  1. 获取当前系统最新的 LSN 区间

你可以通过以下 SQL 获取当前数据库正在写入的 LSN 位置,以此来决定查询的范围:

sql

复制代码
-- 查看当前最新的 WAL 写入位置 (LSN)
SELECT pg_current_wal_lsn();

请谨慎使用此类代码。

假设返回的结果是:0/1A2B3C4D

  1. 编写健康的查询语句

由于一条 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 会自动进入恢复模式:

  1. 寻找控制文件(global/pg_control) :
    PG 启动时先读取控制文件,发现状态是 in production(说明上次是非正常关闭),并找到上一次成功的 检查点(Checkpoint) 位置。
  2. 从检查点开始扫描 WAL :
    PG 从检查点记录的 LSN 开始,向后(向未来)扫描所有的 WAL 日志。
  3. 物理重放(Redo Phase) :
    当 PG 读到一条 WAL 记录时,它会根据记录中的物理地址(表空间OID/数据库OID/RelFileNode/块号)去磁盘上把那个 8KB 的数据页读入内存,然后进行对比 :
    • 如果 WAL_Record.LSN > Page.pd_lsn :说明崩溃前,这条日志对应的修改在内存中发生了,但没来得及刷到磁盘上。PG 会直接重放(Redo) ,把二进制修改应用到这个数据页上。------整页面替换或者页偏移替换
    • 如果 WAL_Record.LSN <= Page.pd_lsn :说明在崩溃前,这个修改其实已经成功刷写到磁盘上了。PG 会直接跳过这条记录。
  4. 无需 Undo,直接标记结束 :
    与 Oracle 或 MySQL 不同,PG 采用 MVCC 机制(多版本并发控制),更新和删除也是插入新行,旧数据并不会被覆盖。因此,PG 恢复时不需要 Undo(回滚)阶段。未提交事务产生的数据直接留在磁盘上,后面正常的事务死锁或可见性判断(Hint Bits/Commit Log)会自然视其为不可见。

三、 场景 2:基于备份的物理恢复(PITR - 归档恢复)

当发生人为误删、磁盘损坏,需要通过基础备份(Base Backup)和归档 WAL 恢复到特定时间点时,流程如下:

复制代码
[基础备份] ➔ [应用归档 WAL] ➔ [持续重放] ➔ [到达指定时间点/LSN] ➔ [切断并打开数据库]
  1. 解压基础备份:将之前的物理备份(全量数据文件)解压到新的目录下。此时的数据文件是旧的。
  2. 配置 signal 文件和 postgresql.conf :
    在数据目录下创建一个名为 recovery.signal(PG 12+ 版本)的空文件,告诉系统:"启动时请进入恢复模式,不要对外提供服务"。同时配置 restore_command 告诉 PG 去哪里拿归档的 WAL 文件。
  3. 应用归档的物理块日志 :
    PG 启动后开始源源不断地读取归档的 WAL 物理文件,用同样的方法(对比 LSN)将所有 8KB 数据页"往前推进",就像放电影快进一样。
  4. 时间点切断(Target Stop) :
    如果你指定了 recovery_target_time(比如恢复到昨晚 8 点),PG 在解析 WAL 时,一旦读到某条事务提交记录的时间戳 大于昨晚 8 点,或者到达了指定的 LSN,就会立刻停止重放。
  5. 数据库初始化(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 恢复参数配置!

相关推荐
SelectDB技术团队2 小时前
Snowflake 的开源替代怎么选:能力对照、三条部署路径与迁移执行
大数据·数据库·云原生·开源·snowflake·apache doris
Elastic 中国社区官方博客2 小时前
告别繁琐控制台:Kabanos 如何将 MongoDB Compass 的易用性带入 Elasticsearch
大数据·数据库·mongodb·elasticsearch·搜索引擎·全文检索
京东云开发者2 小时前
当我谈数据库高可用时我谈些什么
数据库
梦影_2 小时前
MySQL进阶教程(一)—— 储存过程与触发器(上)
开发语言·数据库·后端·sql·mysql·中间件
星禾元亨3 小时前
用结构化数据与干净实体信号,让技术内容被 AI 搜索引擎稳定引用
数据库·人工智能·自动化·创业创新
底层玩家老张3 小时前
45:1压缩、19.1倍查询:1956亿行时序数据复盘
数据库·经验分享·时序数据库·存储压缩·写入优化
qq_白羊座3 小时前
口头说的 JSON 和 MySQL 里的 JSON
数据库·mysql·json
Shaoshing3 小时前
多表查询
数据库
不爱学英文的码字机器3 小时前
从Q1迁到T1,同一个编号已经换了含义
前端·数据库·microsoft