PG.L7.AUTOVACUUM_DISABLED — A table somebody switched autovacuum off for
- Category: performance
- Level: 7
- Confidence: deterministic
- Downtime class: none — nothing here is a schema change
- Stability: stable
- Suites: audit
- Applies to: PostgreSQL 18
What it reports
A table whose storage parameters carry autovacuum_enabled = false, read out of pg_class.reloptions.
Three states, and only one of them is this finding:
| The parameter is… | Meaning | Reported? |
|---|---|---|
false | somebody switched it off for this table | yes |
true | somebody switched it on explicitly | no |
| absent | the table runs under the cluster's setting — the ordinary case | no |
The third row is why the value is kept as the server's own spelling rather than cast to a boolean: a boolean has no third value, and the third value is the common one. Reading "nobody said" as "somebody said no" would report every table in the schema.
Two consequences, and the second is not optional
The estimates stop meaning anything. Without autovacuum the table accumulates dead rows, so its file size says the least about the rows a rewrite would actually copy — and the size is what every time and space statement about a deploy rests on. The row estimate goes stale for the same reason: autovacuum is what triggers most ANALYZE runs, so reltuples stops being refreshed along with the space.
The server takes it back anyway. Turning autovacuum off does not stop the freeze horizon. When the table crosses autovacuum_freeze_max_age, PostgreSQL starts an anti-wraparound vacuum on it regardless of this parameter — and that worker does not yield to a conflicting lock request the way an ordinary one does.
So the setting does not avoid the vacuum. It defers it to a moment nobody chose, and makes the one that eventually comes larger. DEPLOY.PREFLIGHT.FREEZE_HORIZON is the deploy-time half of the same fact: it reports the table this is about to happen to, while a deploy is waiting.
Flagged
Schema::table('events', function (Blueprint $table) {
DB::statement('ALTER TABLE events SET (autovacuum_enabled = false)');
});
Preferred
Leave it on, and tune instead of disabling where the default is genuinely wrong for the table:
DB::statement('ALTER TABLE events SET (autovacuum_vacuum_scale_factor = 0.02)');
A scale factor says vacuum this one more eagerly than the rest; enabled = false says never, until the server insists. The first is a decision about this table, the second is a decision about a moment somebody else will pick.
False positives, and there is a real one
A table maintained by hand is a legitimate shape. A table bulk-loaded on a schedule is vacuumed better at a chosen moment than at an arbitrary one, and a team that runs VACUUM (ANALYZE) in its own window has made a defensible call.
This check cannot see that schedule — it is a cron, a job, an Ansible task, none of which are in the database. So the answer is an ignore entry with the reason beside it, not a heuristic in the rule guessing at intent from a table's name or size. A guess would be wrong on exactly the schemas that thought about this hardest.
What it does not claim
- That the table is bloated. That is a measurement this rule does not make; it reports the setting, and what follows from it depends on how much the table is written to.
- That turning it back on is free. The first autovacuum after a long pause is the large one — which is the argument for turning it on at a moment you pick rather than leaving it off until the server picks one.
- Anything about per-table thresholds. A table whose
autovacuum_vacuum_thresholdorautovacuum_vacuum_scale_factoris set so high that it never fires is the same state written more quietly, and this rule does not read those. It is the narrower, unambiguous case on purpose.
Why there is no MySQL twin
InnoDB has no per-table equivalent. Its purge and its adaptive flushing are server-wide, and the nearest table-level knob — STATS_AUTO_RECALC — decides whether the optimizer's statistics are refreshed, not whether dead rows are reclaimed. A rule registered there would be a different finding wearing this one's name.
Sources
- PostgreSQL 18: routine vacuuming — why dead rows have to be reclaimed at all, and the section on preventing transaction-id wraparound which states that anti-wraparound autovacuum runs even on a table with autovacuum disabled.
- PostgreSQL 18:
CREATE TABLEstorage parameters — whereautovacuum_enabledlives, and the per-table thresholds this rule deliberately does not read.
The deploy-time half
This rule reports every such table in the database, which is the right shape for an audit and the
wrong one for a deploy: the list is read once, and somebody planning a migration has no reason to go
back to it. DEPLOY.PREFLIGHT.AUTOVACUUM_DISABLED asks
the narrower question at the moment it matters — is one of the tables this run touches in that
state — and adds the consequence an audit cannot state: that the report's own size and duration
figures for that table are the least trustworthy ones in it.