
注: 本文为安丫科技刘峰的原创,请尊重知识产权,转发请注明出处,不接受任何抄袭、演绎和未经注明出处的转载。
在任何一个健康的PostgreSQL数据库背后,都有一个默默无闻但至关重要的英雄:autovacuum。
对于许多开发者和初级数据库管理员(DBA)来说,它是一个“即插即用”的后台进程,自动处理着数据库的“家务事”。
然而,要真正发挥PostgreSQL的极致性能,并避免潜在的灾难,深入理解autovacuum的工作原理、如何调优以及如何监控,是必不可少的技能。
01
为什么我们需要 Autovacuum?
从 VACUUM 的基础说起
我们已经知道,VACUUM 是 PostgreSQL 的“健康管家”,它主要负责两件生死攸关的大事:
回收空间,对抗膨胀
清理因 UPDATE 和 DELETE 产生的“死亡元组”,将空间标记为可复用,防止表和索引无限膨胀。
冻结事务ID,防止回卷
将旧的事务ID(TXID)标记为“冻结”,以避免发生 TXID 回卷,这种灾难会导致数据库为保护数据而强制停机。
VACUUM 是解决这些问题的动作。
那么问题来了:谁来执行这个动作?以及,何时执行?
DBA 将面临四个无法解决的困境
如果我们没有 Autovacuum,就意味着所有 VACUUM 操作都必须由数据库管理员(DBA)手动执行。
让我们设想一下一个只有手动 VACUUM 的世界,DBA 将面临四个无法解决的困境:
困境一:时机之谜——“我该何时运行 VACUUM?”
一个生产数据库可能有成百上千张表。DBA 如何知道哪张表“脏了”,需要清理?
盲目执行?
每天对所有表运行一次 VACUUM?
对于大型数据库,这会产生巨大的、不必要的 I/O 开销。
对于几乎不更新的表,这是纯粹的资源浪费。
基于监控?
DBA 需要编写复杂的监控脚本,持续查询 pg_stat_user_tables 视图,检查每张表的 n_dead_tup(死亡元组数)。
当数量超过某个阈值时,再触发 VACUUM。
但这引出了下一个问题:阈值该设为多少?1万?10万?这对大表和小表来说标准完全不同。
手动管理 VACUUM 的时机,是一项极其繁琐、低效且容易出错的工作。
困境二:规模之痛——“我如何管理成千上万张表?”
数据库的表写入模式千差万别:
高频写入表(如 iot_data, logs)
可能每分钟都会产生数万个死亡元组。
中频更新表(如 users, orders)
每天有稳定的更新。
静态配置表(如 countries, settings)
几乎从不改变。
一个负责任的 DBA 必须为不同类型的表制定不同的 VACUUM 策略。
手动去跟踪和管理这一切,在规模扩大后,会成为一场噩梦。
困境三:沉默的杀手——“我如何处理从不‘脏’的表?”
这是手动 VACUUM 策略中最致命的缺陷。
我们知道,VACUUM 的另一个核心任务是防止 TXID 回卷。
TXID 回卷的风险恰恰来自于那些很少或从不更新/删除的表。
因为它们没有死亡元组产生,所以从“表膨胀”的角度看,它们永远是“干净”的。
一个只关心“脏表”的 DBA,可能永远不会对这些静态表执行 VACUUM。
但这些表中的行所持有的 TXID 却在随着时间的推移而不断“老化”。
当老化到极限时,数据库为了防止数据损坏,会强制关闭写服务。
这是一个巨大的陷阱
最需要 VACUUM 来“冻结”TXID 的表,恰恰是手动策略最容易忽略的表。
困境四:人的弱点——“我能保证 24/7 永不疏漏吗?”
依赖手动操作或简单的 Cron 定时任务,本质上是脆弱的。
DBA 会休假,会疏忽。
定时任务可能在业务高峰期触发,影响性能。
在凌晨三点,当一张表因为突发流量而迅速膨胀时,谁在那里手动执行 VACUUM?
Autovacuum:从“手动动作”到“智能系统”
的升华
Autovacuum 的诞生,正是为了系统性地解决以上所有困境。
它不是 VACUUM 的替代品,而是围绕 VACUUM 构建的一套自动化、智能化的调度和执行系统。
它就像一个内置在 PostgreSQL 中的、不知疲倦的初级 DBA 团队:
解决了“时机之谜”
Autovacuum 持续在后台监控每一张表。
它使用基于比例(scale_factor)和固定值(threshold)的算法来判断一张表是否需要 VACUUM 或 ANALYZE。它自动回答了“何时执行”的问题。
解决了“规模之痛”
Autovacuum 拥有一个工作进程池(autovacuum_max_workers),可以并发地处理多张脏表。
更重要的是,它允许 DBA 在表级别覆盖全局设置,从而为高频表和静态表制定截然不同的精细化策略。
解决了“沉默的杀手”
Autovacuum 有一个专门的“反回卷”安全模式。
当它检测到整个数据库的 TXID 年龄接近危险阈值时,它会变得极具侵略性,强制对那些最老的、即使是“干净”的表执行 VACUUM,其唯一目的就是去冻结 TXID。
这个救命的安全网是手动策略几乎无法实现的。
解决了“人的弱点”
Autovacuum 是一个 24/7 运行的守护进程。
它还内置了 I/O 节流阀(cost_delay, cost_limit),确保其工作不会过度干扰正常的业务查询。
02
Autovacuum的调优
autovacuum并非一个单一的进程。
它由一个启动器(launcher)进程和多个工作(worker)进程组成。
启动器定期(由autovacuum_naptime控制,默认为1分钟)唤醒,检查哪些表需要进行维护,然后启动工作进程来执行实际的VACUUM和ANALYZE任务。
autovacuum的调优可以分为两大核心问题:“何时运行?” 和 “运行多快?”。
我们先来解决第一个问题。
调优核心一:决定“何时运行”——
触发机制详解与实验
autovacuum何时对一个表采取行动,主要由以下参数决定:

