运维管理30 分钟阅读
PostgreSQL VACUUM、表膨胀与事务 ID 冻结 100 条命令
PostgreSQL 更新或删除行时,旧版本不会立刻从数据文件里消失。VACUUM 负责回收可再利用的空间,也负责推进事务 ID 冻结边界。平时看到磁盘涨、死元组多,或日志里出现 wraparound 警告,不能只凭一张表的大小就决定跑 `VACUUM FULL`。
2026年9月16日阅读—点赞—收藏—
dba100postgresqlscenario
100 条命令系列文章专栏
PostgreSQL 更新或删除行时,旧版本不会立刻从数据文件里消失。VACUUM 负责回收可再利用的空间,也负责推进事务 ID 冻结边界。平时看到磁盘涨、死元组多,或日志里出现 wraparound 警告,不能只凭一张表的大小就决定跑 `VACUUM FULL`。
PostgreSQL 更新或删除行时,旧版本不会立刻从数据文件里消失。VACUUM 负责回收可再利用的空间,也负责推进事务 ID 冻结边界。平时看到磁盘涨、死元组多,或日志里出现 wraparound 警告,不能只凭一张表的大小就决定跑 VACUUM FULL。
这篇把表统计、autovacuum、冻结年龄和膨胀判断放在一起。先弄清是哪张表增长、VACUUM 为什么没跟上,再决定要不要人工维护。需要排查时可以直接翻,建议收藏并分享给值班同事。
PostgreSQL VACUUM 与冻结年龄排查图
示例为 PostgreSQL 17,SQL 默认连接目标数据库。统计值可能有延迟,死元组是估计数;涉及重写表和索引的操作要另外评估锁、磁盘和执行窗口。
1SELECT datname, pg_size_pretty(pg_database_size(oid)) AS database_size2FROM pg_database3WHERE datallowconn4ORDER BY pg_database_size(oid) DESC;先确认是不是整个库都涨了。这个结果是数据库对象占用,不包括 WAL 归档和外部备份目录。
1SELECT schemaname, relname,2 pg_size_pretty(pg_total_relation_size(relid)) AS total_size,3 pg_total_relation_size(relid) AS total_bytes4FROM pg_stat_user_tables5ORDER BY total_bytes DESC6LIMIT 30;pg_total_relation_size 包含表、索引和 TOAST。它适合找大对象,不能直接把大小当成膨胀量。
1SELECT pg_size_pretty(pg_relation_size('public.orders'::regclass)) AS heap,2 pg_size_pretty(pg_indexes_size('public.orders'::regclass)) AS indexes,3 pg_size_pretty(pg_total_relation_size('public.orders'::regclass)4 - pg_relation_size('public.orders'::regclass)5 - pg_indexes_size('public.orders'::regclass)) AS toast_and_aux;替换成现场表名。若总大小主要来自索引,后面就不能只盯表的 VACUUM;若来自 TOAST,还要继续查大字段的更新方式。
1SELECT schemaname, relname, n_live_tup, n_dead_tup,2 last_vacuum, last_autovacuum, vacuum_count, autovacuum_count3FROM pg_stat_user_tables4ORDER BY n_dead_tup DESC5LIMIT 30;n_dead_tup 是估计值。高值且 last_autovacuum 长时间没推进,才值得继续看阈值、进程和长事务;数值为零也不能证明表没有空间浪费。
1SELECT schemaname, relname, n_mod_since_analyze,2 last_analyze, last_autoanalyze3FROM pg_stat_user_tables4ORDER BY n_mod_since_analyze DESC5LIMIT 30;VACUUM 解决旧版本清理,ANALYZE 更新优化器统计。大量变更后执行计划异常时,不要把统计信息过旧和表膨胀混成一个问题。
1SELECT name, setting, unit, source2FROM pg_settings3WHERE name IN ('autovacuum', 'autovacuum_max_workers',4 'autovacuum_vacuum_threshold', 'autovacuum_vacuum_scale_factor',5 'autovacuum_vacuum_insert_threshold',6 'autovacuum_vacuum_insert_scale_factor')7ORDER BY name;普通清理按基础阈值加表行数比例触发,大表采用默认比例时可能积累很多死元组。还要核对表级存储参数,不能只看全局配置。
1SELECT c.oid::regclass AS table_name, c.reloptions2FROM pg_class AS c3WHERE c.oid = 'public.orders'::regclass;reloptions 为 NULL 通常表示没有表级覆盖。看到 autovacuum_enabled=false 时也别认为冻结风险已关闭;达到事务 ID 安全阈值时仍会强制执行防回卷 VACUUM。
1SELECT a.pid, a.backend_type, a.state, a.wait_event_type,2 a.wait_event, a.query_start, a.query3FROM pg_stat_activity AS a4WHERE a.backend_type = 'autovacuum worker'5 OR a.query ILIKE 'VACUUM%'6ORDER BY a.query_start;读进程和等待事件。VACUUM 在运行但死元组仍持续增长,可能是任务追不上写入,也可能有长事务阻止旧版本清理;要看后续指标再判断。
1SELECT datname, age(datfrozenxid) AS xid_age2FROM pg_database3ORDER BY xid_age DESC;datfrozenxid 是数据库内最老未冻结事务 ID 的下界。年龄高时要继续定位具体表和 TOAST,而不是直接给所有表跑 VACUUM FULL。
1SELECT c.oid::regclass AS table_name,2 greatest(age(c.relfrozenxid), age(t.relfrozenxid)) AS xid_age3FROM pg_class AS c4LEFT JOIN pg_class AS t ON t.oid = c.reltoastrelid5WHERE c.relkind IN ('r', 'm')6ORDER BY xid_age DESC7LIMIT 30;参考官方的查询方式,把 TOAST 一起算进去。最老的表未必最大;如果年龄接近 autovacuum_freeze_max_age,优先看防回卷 VACUUM 是否能运行以及有没有长期保留旧 XID 的会话或复制槽。
1SELECT pid, datname, relid::regclass AS table_name, phase,2 heap_blks_total, heap_blks_scanned, heap_blks_vacuumed3FROM pg_stat_progress_vacuum4ORDER BY pid;这张视图只显示正在执行的普通 VACUUM;VACUUM FULL 的进度在 pg_stat_progress_cluster。扫描块数包含借助可见性映射跳过的块,不能把它当成实际逐页读盘量。
1SELECT pid, relid::regclass AS table_name, phase,2 index_vacuum_count, dead_tuple_bytes,3 max_dead_tuple_bytes, indexes_total, indexes_processed4FROM pg_stat_progress_vacuum5ORDER BY index_vacuum_count DESC;index_vacuum_count 反复增长,说明一次扫描里要多轮清索引。结合死元组量、索引个数以及 maintenance_work_mem/autovacuum_work_mem 判断原因,别只看扫描百分比。
1SELECT a.pid, p.relid::regclass AS table_name, p.phase,2 a.wait_event_type, a.wait_event, a.state3FROM pg_stat_progress_vacuum AS p4JOIN pg_stat_activity AS a USING (pid)5ORDER BY a.pid;VacuumDelay 可能是成本限速;Lock、IO 或长时间不变的阶段需要分别追。state='active' 也可能正在等待,不能只看状态列。
1SELECT pid, datname, usename, state, xact_start,2 now() - xact_start AS xact_age,3 backend_xid, backend_xmin, left(query, 120) AS query4FROM pg_stat_activity5WHERE xact_start IS NOT NULL6ORDER BY xact_start7LIMIT 30;特别留意 idle in transaction。长期持有快照可能让旧行版本暂时无法清理;先确认对应业务连接和事务用途,再决定如何结束,不要看见最老 PID 就直接终止。
1SELECT pid, usename, state, xact_start,2 age(backend_xmin) AS xmin_age,3 left(query, 120) AS query4FROM pg_stat_activity5WHERE backend_xmin IS NOT NULL6ORDER BY xmin_age DESC7LIMIT 30;这比仅按事务开始时间排序更贴近 VACUUM 的可见性边界。结果可能因权限受限而缺少其他会话详情,诊断账号需有读取全局统计的权限。
1SELECT slot_name, slot_type, database, active,2 xmin, age(xmin) AS xmin_age,3 catalog_xmin, age(catalog_xmin) AS catalog_xmin_age4FROM pg_replication_slots5WHERE xmin IS NOT NULL OR catalog_xmin IS NOT NULL6ORDER BY greatest(coalesce(age(xmin), 0),7 coalesce(age(catalog_xmin), 0)) DESC;复制槽的 xmin 和 catalog_xmin 会限制对应旧版本的清理。槽不活跃也不能直接删除:仍在使用它的备库或逻辑订阅可能需要重建。先找到消费者和保留目的。
1-- 在主库执行2SELECT application_name, client_addr, state,3 backend_xmin, age(backend_xmin) AS xmin_age4FROM pg_stat_replication5WHERE backend_xmin IS NOT NULL6ORDER BY xmin_age DESC;启用 hot_standby_feedback 的备库可能向主库报告它仍需要的旧快照。复制延迟、长读事务和主库表膨胀要结合起来看;这里为空不能证明备库完全没有影响。
1SELECT gid, "transaction" AS xid, age("transaction") AS xid_age,2 prepared, owner, database3FROM pg_prepared_xacts4ORDER BY xid_age DESC5LIMIT 30;未提交或回滚的 prepared transaction 也可能保留旧 XID。是否提交必须由业务事务协调方确认;贸然回滚会破坏跨系统事务的一致性。
1SELECT name, setting, unit, source2FROM pg_settings3WHERE name IN ('autovacuum_freeze_max_age', 'vacuum_freeze_min_age',4 'vacuum_freeze_table_age', 'autovacuum_multixact_freeze_max_age',5 'vacuum_multixact_freeze_min_age',6 'vacuum_multixact_freeze_table_age')7ORDER BY name;age(relfrozenxid) 要和防回卷阈值一起看。不要通过提高阈值来掩盖清理任务跑不动;XID 与 multixact 有各自的冻结年龄。
1SELECT c.oid::regclass AS table_name,2 greatest(mxid_age(c.relminmxid), mxid_age(t.relminmxid)) AS mxid_age3FROM pg_class AS c4LEFT JOIN pg_class AS t ON t.oid = c.reltoastrelid5WHERE c.relkind IN ('r', 'm')6ORDER BY mxid_age DESC7LIMIT 30;多事务共同锁行会使用 multixact ID。排查防回卷不能只看 relfrozenxid,还要看 relminmxid;老龄表仍需结合 VACUUM 任务和日志确认是否正在推进。
1SELECT extname, extversion, extnamespace::regnamespace AS extension_schema2FROM pg_extension3WHERE extname = 'pgstattuple';后面的 pgstattuple_approx 和 pgstatindex 来自这个扩展,没安装就不能直接运行。安装扩展属于数据库变更,应在维护流程里处理,不能在读诊断命令时顺手装上。
1-- 当前库已安装 pgstattuple2SELECT table_len, scanned_percent, dead_tuple_count,3 dead_tuple_percent, approx_free_space, approx_free_percent4FROM pgstattuple_approx('public.orders'::regclass);它利用可见性映射跳过部分页面,scanned_percent 会告诉你实际扫描比例。死元组统计和空闲空间估计应分开看:后者通常是表内可再利用空间,不等于 VACUUM 之后能归还给文件系统的字节数。
1-- 当前库已安装 pgstattuple;先评估目标表的大小2SELECT table_len, tuple_count, dead_tuple_count,3 dead_tuple_len, free_space, free_percent4FROM pgstattuple('public.orders'::regclass);它会扫描整张表,比 pgstattuple_approx 更重。并发写入时结果也不是整个扫描期间的同一瞬间快照;大表值班排查优先用上一条做初筛。
1SELECT i.indexrelid::regclass AS index_name,2 pg_size_pretty(pg_relation_size(i.indexrelid)) AS index_size,3 pg_relation_size(i.indexrelid) AS index_bytes4FROM pg_index AS i5WHERE i.indrelid = 'public.orders'::regclass6ORDER BY index_bytes DESC;如果总占用主要来自几个大索引,先确认索引类型、使用情况和页密度,再考虑重建。索引文件大不等于索引失效。
1-- 当前库已安装 pgstattuple;替换为现场 B-tree 索引2SELECT index_size, leaf_pages, deleted_pages,3 avg_leaf_density, leaf_fragmentation4FROM pgstatindex('public.orders_pkey'::regclass);pgstatindex 只用于 B-tree 索引。平均叶页密度偏低可能值得调查,但要结合填充因子、更新模式和索引增长趋势;单次结果不能直接推导出应重建多少空间。
1-- 当前库已安装 pgstattuple;替换为现场 GIN 索引2SELECT pending_pages, pending_tuples3FROM pgstatginindex('public.orders_tags_gin'::regclass);GIN 索引才有这个 pending list。积累较多时结合写入速率、fastupdate 与维护任务看原因;不要把它和 B-tree 的叶页密度混在一起判断。
1SELECT c.oid::regclass AS table_name,2 c.reltoastrelid::regclass AS toast_name,3 pg_size_pretty(pg_total_relation_size(c.reltoastrelid)) AS toast_size4FROM pg_class AS c5WHERE c.oid = 'public.orders'::regclass6 AND c.reltoastrelid <> 0;频繁更新大字段会让 TOAST 也增长。表主体看着不大而总大小明显更大时,先查这一路;没有 TOAST 对象就不会返回行。
1SELECT oid::regclass AS table_name, relpages, reltuples,2 pg_relation_size(oid) AS actual_heap_bytes,3 current_setting('block_size') AS block_size4FROM pg_class5WHERE oid = 'public.orders'::regclass;relpages、reltuples 是规划器使用的估计值,VACUUM/ANALYZE 后会更新;实际文件大小则由 pg_relation_size 读取。两者不同时先看统计信息更新时间,不要直接算成“膨胀百分比”。
1SELECT 'public.orders'::regclass AS table_name,2 pg_relation_filepath('public.orders'::regclass) AS relation_path;输出是相对于数据目录或表空间位置的路径,用于和服务器文件系统占用对照。数据库对象可能有多个文件段,不能只看这一条返回的单个路径判断总大小。
1-- 先确认业务窗口、写入负载和长事务2VACUUM (VERBOSE, ANALYZE) public.orders;它清理可回收的旧版本并更新统计,通常允许表继续读写;普通 VACUUM 大多只是把空间留在表内复用。若目的是让文件系统马上释放大量空间,不能把它当成 VACUUM FULL 的等价命令。VACUUM 也不能放在事务块中执行。
1SELECT c.oid::regclass AS table_name, s.n_dead_tup,2 c.reltuples AS estimated_rows,3 current_setting('autovacuum_vacuum_threshold')::integer4 + current_setting('autovacuum_vacuum_scale_factor')::numeric5 * GREATEST(c.reltuples, 0) AS global_vacuum_threshold6FROM pg_class AS c7JOIN pg_stat_user_tables AS s ON s.relid = c.oid8WHERE c.oid = 'public.orders'::regclass;autovacuum 的基础阈值加上行数比例,决定了普通死元组触发条件。这里按全局参数估算;目标表若设置了自己的 autovacuum_vacuum_* 存储参数,应以第 7 条查出的表级值重新计算。reltuples 和 n_dead_tup 都是估计值,不能把“低于阈值”当成永不清理的保证。
1SELECT c.oid::regclass AS table_name, s.n_ins_since_vacuum,2 current_setting('autovacuum_vacuum_insert_threshold')::integer3 + current_setting('autovacuum_vacuum_insert_scale_factor')::numeric4 * GREATEST(c.reltuples, 0) AS global_insert_threshold,5 s.last_autovacuum6FROM pg_class AS c7JOIN pg_stat_user_tables AS s ON s.relid = c.oid8WHERE c.oid = 'public.orders'::regclass;INSERT 多、UPDATE/DELETE 少的表,死元组未必高,仍可能因为新插入行数触发 VACUUM,推进可见性和冻结。表级参数同样可能覆盖全局值;统计系统更新有延迟,读数接近阈值时还要看最近一次 autovacuum 是否已经运行。
1SELECT c.oid::regclass AS table_name, s.n_mod_since_analyze,2 current_setting('autovacuum_analyze_threshold')::integer3 + current_setting('autovacuum_analyze_scale_factor')::numeric4 * GREATEST(c.reltuples, 0) AS global_analyze_threshold,5 s.last_autoanalyze6FROM pg_class AS c7JOIN pg_stat_user_tables AS s ON s.relid = c.oid8WHERE c.oid = 'public.orders'::regclass;有些表空间增长不明显,计划却变差,问题可能是统计信息没及时更新。这里看修改量是否接近 ANALYZE 门槛;表级覆盖参数和分区父表的特殊情况要单独核对,不能拿这一个全局公式解释所有表。
1SELECT c.oid::regclass AS table_name, c.reloptions,2 age(c.relfrozenxid) AS xid_age3FROM pg_class AS c4JOIN pg_namespace AS n ON n.oid = c.relnamespace5WHERE c.relkind IN ('r', 'm')6 AND n.nspname NOT IN ('pg_catalog', 'information_schema')7 AND c.reloptions IS NOT NULL8 AND array_to_string(c.reloptions, ',')9 ~* 'autovacuum_enabled\s*=\s*(false|off)'10ORDER BY age(c.relfrozenxid) DESC;关掉常规 autovacuum 后,事务 ID 防回卷的强制清理仍可能发生。找到这些表后,先查是谁设的、是否有人工维护计划,再按冻结年龄评估风险;不要把“没看到 worker”误写成 autovacuum 永远不会碰这张表。
1SELECT current_setting('autovacuum_max_workers')::integer2 AS configured_max_workers,3 COUNT(*) FILTER (WHERE backend_type = 'autovacuum worker')4 AS running_workers,5 COUNT(*) FILTER (WHERE backend_type = 'autovacuum launcher')6 AS running_launchers7FROM pg_stat_activity;几个大表长期占住 worker,其他表就可能排队。当前数量碰到上限只是线索,还要看这些 worker 具体在处理什么表、跑了多久,以及数据库间轮询;一次采样不能证明 autovacuum 长期饥饿。
1-- 第 27 条确认目标表确实存在 TOAST 对象后执行2VACUUM (VERBOSE, PROCESS_MAIN OFF, PROCESS_TOAST ON) public.orders;大字段频繁改写,TOAST 占用可能比表主体更值得先处理。PROCESS_MAIN OFF 跳过主表,但命令仍处理对应的 TOAST 关系;执行前看 TOAST 大小和事务边界,别因为只处理 TOAST 就认为没有 I/O 成本。
1-- 已确认死元组与索引维护问题;评估业务 I/O 窗口2VACUUM (VERBOSE, INDEX_CLEANUP ON) public.orders;普通 VACUUM 在死元组很少时可能跳过索引清理;ON 会尽量清理死索引项。若只是为了防回卷,PostgreSQL 的 failsafe 可能仍跳过这一步;INDEX_CLEANUP ON 不是“无论什么情况都保证索引清干净”。
1-- 只并行处理满足大小条件的索引,数字按可用 worker 评估2VACUUM (VERBOSE, PARALLEL 2) public.orders;并行 worker 用于索引清理阶段,不会把整张堆表扫描平均分给两个 worker。表至少要有两个满足条件的索引才可能启动多个 worker,还受 max_parallel_maintenance_workers 限制;命令写 2 不代表现场一定会起两个。
1-- 业务不能接受 VACUUM 为截断末尾空页取得 ACCESS EXCLUSIVE 锁2VACUUM (VERBOSE, TRUNCATE OFF) public.orders;普通 VACUUM 通常允许并发读写,但末尾空页截断需要短暂的 ACCESS EXCLUSIVE 锁。TRUNCATE OFF 放弃这一部分可能返还给文件系统的空间,换取更可控的锁行为;表内死元组回收仍照常进行。
1-- 逐张确认事后是否被跳过,不能只看命令整体结束2VACUUM (VERBOSE, SKIP_LOCKED ON) public.orders, public.events;SKIP_LOCKED 只保证开始处理某张关系时不等冲突锁;打开索引时仍可能阻塞。被跳过的表不会自动补做,下个窗口要重新检查最近 VACUUM 时间。分区表发生冲突时还可能跳过整组分区,不能把“命令成功”当成所有表都清理完。
1-- 第 10、19 条确认表及 TOAST 年龄,安排 I/O 窗口2VACUUM (FREEZE, VERBOSE) public.orders;FREEZE 会以更积极的方式扫描并冻结旧事务 ID,扫描成本可能明显高于普通 VACUUM。它不需要像 VACUUM FULL 那样重写整张表,但仍要先处理长期事务、复制槽等可能挡住旧版本清理的因素;不能用提高防回卷阈值代替它。
1SELECT c.oid::regclass AS table_name,2 age(c.relfrozenxid) AS table_xid_age,3 mxid_age(c.relminmxid) AS table_mxid_age,4 CASE WHEN t.oid IS NOT NULL THEN age(t.relfrozenxid) END5 AS toast_xid_age,6 CASE WHEN t.oid IS NOT NULL THEN mxid_age(t.relminmxid) END7 AS toast_mxid_age8FROM pg_class AS c9LEFT JOIN pg_class AS t ON t.oid = c.reltoastrelid10WHERE c.oid = 'public.orders'::regclass;执行前后都留一份读数。目标表的年龄降了,不代表数据库 datfrozenxid 立刻下降:同库可能还有更老的表,TOAST 也要算进去。若没有推进,结合 VERBOSE 输出、VACUUM 进度及最老事务继续排查。
1-- 同一维护流程后续会补做第 44 条;别单独执行后就停止2VACUUM (VERBOSE, SKIP_DATABASE_STATS ON) public.orders;数据库表非常多时,每次 VACUUM 结束都更新数据库级最老 XID 统计可能浪费时间,也会让并行维护任务争同一环节。这条只跳过该轮数据库级统计更新,不跳过目标表清理;维护结束要再刷新一次。
1-- 不带表名;ONLY_DATABASE_STATS 不能与其他选项并用,VERBOSE 除外2VACUUM (ONLY_DATABASE_STATS ON);它不会再清理任何一张表,只更新当前数据库最老未冻结 XID 的汇总。随后回看第 9 条;如果 datfrozenxid 年龄仍高,找同库更老的表,不能把这个命令当成全库冻结。
1-- 先看当前 vacuum_buffer_usage_limit,按缓存压力试小幅调整2VACUUM (VERBOSE, BUFFER_USAGE_LIMIT '16MB') public.orders;这是 VACUUM 的缓冲访问策略环,不是 maintenance_work_mem,也不代表任务只用 16 MB 内存。环过大可能挤走业务热点页;调整前后要同时看清理速度和业务缓存/读取量,不能只比较 VACUUM 完成时间。
1-- 当前库已安装 pgstattuple;结果只是维护前估算2SELECT pg_relation_size('public.orders'::regclass) AS heap_bytes,3 pg_total_relation_size('public.orders'::regclass) AS total_bytes,4 a.dead_tuple_percent, a.approx_free_percent,5 a.approx_free_space6FROM pgstattuple_approx('public.orders'::regclass) AS a;要从文件系统拿回空间,先分清表主体、索引和 TOAST 哪一部分大。approx_free_space 是表内可复用空间估计,不等于 VACUUM FULL 一定能返还的字节数;重写期间还需要放下新文件,现场磁盘余量必须从服务器上另查。
1SELECT l.pid, l.mode, l.granted,2 a.state, a.xact_start,3 now() - a.xact_start AS xact_age,4 left(a.query, 120) AS query_sample5FROM pg_locks AS l6LEFT JOIN pg_stat_activity AS a ON a.pid = l.pid7WHERE l.locktype = 'relation'8 AND l.relation = 'public.orders'::regclass9ORDER BY l.granted DESC, a.xact_start NULLS LAST;VACUUM FULL 要拿 ACCESS EXCLUSIVE 锁,读写都会被挡住。开工前除了看眼前的持锁会话,还要确认业务是否会不断产生新事务;一次锁快照不能保证维护时一定马上拿到锁。
1-- 已确认 ACCESS EXCLUSIVE 锁影响、额外磁盘、回退及复制延迟2VACUUM (FULL, VERBOSE) public.orders;这条会重写整张表并重建相关索引,通常比普通 VACUUM 慢得多,过程中持续持有 ACCESS EXCLUSIVE 锁。只有明确需要回收文件系统空间、且业务能接受停写停读时才考虑;它不是表增长后的默认维护动作。
1SELECT pid, relid::regclass AS table_name,2 command, phase, heap_blks_total,3 heap_blks_scanned, heap_tuples_written,4 index_rebuild_count5FROM pg_stat_progress_cluster6WHERE command = 'VACUUM FULL';VACUUM FULL 不出现在普通 VACUUM 的进度视图里。扫描、写新堆、交换文件、重建索引是不同阶段;扫描完成不表示锁可以释放,也不表示索引已重建完。结合磁盘 I/O 和等待事件判断是否真正卡住。
1SELECT 'public.orders'::regclass AS table_name,2 pg_relation_size('public.orders'::regclass) AS heap_bytes,3 pg_total_relation_size('public.orders'::regclass) AS total_bytes,4 pg_relation_filenode('public.orders'::regclass) AS current_filenode;用第 46 条留存的维护前大小比较,别只看 VACUUM FULL 返回成功。表文件节点可能换过,旧路径上的文件不会继续代表这张表;空间回收验收还要看服务器文件系统余量,以及业务查询、复制是否恢复正常。
1SELECT i.indexrelid::regclass AS index_name,2 pg_relation_size(i.indexrelid) AS index_bytes,3 s.idx_scan, s.last_idx_scan4FROM pg_index AS i5LEFT JOIN pg_stat_user_indexes AS s6 ON s.indexrelid = i.indexrelid7WHERE i.indrelid = 'public.orders'::regclass8ORDER BY index_bytes DESC;索引大而扫描次数少,不代表它没用:约束索引和写入路径仍可能需要它,统计计数也可能刚被重置。要重建哪个索引,先结合第 25 条页密度、业务查询和增长趋势挑目标,不要对整张表所有索引一键重建。
1SELECT i.indexrelid::regclass AS index_name,2 am.amname AS access_method,3 i.indisunique, i.indisexclusion,4 i.indisvalid, i.indisready5FROM pg_index AS i6JOIN pg_class AS c ON c.oid = i.indexrelid7JOIN pg_am AS am ON am.oid = c.relam8WHERE i.indexrelid = 'public.orders_pkey'::regclass;REINDEX CONCURRENTLY 对排他约束索引有限制;如果目标索引已经无效,先确认它是失败后的 _ccnew 临时索引,还是原索引自身出问题。indisvalid、indisready 影响查询和写入,不能只凭索引文件还在就判断它可用。
1-- 评估新旧索引并存的磁盘空间、长事务与业务 I/O2REINDEX INDEX CONCURRENTLY public.orders_pkey;在线重建允许表继续正常读写,但比普通 REINDEX 更慢,也需要新旧索引同时占用磁盘;它不能放在事务块中。对同一张表不能并行开多个 concurrent index build,执行期间要监控长事务与复制延迟。
1SELECT pid, relid::regclass AS table_name,2 index_relid::regclass AS index_name,3 command, phase, blocks_total, blocks_done,4 lockers_total, lockers_done, current_locker_pid5FROM pg_stat_progress_create_index6WHERE command = 'REINDEX CONCURRENTLY';进度可能长时间停在等待旧事务或快照的阶段;这时 current_locker_pid 比扫描百分比更有用。先找到对应业务事务,别看到进度不动就直接取消任务;取消后可能留下需要处理的无效临时索引。
1SELECT i.indexrelid::regclass AS index_name,2 i.indisvalid, i.indisready,3 pg_relation_size(i.indexrelid) AS index_bytes4FROM pg_index AS i5WHERE i.indrelid = 'public.orders'::regclass6ORDER BY index_bytes DESC;与第 51 条维护前大小对比,同时确认原目标索引处于有效状态。若发现 _ccnew 或 _ccold 无效索引,先按官方恢复步骤识别它的角色,再决定清理;不要看到无效就把原业务索引删掉。
1SELECT c.oid::regclass AS parent_table,2 COUNT(i.inhrelid) AS direct_children,3 s.last_analyze, s.last_autoanalyze4FROM pg_class AS c5LEFT JOIN pg_inherits AS i ON i.inhparent = c.oid6LEFT JOIN pg_stat_user_tables AS s ON s.relid = c.oid7WHERE c.relkind = 'p'8GROUP BY c.oid, s.last_analyze, s.last_autoanalyze9ORDER BY s.last_analyze NULLS FIRST, direct_children DESC;分区父表自己不存行,autovacuum 会处理叶子分区,却不会自动给分区父表做 ANALYZE。父表长期没有人工统计更新时,规划器处理整棵分区树可能使用旧分布。这里只数直接子分区,多级分区要继续向下查。
1SELECT i.inhrelid::regclass AS child_partition,2 c.relkind, s.n_dead_tup,3 s.last_autovacuum, s.last_autoanalyze,4 pg_total_relation_size(i.inhrelid) AS total_bytes5FROM pg_inherits AS i6JOIN pg_class AS c ON c.oid = i.inhrelid7LEFT JOIN pg_stat_user_tables AS s ON s.relid = i.inhrelid8WHERE i.inhparent = 'public.orders_partitioned'::regclass9ORDER BY total_bytes DESC;父表没被 autovacuum 处理,不等于子分区也没清理。先找到增长快或死元组多的叶子分区,再核对对应的 autovacuum;这里显示直接子节点,若 relkind='p',它下面还有一层要查。
1SELECT schemaname, tablename, attname,2 inherited, null_frac, n_distinct3FROM pg_stats4WHERE schemaname = 'public'5 AND tablename = 'orders_partitioned'6ORDER BY inherited DESC, attname;inherited=true 表示这一组列统计包含子分区数据。缺行时先确认是否还没跑过 ANALYZE、当前账号能否读取统计视图;也别只看统计行“存在”就认定它仍适合今天的参数分布。
1-- 分区分布明显变化后执行;在目标数据库操作2ANALYZE (VERBOSE) public.orders_partitioned;PostgreSQL 会从分区采样,建立父表覆盖分区的数据统计,并递归更新各分区。它不是 VACUUM,不会清死元组;业务高峰仍要评估采样 I/O 和对其他维护任务的锁影响。
1SELECT pid, relid::regclass AS table_name,2 phase, sample_blks_total, sample_blks_scanned,3 child_tables_total, child_tables_done,4 current_child_table_relid::regclass AS current_child5FROM pg_stat_progress_analyze6WHERE relid = 'public.orders_partitioned'::regclass;acquiring inherited sample rows 阶段可看到正在处理哪个子分区;最后还会计算统计并写入目录。进度行消失只表示该任务结束,是否成功、父表统计有没有更新时间,应继续看命令返回与 pg_stat_user_tables.last_analyze。
1SELECT extname, extversion,2 extnamespace::regnamespace AS extension_schema3FROM pg_extension4WHERE extname = 'pg_visibility';后面的 pg_visibility_map_summary 来自这个扩展。没安装就不能直接运行;安装扩展是数据库变更,先走现场维护流程。不要为了看一个指标,未经确认就在线上新装模块。
1-- 当前库已安装 pg_visibility;比例以堆表主文件页数为分母2SELECT v.all_visible, v.all_frozen,3 pg_relation_size('public.orders'::regclass)4 / current_setting('block_size')::bigint AS heap_pages,5 ROUND(v.all_visible * 100.0 / NULLIF(6 pg_relation_size('public.orders'::regclass)7 / current_setting('block_size')::bigint, 0), 1)8 AS all_visible_percent9FROM pg_visibility_map_summary('public.orders'::regclass) AS v;all-visible 页多,index-only scan 才更有机会少访问堆表;all-frozen 页则影响下一轮冻结扫描。页面被 UPDATE、DELETE 或 INSERT 改动后,映射位会清掉,单次高比例并不保证业务查询始终不回表。
1-- 只对可控的 SELECT 样例执行;范围替换为业务参数2EXPLAIN (ANALYZE, BUFFERS)3SELECT order_id4FROM public.orders5WHERE order_id BETWEEN 100000 AND 101000;如果计划用了 Index Only Scan,再看 Heap Fetches 是否仍高;有这个节点也不表示完全没读堆页。若计划根本没用 index-only scan,先确认索引是否覆盖查询所需列、成本估计是否合适,再讨论可见性映射。ANALYZE 会真的执行 SELECT,不应拿不受控的大查询直接试。
1-- 当前库已安装 pg_visibility;只取少量命中行的页号2WITH sampled_pages AS (3 SELECT DISTINCT split_part(trim(both '()' FROM ctid::text), ',', 1)::bigint4 AS blkno5 FROM public.orders6 WHERE order_id BETWEEN 100000 AND 1010007 LIMIT 208)9SELECT s.blkno, v.all_visible, v.all_frozen10FROM sampled_pages AS s11CROSS JOIN LATERAL pg_visibility_map(12 'public.orders'::regclass, s.blkno) AS v13ORDER BY s.blkno;这只抽样检查查询命中的部分堆页,不是全表扫描结论。大量抽样页都不 all-visible 时,index-only scan 仍要回表核对可见性;还要结合更新速率与最近 VACUUM 时间,不能凭 20 个页号判定整表映射有问题。
1SELECT relid::regclass AS table_name,2 n_dead_tup, n_ins_since_vacuum,3 n_tup_upd, n_tup_del,4 last_vacuum, last_autovacuum5FROM pg_stat_user_tables6WHERE relid = 'public.orders'::regclass;n_tup_upd、n_tup_del 是累计计数,不是“上次 VACUUM 后的更新数”;读数高时要看两次采样的差值。频繁改动会反复清除 all-visible 位,普通 VACUUM 跑过也未必能让这类热点表长期保持 index-only scan 的低 Heap Fetches。
1SELECT relid::regclass AS table_name,2 n_tup_upd, n_tup_hot_upd,3 ROUND(n_tup_hot_upd * 100.0 / NULLIF(n_tup_upd, 0), 1)4 AS hot_update_percent5FROM pg_stat_user_tables6WHERE relid = 'public.orders'::regclass;HOT 更新无需为常规索引写入新的索引项,能减轻更新引起的索引负担。比例低时先看哪些列被更新、这些列是否被索引引用,以及原页有没有空间;累计比例会掩盖最近变化,最好留两次采样比较增量。
1SELECT relid::regclass AS table_name,2 n_tup_upd, n_tup_newpage_upd,3 ROUND(n_tup_newpage_upd * 100.0 / NULLIF(n_tup_upd, 0), 1)4 AS moved_to_new_page_percent5FROM pg_stat_user_tables6WHERE relid = 'public.orders'::regclass;新版本落在别的堆页就不是 HOT 更新。这个比例高时,页内余量和行宽变化值得查;但统计只告诉你发生了跨页更新,不会告诉你具体是哪一列变长或哪个索引阻止 HOT。
1SELECT i.indexrelid::regclass AS index_name,2 pg_get_indexdef(i.indexrelid) AS index_definition3FROM pg_index AS i4WHERE i.indrelid = 'public.orders'::regclass5ORDER BY i.indexrelid::regclass::text;应用经常修改的列若参与普通索引,HOT 条件可能不成立;表达式索引和 INCLUDE 列也要看。这里先把定义列出来交给开发核对更新语句,不能只凭 HOT 比例低就建议删除索引。
1-- 当前库已安装 pgstattuple;reloptions 为 NULL 通常使用默认值2SELECT c.oid::regclass AS table_name, c.reloptions,3 a.approx_free_percent, a.scanned_percent4FROM pg_class AS c5CROSS JOIN LATERAL pgstattuple_approx(c.oid::regclass) AS a6WHERE c.oid = 'public.orders'::regclass;降低 fillfactor 能在新页预留更多更新空间,但已有页不会因为改参数就立即腾出余量。先结合真实写入模式与空闲空间估计,别把所有低 HOT 比例都归因于 fillfactor;索引列被修改时,留空也不能产生 HOT。
1-- 仅在确认更新模式、额外空间成本和维护窗口后执行2ALTER TABLE public.orders SET (fillfactor = 80);80 是示例,降低 fillfactor 会增加表占用空间,换取后续同页更新的机会。修改表存储参数会取 SHARE UPDATE EXCLUSIVE 锁;现有页不会立刻重排,不能把执行成功当成 HOT 比例马上提高。后续按第 66、67 条看增量是否改善。
1SELECT name, setting, unit, source2FROM pg_settings3WHERE name IN ('autovacuum_vacuum_cost_delay',4 'autovacuum_vacuum_cost_limit',5 'vacuum_cost_delay', 'vacuum_cost_limit',6 'autovacuum_work_mem', 'maintenance_work_mem',7 'autovacuum_max_workers')8ORDER BY name;人工 VACUUM 默认通常不做成本限速,autovacuum 则可能主动睡眠以压低 I/O。autovacuum_work_mem=-1 表示使用 maintenance_work_mem;估算内存时还要乘可能同时工作的 worker 数,不能只看单进程配置。
1SELECT a.pid, p.relid::regclass AS table_name,2 now() - a.query_start AS run_time,3 p.phase, a.wait_event_type, a.wait_event4FROM pg_stat_activity AS a5JOIN pg_stat_progress_vacuum AS p ON p.pid = a.pid6WHERE a.backend_type = 'autovacuum worker'7ORDER BY a.query_start;VacuumDelay 是成本限速的一次正常休眠,不是“任务死锁”;但如果 worker 长时间占着表且进度慢,就要看限速是否过严、表写入是否持续追不上。等待事件是瞬时采样,两次进度对比比一次截图更可靠。
1SELECT relid::regclass AS table_name, phase,2 dead_tuple_bytes, max_dead_tuple_bytes,3 index_vacuum_count,4 ROUND(dead_tuple_bytes * 100.05 / NULLIF(max_dead_tuple_bytes, 0), 1)6 AS dead_buffer_percent7FROM pg_stat_progress_vacuum8ORDER BY index_vacuum_count DESC, dead_buffer_percent DESC;死元组缓冲反复接近上限、索引清理轮数不断增长时,核对第 71 条的 worker 内存和第 12 条索引阶段。调大内存不是唯一答案:表更新过快、索引过多也会增加工作量,而且每个 worker 都可能申请自己的内存。
1SELECT c.oid::regclass AS table_name,2 option AS table_setting3FROM pg_class AS c4CROSS JOIN LATERAL unnest(c.reloptions) AS option5WHERE c.relkind IN ('r', 'm')6 AND option ~ '^autovacuum_vacuum_cost_(delay|limit)='7ORDER BY table_name, table_setting;有表级 cost delay/limit 时,该 worker 不一定参与全局 worker 的成本均衡。现场同样的全局参数,不同表的运行速度可能差很多;先找出覆盖设置,再解释它为何被单独限速。
1SELECT name, setting, unit, source2FROM pg_settings3WHERE name IN ('log_autovacuum_min_duration',4 'log_destination', 'logging_collector')5ORDER BY name;没有 autovacuum 日志,不能直接认定任务没运行:可能是阈值设得太高,或日志写在另一个目标。需要追“为什么被取消”“清了多少页”时,先确认日志是否开启、去哪里查,再结合统计视图的最近运行时间。
1SELECT c.oid::regclass AS table_name,2 c.reltuples AS estimated_rows, s.n_dead_tup,3 current_setting('autovacuum_vacuum_threshold')::numeric4 + current_setting('autovacuum_vacuum_scale_factor')::numeric5 * GREATEST(c.reltuples, 0) AS global_trigger_rows,6 s.last_autovacuum7FROM pg_class AS c8JOIN pg_stat_user_tables AS s ON s.relid = c.oid9WHERE c.relkind = 'r' AND c.reloptions IS NULL10 AND s.n_dead_tup >11 current_setting('autovacuum_vacuum_threshold')::numeric12 + current_setting('autovacuum_vacuum_scale_factor')::numeric13 * GREATEST(c.reltuples, 0)14ORDER BY s.n_dead_tup DESC15LIMIT 30;只列 reloptions 为空的普通表,避免拿全局公式错算特殊表;仅设置了 fillfactor 等其他参数的表也会被这条排除,要另查第 77 条。跨过门槛说明它应有机会被调度,不表示 worker 一定马上空出来;再结合运行中任务、日志和最近清理时间判断有没有排队。
1SELECT c.oid::regclass AS table_name, s.n_dead_tup,2 COALESCE((SELECT split_part(opt, '=', 2)::numeric3 FROM unnest(c.reloptions) AS opt4 WHERE split_part(opt, '=', 1) =5 'autovacuum_vacuum_threshold' LIMIT 1),6 current_setting('autovacuum_vacuum_threshold')::numeric)7 + COALESCE((SELECT split_part(opt, '=', 2)::numeric8 FROM unnest(c.reloptions) AS opt9 WHERE split_part(opt, '=', 1) =10 'autovacuum_vacuum_scale_factor' LIMIT 1),11 current_setting('autovacuum_vacuum_scale_factor')::numeric)12 * GREATEST(c.reltuples, 0) AS effective_trigger_rows13FROM pg_class AS c14JOIN pg_stat_user_tables AS s ON s.relid = c.oid15WHERE c.oid = 'public.orders'::regclass;第 31 条只按全局参数估算;这一条先找表级值,没有才回退全局值。reltuples 和死元组数仍是估计数。若目标表明确关闭常规 autovacuum,跨过普通门槛也不会按常规方式调度,但防回卷强制清理另算。
1SELECT name, setting, unit, source2FROM pg_settings3WHERE name IN ('autovacuum', 'track_counts',4 'autovacuum_naptime', 'autovacuum_max_workers')5ORDER BY name;autovacuum 依赖累计统计系统,track_counts 关了就不能按正常统计触发。autovacuum_naptime 是轮询间隔,不是每张表精确多久清一次;数据库多、worker 忙时,实际等待可能更长。
1-- 先计算旧门槛,评估 worker I/O 与业务窗口2ALTER TABLE public.orders SET (3 autovacuum_vacuum_scale_factor = 0.02,4 autovacuum_analyze_scale_factor = 0.015);这两个比例是示例,应该按表大小、更新速率和 I/O 能力定。阈值降低,任务可能更频繁,不能只看死元组下降就宣布配置更好;业务读取、日志和 worker 排队也要一起观察。修改表存储参数需要短暂锁,先确认现场变更窗口。
1SELECT c.oid::regclass AS table_name, c.reloptions,2 s.n_dead_tup, s.n_mod_since_analyze,3 s.last_autovacuum, s.last_autoanalyze,4 s.autovacuum_count, s.autoanalyze_count5FROM pg_class AS c6JOIN pg_stat_user_tables AS s ON s.relid = c.oid7WHERE c.oid = 'public.orders'::regclass;先确认参数确实写在目标表上,再等下一轮 worker 运行后比较死元组、统计更新时间和业务负载。计数可能被统计重置,时间戳也不会因为改参数立刻更新;“回读配置成功”与“调优见效”是两回事。
1SELECT c.oid::regclass AS temp_table,2 c.relpersistence, c.reltuples,3 pg_total_relation_size(c.oid) AS total_bytes4FROM pg_class AS c5WHERE c.relnamespace = pg_my_temp_schema()6 AND c.relkind = 'r'7ORDER BY total_bytes DESC;pg_my_temp_schema() 只指向当前会话的临时模式,没有临时表时返回零。临时表由创建它的会话使用,autovacuum 无法跨会话替它清理或分析;不要在另一个 DBA 连接里找到了名字,就以为能维护业务会话里的对象。
1SELECT s.relid::regclass AS temp_table,2 s.n_live_tup, s.n_dead_tup,3 s.last_vacuum, s.last_analyze,4 s.vacuum_count, s.analyze_count5FROM pg_stat_all_tables AS s6JOIN pg_class AS c ON c.oid = s.relid7WHERE c.relnamespace = pg_my_temp_schema()8 AND c.relkind = 'r'9ORDER BY s.n_dead_tup DESC;会话内重复装载、更新临时表时,死元组和统计信息也可能变旧。统计读数有延迟;任务若每次都创建后立即丢弃临时表,人工 VACUUM 通常没有意义,但复杂查询前的 ANALYZE 仍可能有价值。
1-- 当前会话持有并重复使用这张临时表;不能放进事务块2VACUUM (VERBOSE) pg_temp.session_stage;临时表复用多轮且有大量 UPDATE/DELETE 时,自己安排维护;别指望后台 autovacuum。pg_temp 解析到本会话的临时模式,换一个连接不会维护原会话那张表。先确认会话事务流程允许独立执行 VACUUM。
1-- 数据量或分布明显变化、且后续有复杂连接查询2ANALYZE pg_temp.session_stage;临时表没有自动 ANALYZE,旧估计很容易让后续 JOIN 选错方法。刚装入大批数据后手工分析通常比只看 reltuples 更可靠;这一条只更新统计,不会回收死元组。
1SELECT schemaname, tablename, attname,2 null_frac, n_distinct, most_common_freqs3FROM pg_stats4WHERE schemaname = (5 SELECT nspname FROM pg_namespace6 WHERE oid = pg_my_temp_schema())7 AND tablename = 'session_stage'8ORDER BY attname;统计行存在,说明目标会话已有可供规划器参考的列分布;但这不是计划一定变好的证明。继续用真实业务 SQL 的 EXPLAIN 看行数估计和连接方法,尤其注意数据装载后的再次变化。
1SELECT m.schemaname, m.matviewname, m.ispopulated,2 m.hasindexes,3 pg_total_relation_size(4 format('%I.%I', m.schemaname, m.matviewname)::regclass)5 AS total_bytes6FROM pg_matviews AS m7ORDER BY total_bytes DESC;ispopulated=false 的物化视图当前不能被正常查询,也不能做 concurrent refresh。总大小包含索引和可能的 TOAST;先找到真正占空间的对象,再看它的刷新方式与最近维护时间。
1SELECT i.indexrelid::regclass AS index_name,2 i.indisunique, i.indisvalid, i.indisready,3 i.indexprs IS NULL AS no_expression,4 i.indpred IS NULL AS no_partial_predicate5FROM pg_index AS i6WHERE i.indrelid = 'public.daily_orders_mv'::regclass7ORDER BY i.indisunique DESC, i.indexrelid::regclass::text;REFRESH MATERIALIZED VIEW CONCURRENTLY 至少需要一条只基于列、覆盖所有行的 UNIQUE 索引,且物化视图已填充。这里把候选索引筛出来,还要核对索引定义和状态;不能把 hasindexes=true 当成 concurrent refresh 的充分条件。
1-- 第 86、87 条均通过;确认源查询、磁盘与业务负载2REFRESH MATERIALIZED VIEW CONCURRENTLY public.daily_orders_mv;并发刷新允许读者继续查询旧内容,且同一物化视图一次只能运行一个刷新任务。它通常比普通刷新做更多工作;源数据变化较大时,刷新后的旧版本与空间占用值得继续查。普通刷新可能直接替换内容,两种刷新不能用相同的空间预期验收。
1SELECT c.oid::regclass AS matview_name,2 pg_total_relation_size(c.oid) AS total_bytes,3 s.n_live_tup, s.n_dead_tup,4 s.last_autovacuum, s.last_autoanalyze5FROM pg_class AS c6LEFT JOIN pg_stat_user_tables AS s ON s.relid = c.oid7WHERE c.oid = 'public.daily_orders_mv'::regclass8 AND c.relkind = 'm';并发刷新之后若死元组仍多,结合更新量、长事务和 autovacuum 再决定是否人工清理。统计列为空时先确认目标对象是否被统计视图收录以及读取权限。n_dead_tup 是估计数;视图文件变大也可能主要来自索引,不能把刷新后每一字节增长都叫“表膨胀”。
1-- 确认刷新已结束、源查询稳定、目标视图可维护2VACUUM (VERBOSE, ANALYZE) public.daily_orders_mv;普通 VACUUM 可回收物化视图里的可复用空间,ANALYZE 更新供查询使用的统计;多数空间仍留在对象内复用,不保证文件系统马上减少。若刚做的是整表替换式刷新,先看第 89 条,别无条件再补一轮 VACUUM。
1SELECT datname, mxid_age(datminmxid) AS mxid_age,2 current_setting('autovacuum_multixact_freeze_max_age')::bigint3 AS multixact_freeze_limit4FROM pg_database5WHERE datallowconn6ORDER BY mxid_age DESC;行锁和外键检查可能产生 MultiXact。它与普通事务 ID 分开计龄;第 9 条只看 datfrozenxid,不能代替这一条。最老的库若已接近阈值,进库找表和 TOAST 的 relminmxid,同时检查自动维护有没有被长事务挡住。
1SELECT c.oid::regclass AS relation,2 c.relkind, mxid_age(c.relminmxid) AS mxid_age,3 pg_total_relation_size(c.oid) AS total_bytes4FROM pg_class AS c5WHERE c.relkind IN ('r', 't', 'm')6ORDER BY mxid_age DESC7LIMIT 30;在第 91 条提示的数据库内执行。t 是 TOAST 表,不能只维护业务表而漏掉它;relminmxid 接近阈值时,再看目标对象正在运行的 VACUUM 和持有旧快照的会话。排序只用于定位,不是“前 30 张表都要立即手工冻结”的任务单。
1SELECT a.pid, a.datname, a.query,2 p.relid::regclass AS relation,3 p.phase, p.heap_blks_scanned, p.heap_blks_total4FROM pg_stat_activity AS a5LEFT JOIN pg_stat_progress_vacuum AS p ON p.pid = a.pid6WHERE a.backend_type = 'autovacuum worker'7 AND a.query LIKE '%to prevent wraparound%'8ORDER BY a.pid;防回卷 worker 的活动描述会标出 to prevent wraparound。查询为空不等于风险消失:可能尚未触发、刚结束,或只因权限看不到完整活动文本。先对照第 91、92 条的年龄和普通 autovacuum 运行情况,再判断是否需要人工介入。
1SELECT name, setting, unit, source2FROM pg_settings3WHERE name IN (4 'autovacuum_freeze_max_age',5 'autovacuum_multixact_freeze_max_age',6 'vacuum_failsafe_age',7 'vacuum_multixact_failsafe_age')8ORDER BY name;autovacuum_*_freeze_max_age 决定何时强制防回卷维护;failsafe 年龄更高,触发后 VACUUM 会优先推进冻结,可能跳过非必要的索引清理并放宽限速。不要把 failsafe 当成日常清理策略。改阈值前应同时核对磁盘、维护吞吐和最老关系的年龄。
1SELECT d.datname,2 age(d.datfrozenxid) AS xid_age,3 current_setting('autovacuum_freeze_max_age')::bigint4 - age(d.datfrozenxid) AS xid_headroom,5 mxid_age(d.datminmxid) AS mxid_age,6 current_setting('autovacuum_multixact_freeze_max_age')::bigint7 - mxid_age(d.datminmxid) AS mxid_headroom8FROM pg_database AS d9WHERE d.datname = current_database();余量为负说明已经越过自动维护触发阈值,不是事务 ID 已经回卷。数据库级读数还取决于其他表是否已冻结;维护一张表后没有立即下降,要回到第 91、92 条找仍然最老的对象,并看第 93 条 worker 是否推进。
1-- 确认最老关系、长事务和业务窗口;在目标数据库独立执行2VACUUM (FREEZE, VERBOSE) public.orders_history;第 41 条是普通目标表冻结示例;这一条针对第 92、95 条已经确认的防回卷对象。FREEZE 会积极冻结该表符合条件的旧事务版本,但不能越过仍被长事务需要的可见性边界。运行时看 VERBOSE 与进度,结束后分别回读表和数据库年龄;TOAST 或其他表仍老时还要继续定位。
1SELECT t.oid::regclass AS table_name,2 x.oid::regclass AS index_name,3 i.indisvalid, i.indisready,4 pg_relation_size(x.oid) AS index_bytes5FROM pg_index AS i6JOIN pg_class AS x ON x.oid = i.indexrelid7JOIN pg_class AS t ON t.oid = i.indrelid8WHERE NOT i.indisvalid9 AND t.relkind IN ('r', 'm')10ORDER BY index_bytes DESC;REINDEX CONCURRENTLY 失败可能留下 _ccnew 或 _ccold,它们会占空间,部分状态下还可能拖累写入。先按索引定义、命名和 indisready 确认是哪一步留下的对象;普通无效业务索引不能按后缀规则直接删除。清理前保存 DDL,并按官方的失败恢复步骤处理。
1SELECT s.relid::regclass AS relation,2 s.last_vacuum, s.last_autovacuum,3 s.vacuum_count, s.autovacuum_count,4 s.n_live_tup, s.n_dead_tup,5 s.n_mod_since_analyze6FROM pg_stat_user_tables AS s7WHERE s.relid = 'public.orders_history'::regclass;维护时间和计数用于确认工作确实落在目标表;死元组数是估计值,统计有延迟,还可能被重置。若 n_mod_since_analyze 很高,规划器估计仍可能过时,可按第 30 条补 ANALYZE。不要只凭 n_dead_tup 降到零就宣布问题解决。
1SELECT c.oid::regclass AS relation,2 age(c.relfrozenxid) AS xid_age,3 mxid_age(c.relminmxid) AS mxid_age,4 pg_relation_size(c.oid) AS heap_bytes,5 pg_indexes_size(c.oid) AS indexes_bytes6FROM pg_class AS c7WHERE c.oid = 'public.orders_history'::regclass;把这组读数与维护前同口径快照对照。普通 VACUUM 后冻结年龄可能明显下降,但 heap 文件未缩小,这并不矛盾:空间通常在表内复用。若索引大小持续涨、死元组已少,回到第 51–55 条判断是否有独立的索引膨胀问题。
1SELECT current_database() AS database_name,2 age(d.datfrozenxid) AS oldest_xid_age,3 mxid_age(d.datminmxid) AS oldest_mxid_age,4 d.datfrozenxid, d.datminmxid5FROM pg_database AS d6WHERE d.datname = current_database();这是防回卷维护的最后一层回读,不是要求两个数字每次都下降。一张表处理完,本库最老边界可能仍落在别的表或 TOAST 上;按第 91、92 条再查。验收时还要把第 98、99 条、VACUUM 输出、业务负载和无效索引清单放在一起看。
排查 VACUUM,先分清是死元组清理慢、旧快照挡住了回收,还是冻结年龄真的接近阈值。普通 VACUUM 清出的空间通常留在表里复用;要缩小文件,得另算锁和磁盘成本。维护结束,把表、索引与数据库级年龄各回读一次,比看一条“命令完成”更稳妥。

这份清单建议收藏。其他数据库运维场景的命令可在 ORA100 · DBA100 查看:
微信里也可以搜索小程序 「三笠的百令册」,需要时按专题查。