概念概述
自动清理与死元组回收(Autovacuum and Dead Tuple Reclamation)指的是:自动清理回收 MVCC 更新留下的死行版本并刷新统计,调节其阈值可防止表膨胀与事务 ID 回卷。
从更严格的定义看,Autovacuum is the background process that removes row versions made obsolete by updates and deletes, refreshes planner statistics, and advances the visibility map so indexes can be scanned more cheaply. 该概念在工程实践中的价值,在于它能帮助团队用统一的语言描述一个具体的质量属性或设计约束,从而在评审与协作中减少歧义。
为什么需要它
It is the operational counterpart of the multi-version concurrency control model used by PostgreSQL and similar engines. 它与 LATERAL Joins、Upsert with INSERT … ON CONFLICT、The RETURNING Clause 等概念经常一起出现,用于解决相近的工程问题。理解它的适用场景,有助于在多种备选方案之间做出有依据的取舍,而不是凭直觉选择技术方案。
常见误解
- vacuum rewrites tables into a compact form immediately
- disabling autovacuum improves performance on a busy table
需要避免的反模式
- disabling autovacuum on high-churn tables to avoid the overhead
- setting thresholds so high that a large table is never vacuumed
实践要点
- Every update creates a new row version and marks the old one dead; without reclamation the table grows even when the row count is constant, a condition called bloat.
- Plain vacuum only makes space reusable inside the table, whereas vacuum full rewrites it and takes an exclusive lock, so routine maintenance relies on the former.
- The visibility map and the free space map shorten index-only scans and speed future inserts, which is why monitoring vacuum lag is as important as monitoring query time.
参考来源
https://www.postgresql.org/docs/current/routine-vacuuming.html
代码示例
interface VersionedRow {
readonly key: string;
readonly value: string;
readonly startTs: number;
readonly endTs: number;
}
export class MvccStore {
private versions: VersionedRow[] = [];
private clock = 0;
beginTransaction(): number {
return ++this.clock;
}
write(key: string, value: string, ts: number): void {
for (const row of this.versions) {
if (row.key === key && row.endTs === Number.POSITIVE_INFINITY) row.endTs = ts;
}
this.versions.push({ key, value, startTs: ts, endTs: Number.POSITIVE_INFINITY });
}
read(key: string, snapshotTs: number): string | undefined {
let found: VersionedRow | undefined;
for (const row of this.versions) {
if (row.key !== key) continue;
if (row.startTs <= snapshotTs && snapshotTs < row.endTs) {
if (!found || row.startTs > found.startTs) found = row;
}
}
return found?.value;
}
}