为了更直观地理解这些参数如何相互作用,让我们通过一个实验来观察。
实验准备
1)连接到您的PostgreSQL数据库。
2)为了能清晰地观察到autovacuum的每一次执行,我们修改配置,让所有autovacuum活动都被记录下来。
-- 超级用户执行,设置为记录所有自动 vacuum 操作(值为0)
ALTER SYSTEM SET log_autovacuum_min_duration = 0;
-- 重新加载配置文件
SELECT pg_reload_conf();
-- 查看当前参数值
SHOW log_autovacuum_min_duration;
3)您需要有权限查看PostgreSQL的日志文件,具体位置取决于您的操作系统和安装方式。
场景一:小表(默认配置下的行为)
我们将创建一个小表,并观察默认配置如何轻松应对。
1)创建表并插入数据
CREATE TABLE small_test_table (id INT, data TEXT);
INSERT INTO small_test_table SELECT generate_series(1, 1000), 'initial data';
我们现在有了一个1000行的表。
2)计算触发阈值
根据默认配置 (threshold=50, scale_factor=0.2),VACUUM的触发阈值为: 50 + 0.2 * 1000 = 250 个死元组。
3)制造死元组
我们通过UPDATE操作来制造死元组。
更新200行数据,使其不超过阈值。
UPDATE small_test_table SET data = 'updated data' WHERE id <= 200;
4)查询系统视图
可以验证autovacuum是否已经执行。
SELECT relname, n_dead_tup, last_autovacuum
FROM pg_stat_user_tables
WHERE relname = 'small_test_table';
结果示例
relname | n_dead_tup | last_autovacuum
------------------+------------+-------------------------------
small_test_table | 200 |
(1 row)
在autovacuum执行前,您会看到n_dead_tup(死元组数)变为200,并且last_autovacuum时间戳未被更新。
5)制造更多死元组
我们通过UPDATE操作来制造死元组,使其刚好超过阈值。
UPDATE small_test_table SET data = 'updated data' WHERE id <= 50;
6)等待并观察
现在,等待autovacuum启动器下一次唤醒(最多等待autovacuum_naptime,即1分钟)。
查看日志
您应该能在PostgreSQL的日志文件中看到类似下面的信息:
2025-10-26 06:52:41.473 CST [234585] LOG: automatic vacuum of table "postgres.public.small_test_table": index scans: 0
pages: 0 removed, 9 remain, 9 scanned (100.00% of total), 0 eagerly scanned
tuples: 1 removed, 1000 remain, 0 are dead but not yet removable
removable cutoff: 834, which was 0 XIDs old when operation ended
frozen: 3 pages from table (33.33% of total) had 261 tuples frozen
visibility map: 4 pages set all-visible, 3 pages set all-frozen (0 were all-visible)
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removed
avg read rate: 0.000 MB/s, avg write rate: 13.611 MB/s
buffer usage: 49 hits, 0 reads, 5 dirtied
WAL usage: 10 records, 6 full page images, 36564 bytes, 0 buffers full
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
查询系统视图
可以验证autovacuum是否已经执行。
SELECT relname, n_dead_tup, last_autovacuum
FROM pg_stat_user_tables
WHERE relname = 'small_test_table';
结果示例
relname | n_dead_tup | last_autovacuum
------------------+------------+-------------------------------
small_test_table | 0 | 2025-10-26 06:52:41.473318+08
(1 row)
在autovacuum执行后,您会看到n_dead_tup(死元组数)变为0,并且last_autovacuum时间戳被更新。
结论
对于小表,默认配置工作得很好,threshold和scale_factor共同作用,能在合理的时机触发清理。
场景二:大表(默认配置的问题与调优)
现在我们来模拟一个大表,并揭示为什么默认配置对其不适用。
1)创建表并插入数据:
CREATE TABLE large_test_table (id INT, data TEXT);
INSERT INTO large_test_table SELECT generate_series(1, 1000000), 'initial data';
我们创建了一个100万行的大表。
2)计算默认触发阈值
50 + 0.2 * 1,000,000 = 200,050 个死元组。
问题显而易见
数据库需要累积超过20万个死元组(占整个表的20%)才会触发一次autovacuum。
在此期间,表已经显著膨胀,查询性能可能已经开始下降。
3)进行调优
我们不希望等待20万死元组的出现。
让我们为这个大表设置更合理的autovacuum参数。
一个常见的策略是完全依赖一个固定的阈值。
ALTER TABLE large_test_table SET (autovacuum_vacuum_scale_factor = 0, autovacuum_vacuum_threshold = 5000);
现在,VACUUM的触发阈值被固定为5000个死元组,这对于一个百万行级别的表来说是一个更及时、更合理的数字。
4)制造死元组并观察
我们更新5100行数据来超过新的阈值。
UPDATE large_test_table SET data = 'updated data' WHERE id <= 5100;
5)等待并观察
同样,等待最多1分钟。
您将在日志中看到autovacuum: VACUUM public.large_test_table的记录,并且通过查询pg_stat_user_tables可以确认死元组已被清理。
实验结论与最终建议
Autovacuum并非一刀切
默认配置对小表友好,但对大表(通常是写入频繁的核心业务表)来说是不合适的,甚至是危险的。
比例因子是陷阱
autovacuum_vacuum_scale_factor是导致大表autovacuum不及时的主要原因。
表级别调优是关键
识别出系统中的大表和写入频繁的表,并使用ALTER TABLE为它们设置独立的、更激进的autovacuum策略,是保证数据库健康的关键一步。
最终设置建议总结
1)全局配置 (postgresql.conf)
可以保持相对保守的默认值,服务于数据库中的大量普通小表。
2)识别关键表
找出那些行数巨大(如超过50万行)或写入(UPDATE/DELETE)操作非常频繁的表。
3)应用表级别配置
对于这些关键表,执行以下操作:
-- 方案A:使用更小的比例因子和固定的基础阈值
ALTER TABLE your_large_table SET (autovacuum_vacuum_scale_factor = 0.02, autovacuum_vacuum_threshold = 1000);
-- 方案B:完全使用固定的阈值(更可预测)
ALTER TABLE your_very_busy_table SET (autovacuum_vacuum_scale_factor = 0, autovacuum_vacuum_threshold = 10000);
threshold的具体值应根据表的写入频率和可接受的膨胀程度来决定。
10000是一个相对安全的起点。
调优核心二:决定“运行多快”——
速率控制详解
解决了“何时运行”后,我们来看autovacuum如何控制自己的工作速度,以避免对系统造成过大冲击。
这就是基于成本的延迟(Cost-Based Vacuum Delay)机制。
这套系统像一个记账本。
autovacuum有一个“预算”,每执行一个操作(如读取一个数据块)就会“花钱”,当预算用完时,就必须“强制休息”来攒钱。

