# PostgreSQL 核心运行信号

LLMS 索引： [llms.txt](/llms.txt)

---

PostgreSQL 自带两类观察接口：

```text
dynamic current state
  当前 backend、锁、复制、进度

cumulative statistics
  自 reset 以来的 transaction、I/O、WAL、maintenance、statement
```

查询视图很简单，正确解释并不简单。以下事实可以同时成立：

```text
pg_stat_activity 当前没有 lock wait
过去五分钟 lock wait 曾导致用户超时

pg_stat_archiver.failed_count = 21
当前归档已经恢复并持续成功

pg_stat_io.read_time 增加
物理磁盘没有等量读取，因为 OS page cache 参与

replica WAL distance = 0
应用仍可能因为路由、事务快照或缓存读到旧结果

n_dead_tup = 0
表仍可能存在已分配但未归还给操作系统的空间
```

本节目标不是记住所有列，而是掌握一套读法：

```text
view
  -> source and update path
      -> current/cumulative/estimate/progress
          -> reset and snapshot
              -> independent corroboration
                  -> safe action
```

完整视图以当前版本官方文档为准：
[PostgreSQL 18 Monitoring Stats](https://www.postgresql.org/docs/18/monitoring-stats.html)。

## 25.2.1 会话、事务、等待与锁 {#item-25-2-1}

### 先确认统计功能是否开启

关键设置：

```sql
SELECT name, setting, unit, source
FROM pg_settings
WHERE name IN (
  'track_activities',
  'track_counts',
  'track_functions',
  'track_io_timing',
  'track_wal_io_timing',
  'stats_fetch_consistency'
)
ORDER BY name;
```

它们不是同一个开关：

| 设置 | 作用 | 关闭后的含义 |
|---|---|---|
| `track_activities` | 当前命令与开始时间 | activity 信息受限 |
| `track_counts` | 数据库/表等累计活动 | autovacuum 也依赖它 |
| `track_functions` | 函数调用统计 | 不代表函数没有运行 |
| `track_io_timing` | 数据文件 I/O timing | 时间未测量，不是零成本 |
| `track_wal_io_timing` | WAL I/O timing | WAL 时间未测量 |
| `stats_fetch_consistency` | 一个事务内统计读取一致性 | 影响缓存/快照行为 |

统计有开销，timing 尤其依赖平台时钟成本；但关闭后必须把“未测量”保留下来。
绝不能把：

```text
track_wal_io_timing=off
wal_write_time=0
```

解释为 WAL 写入没有花时间。

本章沙箱：

```text
track_activities       on
track_counts           on
track_functions        all
track_io_timing        on
track_wal_io_timing    off
stats_fetch_consistency cache
```

### `pg_stat_activity` 是当前 backend 视图

先使用不导出 query text 的聚合：

```sql
SELECT
  backend_type,
  state,
  wait_event_type,
  count(*) AS sessions,
  max(clock_timestamp() - xact_start)
    FILTER (WHERE xact_start IS NOT NULL) AS max_xact_age,
  max(clock_timestamp() - query_start)
    FILTER (WHERE query_start IS NOT NULL) AS max_query_age
FROM pg_stat_activity
GROUP BY backend_type, state, wait_event_type
ORDER BY backend_type, state NULLS LAST, wait_event_type NULLS LAST;
```

为什么带 `backend_type`？PostgreSQL 18 里不只有 client backend：

```text
autovacuum launcher / worker
background writer
checkpointer
walwriter / walsender / walreceiver
io worker
slotsync worker
logical replication worker
```

如果把后台进程与 client backend 混在一起，某个长期运行的 background worker
可能被误判为“用户 SQL 运行几小时”。

### `state` 与 `wait_event` 独立

常见错误：

```text
state = active
  therefore CPU is executing
```

实际：

```text
state=active, wait_event is null
  -> backend 正在运行，或刚好未被采样到等待

state=active, wait_event is not null
  -> SQL 仍是 active，但正在等待

state=idle, wait_event_type=Client
  -> 等客户端发下一条命令

state=idle in transaction
  -> 事务仍开着，可能保留 snapshot/lock/xmin
```

所以等待查询写成：

```sql
SELECT
  pid,
  backend_type,
  state,
  wait_event_type,
  wait_event,
  clock_timestamp() - xact_start AS xact_age,
  clock_timestamp() - query_start AS query_age
FROM pg_stat_activity
WHERE backend_type = 'client backend'
  AND state = 'active'
  AND wait_event IS NOT NULL
ORDER BY query_start;
```

这条查询没有读取 `query` 列。需要 SQL 上下文时，应在受限交互会话中按
`queryid`、application、database 和 owner 缩小范围，避免把全文复制进工单。

### wait event 是“正在等什么”，不是“根因”

`wait_event_type` 先把等待分大类：

```text
Lock
LWLock
IO
Client
IPC
Activity
Timeout
BufferPin
Extension
```

同一种等待可能有多种机制：

```text
Lock
  application transaction contention
  DDL conflicting with queries
  idle in transaction retaining locks

IO
  cache miss
  sequential scan
  checkpoint-related work
  WAL read/write
  extension access

Client
  server waits for client
  slow consumer
  application not reading results
```

因此：

```text
wait type
  -> affected backend/queryid
  -> blocker/resource
  -> user path
  -> corroborating counter/host evidence
```

才形成诊断。

PostgreSQL 18 的异步 I/O 引入 `io worker` 等 backend type 和相应等待。升级后
不要假设旧版 wait event 列表仍完整；dashboard 和规则要按当前版本校验。

### 当前统计在一个事务里可能保持不变

累计统计不是每次访问都无条件读取最新值。PostgreSQL 会把统计写入共享内存，
各进程最迟按一定节奏 flush；访问者又可能在当前事务内缓存读取结果。

这段会造成困惑：

```sql
BEGIN;
SELECT xact_commit FROM pg_stat_database WHERE datname = current_database();
-- 等待或在其他连接产生工作
SELECT xact_commit FROM pg_stat_database WHERE datname = current_database();
COMMIT;
```

在 `stats_fetch_consistency=cache` 下，同一事务后续读取可能继续看到缓存值。
诊断时优先：

```text
每次采样使用短事务
不要在长事务里刷新 dashboard 数据
必要时调用 pg_stat_clear_snapshot()
记录采样时间和 stats_fetch_consistency
```

`pg_stat_clear_snapshot()` 清的是当前 session 的统计 snapshot，不是重置全局
统计；不要与 `pg_stat_reset*` 混淆。

### 统计更新也有时间边界

累计统计通常在 transaction 完成后才反映：

```text
active transaction
  current activity can show it
  cumulative table/database changes may not yet be flushed
```

因此调查进行中的大事务：

- activity 看当前 transaction age；
- locks 看当前持有/等待；
- progress 看支持的维护动作；
- WAL/IO counter 看累计变化；
- 不等待累计表统计“先证明它存在”。

### crash、恢复与复制会改变统计历史

PostgreSQL 正常关闭会保存累计统计；非正常关闭、从 base backup 恢复或 PITR
可能导致统计 reset。跨 failover 比较时：

```text
same metric name
  does not imply same counter history
```

必须同时保存：

- member/timeline；
- stats reset；
- postmaster start；
- role transition；
- source instance；
- sampling window。

### long query 与 long transaction 不同

```text
query_age = now - query_start
xact_age  = now - xact_start
```

场景：

| 状态 | query age | xact age | 风险 |
|---|---:|---:|---|
| active long query | 长 | 约等于或短于 xact | 执行/等待资源 |
| idle in transaction | 当前 query 已结束 | 长 | lock、xmin、vacuum |
| active in old transaction | 当前 query 短 | 很长 | snapshot/业务批次 |
| idle | 上条 query 的开始时间不代表在执行 | 无 transaction | 通常只是连接 |

因此不要用 `query_start` 对所有 state 排序然后自动 cancel。

### `idle in transaction` 为什么危险

它可能：

- 保留 row/table lock；
- 持有旧 snapshot；
- 阻碍 dead tuple 回收；
- 拉长 `backend_xmin`；
- 占用 connection/pool slot；
- 让后续应用错误更难定位。

诊断字段：

```sql
SELECT
  pid,
  datname,
  usename,
  application_name,
  state,
  clock_timestamp() - xact_start AS xact_age,
  wait_event_type,
  wait_event,
  backend_xid,
  backend_xmin
FROM pg_stat_activity
WHERE backend_type = 'client backend'
  AND state = 'idle in transaction'
ORDER BY xact_start;
```

取消或终止是变更动作，不属于本章 L0 采集。先确认：

- owner/application；
- transaction 是否仍有不可重试副作用；
- pool mode；
- unknown commit outcome；
- cancel 与 terminate 的差异；
- rollback/重连影响；
- 用户症状是否关联。

### `pg_locks` 是锁申请，不是完整业务解释

安全聚合：

```sql
SELECT locktype, mode, granted, count(*) AS locks
FROM pg_locks
GROUP BY locktype, mode, granted
ORDER BY locktype, mode, granted;
```

找 blocker 可使用 `pg_blocking_pids()`：

```sql
SELECT
  a.pid AS waiting_pid,
  a.datname,
  a.usename,
  a.application_name,
  a.wait_event_type,
  a.wait_event,
  clock_timestamp() - a.query_start AS wait_age,
  pg_blocking_pids(a.pid) AS blocking_pids
FROM pg_stat_activity AS a
WHERE cardinality(pg_blocking_pids(a.pid)) > 0
ORDER BY a.query_start;
```

这个函数给出 blocker PID，但仍要判断：

```text
direct blocker or blocker behind blocker?
transaction or prepared transaction?
DDL, row lock, advisory lock, relation extension?
which user journey?
is blocker making progress?
what is safe to cancel?
```

### 锁图而不是最长列表

事故中更有用的是：

```text
waiting backend
  -> direct blocker
      -> root blocker
          -> owner / transaction age / state
```

并保存：

- edge 采样时刻；
- blocker state；
- `backend_xid/xmin`；
- queryid，而不是默认 query text；
- application/release；
- lock type/mode；
- user impact。

一条锁边可能瞬间消失。诊断包应保存有界快照，而不是事后只看当前视图。

### deadlock 与普通阻塞

普通 lock wait 可以持续；deadlock 是一个等待环，PostgreSQL 会检测并中止其中
一个 transaction。

观察：

```text
pg_stat_database.deadlocks       cumulative
log_lock_waits                   waits beyond deadlock_timeout
deadlock error log               discrete event
application retry/outcome        user semantics
```

`log_lock_waits=on` 只在等待超过 `deadlock_timeout` 后记录，短等待不会出现。
日志“没有 lock wait”不能证明没有短暂锁竞争。

### 权限边界

普通用户只能看到其他 session 的有限信息。`pg_read_all_stats` 能读取全库统计和
其他 session 的更多信息，但这仍然是高敏感可观测权限：

- query text 可能含业务值；
- application name 可能带身份；
- client address 暴露拓扑；
- activity 能推断业务行为。

建议：

```text
exporter role
  stable, narrow, machine-only

interactive diagnostic role
  time-bounded, reviewed, pg_read_all_stats or narrower

evidence export
  aggregate and redact, no query text/client address
```

不要因为它不是 superuser 就把它当低风险权限。

### 查询本身也会进入观察结果

读取 `pg_stat_activity` 时，你自己的查询也是 active；访问许多系统视图也会拿
`AccessShareLock`。本章正式快照出现的 relation locks 就包括采集查询本身。

因此：

- 标识 collector application；
- 从结果中区分自身；
- 限制 statement timeout；
- 不在 tight loop 高频轮询；
- 避免一次展开所有 query text；
- 将 observer effect 写进证据。

## 25.2.2 缓冲、I/O、WAL、检查点与复制 {#item-25-2-2}

### 数据路径不是“内存或磁盘”二选一

一个 PostgreSQL page 读取可能经过：

```text
PostgreSQL shared buffers
  -> operating-system page cache
      -> filesystem / block layer
          -> physical or virtual storage
```

所以：

```text
PostgreSQL read()
  may be satisfied by OS cache

shared buffer hit
  does not require an OS read

device read
  may be caused by another process
```

`pg_stat_io` 明确不区分物理磁盘和 OS page cache。必须结合 node/block-device
证据。

### `pg_stat_database` 给出数据库级累计轮廓

常用字段：

```sql
SELECT
  datname,
  numbackends,
  xact_commit,
  xact_rollback,
  blks_read,
  blks_hit,
  temp_files,
  temp_bytes,
  deadlocks,
  blk_read_time,
  blk_write_time,
  stats_reset
FROM pg_stat_database
WHERE datname IS NOT NULL
ORDER BY datname;
```

它适合：

- database-level rate；
- hit/read 变化；
- temp spill 趋势；
- rollback/deadlock 变化；
- reset-aware baseline。

不适合：

- 归因到具体 query；
- 直接推物理 IOPS；
- 用 `blks_hit / (blks_hit + blks_read)` 单独判断内存是否足够；
- 比较不同 reset 区间的裸总数。

### cache hit ratio 不是性能分数

$$
\text{hit ratio}
=
\frac{\Delta hits}
{\Delta hits + \Delta reads}
$$

即使正确用窗口增量，也受 workload 影响：

- 大表顺序扫描天然产生 reads；
- 小表热点容易高命中；
- OS cache 命中仍记为 PostgreSQL read；
- 低 hit 可能是合理批处理；
- 高 hit 不代表 CPU、lock 或 plan 健康。

把它作为 workload 特征，不要设一个跨服务的“低于 99% 就 page”。

### `pg_stat_io` 按谁、什么对象、什么上下文拆分

PostgreSQL 18 可按：

```text
backend_type
object
context
```

观察：

```text
reads / read_bytes / read_time
writes / write_bytes / write_time
extends / extend_bytes / extend_time
hits / evictions / reuses
fsyncs / fsync_time
```

先聚合非零工作：

```sql
SELECT
  backend_type,
  object,
  context,
  sum(reads) AS reads,
  sum(read_bytes) AS read_bytes,
  sum(read_time) AS read_ms,
  sum(writes) AS writes,
  sum(write_bytes) AS write_bytes,
  sum(write_time) AS write_ms,
  sum(fsyncs) AS fsyncs,
  sum(fsync_time) AS fsync_ms
FROM pg_stat_io
GROUP BY backend_type, object, context
HAVING
  coalesce(sum(reads), 0)
  + coalesce(sum(writes), 0)
  + coalesce(sum(fsyncs), 0) > 0
ORDER BY backend_type, object, context;
```

窗口诊断需要两次快照求 delta 或 exporter counter rate。裸累计值只说明 reset
以来总量。

### timing 要与 byte/count 一起读

```text
read_time rises, read_bytes rises
  -> more work or slower work

read_time per read rises
  -> average operation cost rises, but distribution unknown

read_bytes rises, device reads stable
  -> OS cache may serve more reads

timing is zero and tracking is off
  -> unknown, not free
```

平均值会掩盖 tail。需要时结合：

- block-device latency histogram；
- request latency histogram；
- trace sampled tail；
- queryid-level I/O；
- workload mix。

### buffer、backend 与 checkpoint 写入

dirty buffer 可能由 backend、background writer、checkpointer 等路径写出。
`pg_stat_io` 帮助按 backend type 区分；`pg_stat_checkpointer` 给 checkpoint
累计结果。

PostgreSQL 18：

```sql
SELECT *
FROM pg_stat_checkpointer;
```

重要字段包括：

```text
num_timed
num_requested
num_done
restartpoints_*
write_time
sync_time
buffers_written
slru_written
stats_reset
```

解释：

```text
requested checkpoint rate rises
  -> workload/config/administrative events may force checkpoints

write_time dominates
  -> checkpoint spreads writes

sync_time spikes
  -> fsync phase or storage path needs correlation

buffers_written rises
  -> more dirty data, not automatically a problem
```

不要只看一次 `write_time` 总值。计算每窗口的 checkpoint count、buffer、write
和 sync delta，再与用户 latency、WAL rate 和 host I/O 对齐。

### checkpoint 不是越少越好

过频可能增加写入压力和 full-page image；过稀可能：

- 增加 crash recovery 时间；
- 需要更多 WAL；
- 在 checkpoint 集中更多工作；
- 改变恢复和容量特征。

任何调参要结合：

```text
checkpoint completion
WAL generation
dirty buffer path
storage latency
recovery objective
memory and workload
```

本章只观察，不修改 `checkpoint_timeout`、`max_wal_size` 或 completion target。

### `pg_stat_wal` 是 WAL 生成轮廓

```sql
SELECT
  wal_records,
  wal_fpi,
  wal_bytes,
  wal_buffers_full,
  stats_reset
FROM pg_stat_wal;
```

用途：

- WAL byte rate；
- full-page image 比例变化；
- WAL buffer full；
- 与 write workload、checkpoint、replication、archive 对齐。

不能直接说明：

- 哪个 query 产生 WAL；
- WAL 是否已归档；
- replica 是否已回放；
- recovery 能否成功。

query 归因需要 `pg_stat_statements.wal_bytes` 等；归档与恢复需要另外的证据。

### full-page image 的上下文

checkpoint 后 page 首次修改可能记录 full-page image，以支持恢复。`wal_fpi`
变化与：

- checkpoint 频率；
- 工作集；
- `full_page_writes`；
- page 修改模式；
- compression；
- backup/recovery policy

相关。不要看到 FPI 高就关闭安全机制。

### `pg_stat_archiver` 同时有 counter 和最后事件

```sql
SELECT
  archived_count,
  last_archived_wal,
  last_archived_time,
  failed_count,
  last_failed_wal,
  last_failed_time,
  stats_reset
FROM pg_stat_archiver;
```

三种不同问题：

```text
历史是否失败过？
  failed_count since reset

最近是否出现新失败？
  increase(failed_count[window])

当前是否推进？
  last success age + WAL generation + archive queue
```

沙箱快照：

```text
failed_count           21
last_failed_time       18:57:58Z
last_archived_time     22:27:54Z
```

最近成功晚于失败，说明历史 counter 非零不能证明当前仍失败。

候选告警：

```promql
pg36:archive_failures:increase15m > 0
and on (cls, ins, ip)
(time() - pg_archiver_finish_time) > 900
```

这仍只是 recovery risk candidate。下一步要复核：

- 当前 WAL 是否继续生成；
- archive command/pgBackRest 状态；
- repository；
- latest archive；
- backup/WAL coverage；
- 最近 restore drill。

“归档恢复”不等于“恢复就绪”。

### replication 有位置、时间、状态三套语义

primary：

```sql
SELECT
  application_name,
  state,
  sync_state,
  sent_lsn,
  write_lsn,
  flush_lsn,
  replay_lsn,
  pg_wal_lsn_diff(sent_lsn, replay_lsn) AS sent_replay_gap_bytes,
  write_lag,
  flush_lag,
  replay_lag
FROM pg_stat_replication
ORDER BY application_name;
```

replica：

```sql
SELECT
  status,
  sender_host,
  sender_port,
  written_lsn,
  flushed_lsn,
  latest_end_lsn,
  last_msg_send_time,
  last_msg_receipt_time,
  latest_end_time
FROM pg_stat_wal_receiver;
```

公开/长期证据不一定应保存 sender/client address；可以保留 member identity、
state、sync_state 和位置差。

### 位置 gap

```text
sent - write    network/receiver write path
write - flush   replica flush path
flush - replay  replay/apply path
```

位置差按 WAL byte 计，不是秒：

$$
\text{catch-up time}
\ne
\frac{\text{WAL gap bytes}}{\text{current generation rate}}
$$

因为 replay throughput、workload、conflict、I/O 和 future WAL 都会变化。它可
用于容量与趋势，不应直接承诺 RTO。

### 时间 lag 可能是 `NULL`

`write_lag`、`flush_lag`、`replay_lag` 是最近同步交互产生的测量，并不保证
持续给出“当前落后秒数”。空闲系统可能为 `NULL`；这不等于零，也不等于故障。

使用：

- LSN distance；
- connection/state；
- last message；
- known commit probe；
- workload/WAL generation

共同解释。

本章正式 SQL 快照中，两条 streaming async replica 的 LSN gap 都为 0，而
lag interval 为 `NULL`。这正好说明：

```text
NULL time lag
  can coexist with zero position gap in an idle/current sample
```

### sync_state 与 commit durability

`sync_state=async` 表示它不是当前同步确认的一部分。即使 gap 为零：

- 后续 commit 仍可能尚未复制；
- primary 立即丢失会有 write gap 风险；
- 应用 commit acknowledgment 语义取决于 `synchronous_commit` 和配置；
- user freshness 取决于读取路径。

把 `pg_stat_replication` 与第 20 章 HA 合同一起读，不要从瞬时 gap 倒推出
durability guarantee。

### replication slot 与 WAL retained risk

除了 replica gap，还要观察：

```sql
SELECT
  slot_name,
  slot_type,
  active,
  wal_status,
  safe_wal_size,
  pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)
    AS retained_bytes
FROM pg_replication_slots
ORDER BY slot_name;
```

slot identity 可能属于 extension/consumer；不要自动删除 inactive slot。风险：

- WAL retention 填满磁盘；
- logical consumer 落后；
- slot 失效；
- consumer 被误认作废弃。

容量规则默认 ticket，动作需 owner 和 consumer 证据。

### 复制冲突

hot standby 查询可能与 replay 冲突。观察：

- `pg_stat_database_conflicts`；
- replica activity；
- query cancellation log；
- feedback/delay 设置；
- retained xmin/WAL；
- user read path。

降低冲突的设置可能增加 bloat 或 freshness lag，不能只优化一张图。

## 25.2.3 vacuum、冻结、膨胀与对象增长 {#item-25-2-3}

### vacuum 有四个主要目的

常规 vacuum 不是“清空表”，而是：

1. 回收 dead row version，使空间可在表内复用；
2. 更新 visibility map，支持 index-only scan 等；
3. 防止 transaction ID wraparound；
4. 维护统计/冻结等运行状态。

`ANALYZE` 更新 planner statistics；它与 vacuum 可以一起运行，但目的不同。

### autovacuum 依赖统计

`track_counts=on` 不只是“多收指标”。autovacuum 使用累计活动决定何时处理表。
关闭它会影响维护机制。

每表触发近似由：

```text
vacuum threshold
  base threshold + scale factor * relation tuples

analyze threshold
  base threshold + scale factor * relation tuples
```

决定，并受 insert threshold、per-table storage parameter、cost limit、worker 数、
全局设置等影响。

不能只看“autovacuum process 存在”。要看：

- table change pressure；
- last vacuum/autovacuum；
- dead tuples；
- analyze freshness；
- blockers；
- progress；
- freeze age；
- runtime/duration。

### 表级统计是 estimate + counter

```sql
SELECT
  schemaname,
  relname,
  n_live_tup,
  n_dead_tup,
  n_mod_since_analyze,
  n_ins_since_vacuum,
  last_vacuum,
  last_autovacuum,
  last_analyze,
  last_autoanalyze,
  vacuum_count,
  autovacuum_count,
  analyze_count,
  autoanalyze_count
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC NULLS LAST
LIMIT 50;
```

`n_live_tup`、`n_dead_tup` 是估算；count 是自 reset 累计。它们适合找候选，不
适合直接计算“精确 bloat 百分比”。

### dead tuple 不等于 bloat

```text
dead tuple
  an old row version no longer visible to current snapshots

reusable free space
  vacuum processed space available for future tuples

table bloat
  allocated pages exceed what current data/layout needs

filesystem size
  file blocks currently allocated
```

vacuum 后 `n_dead_tup` 可能下降，但文件通常不缩小，因为普通 vacuum 将空间
留给表内复用。需要归还操作系统的操作通常更重，涉及 rewrite/lock/extra disk；
不能因为文件没缩就说 vacuum 失败。

### 长事务如何阻止回收

MVCC 需要保留旧版本给仍可能看见它的 snapshot。长 transaction、prepared
transaction、replication slot/feedback 等可能拉住 xmin：

```text
updates/deletes create dead versions
  -> vacuum evaluates global visibility horizon
      -> old xmin still needs them
          -> cannot remove
              -> dead tuples/object size grow
```

观察：

```sql
SELECT
  pid,
  datname,
  usename,
  application_name,
  state,
  backend_xid,
  backend_xmin,
  clock_timestamp() - xact_start AS xact_age
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;
```

还要查：

- prepared transactions；
- replication slots；
- replica feedback；
- vacuum progress；
- table-level age。

“找到最老 PID 就 terminate”不是安全策略。

### freeze age 是剩余空间，不是普通 latency

transaction ID 是有限循环空间。旧 tuple 必须被 freeze，避免 wraparound 后
可见性灾难。

数据库级：

```sql
SELECT
  datname,
  age(datfrozenxid) AS xid_age,
  mxid_age(datminmxid) AS multixact_age
FROM pg_database
ORDER BY xid_age DESC;
```

表级：

```sql
SELECT
  n.nspname,
  c.relname,
  age(c.relfrozenxid) AS xid_age,
  mxid_age(c.relminmxid) AS multixact_age,
  pg_total_relation_size(c.oid) AS total_bytes
FROM pg_class AS c
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'm')
  AND n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY xid_age DESC
LIMIT 50;
```

不要把阈值写成与配置无关的魔法数字。应比较：

```text
current age
autovacuum_freeze_max_age
table override
consumption rate
vacuum throughput
blocker
remaining time with uncertainty
```

本章候选 `PG36FreezeAgeHorizon` 使用固定数字只是 isolated lab 的测试输入，
明确标为 proposed；生产规则应由当前配置和容量政策生成。

### anti-wraparound autovacuum 的特殊性

为防 wraparound 启动的 autovacuum 通常不会像普通 autovacuum 那样轻易被冲突
动作自动打断。不要把它当“可以随时 kill 的后台噪声”。

如果已经进入紧急区：

- 停止增加风险的长事务；
- 找出不能推进的表与 blocker；
- 评估 I/O/空间/锁；
- 按 runbook 控制维护；
- 不并行执行未经评估的 rewrite；
- 保留 evidence。

最优策略是在容量 horizon 阶段用 ticket 解决，而不是等 emergency page。

### progress view 是当前进度，不是历史

vacuum：

```sql
SELECT
  pid,
  datid,
  relid,
  phase,
  heap_blks_total,
  heap_blks_scanned,
  heap_blks_vacuumed,
  index_vacuum_count,
  num_dead_item_ids,
  max_dead_item_ids
FROM pg_stat_progress_vacuum;
```

PostgreSQL 还为：

- `ANALYZE`；
- `CREATE INDEX` / `REINDEX`；
- `CLUSTER` / `VACUUM FULL`；
- `COPY`；
- base backup

提供相应 progress view，具体列按版本文档。

没有行只表示当前没有该动作被报告，不表示：

- 从未运行；
- 上次成功；
- 下一次会成功；
- 没有被瞬间启动后失败。

历史需要日志、事件和累计 count。

### progress 百分比可能不单调

不同 phase 使用不同总量；并行、索引清理和 dead item cycle 也会改变解释。
不要把：

```text
heap_blks_scanned / heap_blks_total
```

当成整个 vacuum 的精确完成百分比。应同时显示 phase 和相关量。

### analyze freshness

planner statistics 变旧会导致估算偏差。候选信号：

```text
n_mod_since_analyze
last_analyze / last_autoanalyze
table size
query plan estimate vs actual
statistics target
column distribution
```

`n_mod_since_analyze` 是估算/累计线索，不是每行精确 change counter。对热点、
分区和高度偏斜列，需要 workload-aware 策略。

### 对象增长要拆分

```sql
SELECT
  n.nspname,
  c.relname,
  pg_relation_size(c.oid) AS heap_bytes,
  pg_indexes_size(c.oid) AS index_bytes,
  pg_total_relation_size(c.oid) AS total_bytes
FROM pg_class AS c
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'm')
  AND n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY total_bytes DESC
LIMIT 50;
```

增长可能来自：

- 正常业务数据；
- index 数量；
- TOAST；
- dead/reusable space；
- fillfactor；
- 分区保留；
- 临时/中间对象；
- rewrite；
- 失控 batch。

不要只按总大小 page。需要：

```text
growth rate
retention expectation
free-space horizon
maintenance/rewrite headroom
business volume
owner plan
```

容量是预测和计划问题，默认 ticket。

### partition 会改变聚合方式

父表和各分区的：

- size；
- table stats；
- autovacuum；
- analyze；
- index；
- freeze age

需要分别观察，再按业务分区策略聚合。只看父表可能近乎空；只列每个分区又会
产生巨大 cardinality。

推荐：

```text
metric
  aggregate by parent / age band / size band

dashboard
  top-N and drill-down

SQL
  on-demand exact partition inventory
```

不要把每个临时分区名永久做成高基数 alert label。

### bloat estimate 的边界

extension 或 SQL 估算 bloat 常依赖：

- row width；
- null bitmap；
- alignment；
- fillfactor；
- statistics；
- page sample；
- index type。

它是排序候选，不是字节级财务账。要做重操作前：

1. 复核对象大小与增长趋势；
2. 判断空间能否复用；
3. 找出生成机制和 blocker；
4. 评估 rewrite/lock/replication/WAL/backup 影响；
5. 准备额外磁盘和回退/前滚；
6. 在维护窗口验证。

本章不会自动 `VACUUM FULL` 或 `REINDEX`。

### 把三类维护信号分开

| 类别 | 问题 | 典型动作 |
|---|---|---|
| 运行正确性 | wraparound 是否接近 | 高优先级维护/停止风险来源 |
| 性能卫生 | dead tuple、stats 是否影响 workload | vacuum/analyze 调整与 blocker 修复 |
| 容量 | 对象/索引/WAL 是否耗尽空间 | retention、扩容、结构优化、rewrite 计划 |

它们的 severity、owner 和时间尺度不同。一个“表大”告警不能同时代表三者。

### 沙箱快照如何读

正式采集在 `pg-test-1` 观察到：

```text
user tables             1
estimated dead tuples   0
max table freeze age    1,050
```

这不是“vacuum 永远健康”的证明：

- synthetic workload 很小；
- 只有一次瞬时快照；
- 没有长期增长率；
- 没有生产 transaction rate；
- 没有生产配置与 margin；
- estimate 可能变化。

可以得出的结论只有：

```text
at captured_at,
the declared sandbox target reported this small current baseline.
```

### PostgreSQL 信号最小关联表

| 用户症状 | PG 入口 | 原生证据 | 外部复核 |
|---|---|---|---|
| latency | active/wait | activity、locks、queryid | pool、host、trace |
| errors | rollback/deadlock | database counter、logs | app outcome |
| stale read | replica path | replication position/state | commit token probe |
| write latency | WAL/checkpoint | wal、checkpointer、I/O | device、app histogram |
| query spill | temp bytes | database、statement、logs | plan/work_mem policy |
| vacuum delay | old xmin | activity、table stats、progress | workload/change event |
| freeze risk | XID age | database/class age | rate/horizon/capacity |
| recovery risk | archive | archiver + WAL | pgBackRest + restore drill |

### 本节验收

你应当能解释：

1. `active` 为什么仍可能在等待；
2. 同一 transaction 为什么可能读到缓存的累计统计；
3. `track_wal_io_timing=off` 为什么不能把时间解释为零；
4. `pg_stat_io` 为什么不等于物理磁盘 I/O；
5. `failed_count>0` 为什么不等于当前归档失败；
6. time lag `NULL` 与 WAL gap `0` 为什么可以同时出现；
7. WAL gap `0` 为什么不能证明用户新鲜度；
8. `n_dead_tup` 为什么不等于精确 bloat；
9. 普通 vacuum 为什么通常不缩小文件；
10. progress view 没有行为什么不证明历史成功；
11. query、lock、vacuum 和 freeze 信号分别对应哪种动作；
12. 任何取消、终止、reset、vacuum 或配置变更为什么不属于 L0 观察。

---

[上一节：从问题选择可观测信号](../01/) · [返回本章目录](../) · [下一节：SQL 可观测基线](../03/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