工作流程
1)autovacuum工作进程开始处理一个表,其内部成本计数器清零。
2)它扫描数据块(page,通常为8KB)。
3)每扫描一个块,就根据上述规则累加成本:
如果块在内存中
成本 +1 (vacuum_cost_page_hit)。
如果块在磁盘中
成本 +2 (vacuum_cost_page_miss)。
如果VACUUM需要修改这个块(例如,移除死元组并更新可见性映射),则额外增加成本 +20 (vacuum_cost_page_dirty)。
4)在处理完每个块后,检查累计成本是否超过 autovacuum_vacuum_cost_limit。
5)如果超过,进程将暂停 autovacuum_vacuum_cost_delay 毫秒,然后将成本计数器清零,继续从上一个位置工作。
速率的本质
VACUUM的整体速率,实际上就是 (每个工作周期处理的数据量) (每个工作周期花费的时间)。
而暂停时间 (cost_delay) 往往是决定速率的主要因素。
不同参数配置下的VACUUM1秒内清理速率估算
为了直观地展示参数的影响,我们来模拟几种场景。
假设我们的数据块大小为8KB。
默认情况下,autovacuum_vacuum_cost_delay=2ms,我们可以近似估算出vacuum进程1秒内有500个工作周期。
1s=1000ms,1秒可以调用vacuum进程1000/2=500次。
场景一:绝对理想情况(所有块都在缓存,且都被清理)
单个块的成本 = hit(1) + dirty(20) = 21
每个工作周期能清理的块数 = 200 (limit) 21 (cost) ≈ 9.52,取整为 9个块。
1秒内理论上能清理的总块数 = 500 (周期数) * 9 (块/周期) = 4500 个块。
转换成吞吐率 = (4500 * 8 KB) 1024 = 35.16 MB/s
场景二:最坏情况(所有块都不在缓存,且都被清理)
单个块的成本 = miss(2) + dirty(20) = 22
每个工作周期能清理的块数 = 200 (limit) 22(cost) ≈ 9.09,取整为 9个块。
1秒内理论上能清理的总块数 = 500 (周期数) * 9 (块/周期) = 4500 个块。
转换成吞吐率 = (4500 * 8 KB) 1024 = 35.16 MB/s
场景三:只读扫描,无清理(例如,一个干净的表被VACUUM扫描)
场景3A (全在缓存)
成本为1,周期块数200,总块数 500 * 200 = 100,000,吞吐率 781.25 MB/s。
场景3B (全在磁盘)
成本为2,周期块数100,总块数 500 * 100 = 500,000,吞吐率 380.62MB/s。
总结

速率控制的调优建议
检查cost_delay
如果你的PostgreSQL版本低于12且使用SSD,首要任务是检查autovacuum_vacuum_cost_delay是否合理,若设置过大,建议调整为2ms。
按需提高cost_limit
如果autovacuum总是跟不上,可以适度提高autovacuum_vacuum_cost_limit(例如从200提高到500或1000),让它在每次暂停前能做更多的工作。
手动VACUUM加速
在维护窗口执行手动VACUUM时,可以通过SET vacuum_cost_delay = 0;临时禁用速率限制,使其全速运行。
03
监控Autovacuum的健康状况
调优并非一劳永逸,持续的监控至关重要。
关键监控手段
1)查询系统视图
pg_stat_activity
可以查看当前正在运行的autovacuum工作进程。
pg_stat_user_tables
这是最重要的监控视图。
其中的n_dead_tup(死元组数)、last_autovacuum和last_autoanalyze(上次执行时间)等列,能清晰地反映autovacuum的工作情况。
一个有用的查询,用于找出可能需要关注的表(死元组多且长时间未VACUUM):
SELECT
relname,
n_live_tup,
n_dead_tup,
last_autovacuum,
last_autoanalyze
FROM
pg_stat_user_tables
WHERE
n_dead_tup > 10000 -- 根据实际情况调整
ORDER BY
n_dead_tup DESC;
2)日志分析
设置log_autovacuum_min_duration参数(例如250ms或1s),可以将运行时间超过指定阈值的autovacuum活动记录到日志中。
这有助于发现那些VACUUM缓慢、存在问题的表。
写在最后
Autovacuum 不是可有可无的后台线程,而是 PostgreSQL 自愈系统的核心。
理解它,你才能真正掌控数据库的生命节奏;调优它,你才能让系统在负载中依旧平稳。
当下次你再看到“autovacuum”出现在日志里,请记得——那不是噪音,而是守护。
实际上,多数数据库性能问题的根源,常常与操作系统层面的资源配置、性能瓶颈等因素密切相关。
如果你还想系统补齐 Linux 的实战短板,推荐看看刘峰老师的 Linux 系列课程,循序渐进,从命令行到运维部署,一步到位。

作者介绍
大家好,我是刘峰,安丫科技创始人 & 数据库技术高级讲师,专注于 PostgreSQL、国产数据库运维与迁移、数据库性能优化 等方向。
作为 PG中国分会官方授权讲师、PostgreSQL ACE 讲师认证专家,我长期活跃在一线项目实战中,拥有 10年以上大型数据库管理与优化经验,曾深度参与电信、金融、政务等多个行业的数据库性能调优与迁移项目。
欢迎关注我,一起深入探索数据库的无限可能,技术交流不设限!
📌 觉得有收获的话,记得点赞、收藏、转发支持一下哦,别忘了关注我获取更多数据库干货~
安呀智数据坊|我们能做什么
无论你是业务系统的技术负责人,还是数据部门的第一响应人,我们都能为你提供可靠的支持:
数据库类型支持
Oracle / MySQL / PostgreSQL / SQL Server 等主流数据库
核心服务内容
性能优化 / 故障处理 / 数据迁移 / 备份恢复 / 版本升级 / 补丁管理
系统性支持
深度巡检 / 高可用架构设计 / 应用层兼容评估 / 运维工具集成
专项能力补充
定制课程培训 / 甲方团队辅导 / 复杂问题协作排查 / 紧急救援支持
📮 如果你有一张删不掉的表、一个跑不动的查询,或者一场说不清的升级风险,欢迎来找我们聊聊。
关键词回复(可见相应文章):
oracle、mysql、pg、postgresql、sql、性能优化、故障处理、数据迁移、备份恢复、版本升级、补丁管理、深度巡检、解决方案、架构设计......

添好友1对1咨询
\ | /
★
动动你的手指
给【安呀智数据坊】加个星标吧~
这样你就不会丢下我啦~
记得加星标呀!









