Cheatsheet for Postgres maintenance. Mostly handled by autovacuum; sometimes manual is needed.

VACUUM basics

VACUUM users;             -- reclaim deleted rows
VACUUM ANALYZE users;     -- + update planner stats
VACUUM FULL users;        -- rewrites table; takes exclusive lock (avoid in prod)

VACUUM FULL reclaims space to OS but locks. Use pg_repack instead for online.

Autovacuum

Enabled by default. Tune per-table:

__table_args__ = (
    {"postgresql_with": {
        "autovacuum_vacuum_scale_factor": 0.05,    # vacuum when 5% of table is dead
        "autovacuum_analyze_scale_factor": 0.02,
        "autovacuum_vacuum_threshold": 1000,
        "autovacuum_analyze_threshold": 500,
    }},
)

For high-churn tables: lower thresholds.

ANALYZE

ANALYZE users;
ANALYZE users (email);   -- specific column

Updates statistics used by the query planner. Run after bulk inserts.

REINDEX

-- Single index
REINDEX INDEX CONCURRENTLY ix_users_email;

-- Whole table
REINDEX TABLE CONCURRENTLY users;

-- Schema or database (non-concurrent)
REINDEX SCHEMA public;
REINDEX DATABASE myapp;

CONCURRENTLY is online. For heavily-bloated indexes.

Find bloat

SELECT
    schemaname, tablename,
    pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS total,
    pg_size_pretty(pg_relation_size(schemaname||'.'||tablename)) AS table_size,
    pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename) - pg_relation_size(schemaname||'.'||tablename)) AS index_size,
    n_dead_tup, n_live_tup
FROM pg_stat_user_tables
JOIN pg_class c ON c.relname = tablename
ORDER BY n_dead_tup DESC LIMIT 20;

High n_dead_tup vs n_live_tup = bloated.

pgstattuple (precise bloat)

CREATE EXTENSION pgstattuple;
SELECT * FROM pgstattuple('users');

dead_tuple_percent field tells the story.

pg_repack (online table rewrite)

pg_repack -h db -U user -d myapp -t huge_table

Online table compaction. No exclusive lock. Use periodically for heavy-write tables.

For partitioned tables: repack each partition.

VACUUM FREEZE / wraparound

Postgres uses transaction IDs (32-bit). After ~2B transactions: wraparound.

Autovacuum runs aggressive VACUUM FREEZE proactively. If you see autovacuum_freeze_max_age warnings: don’t disable autovacuum on those tables.

SELECT
    relname,
    age(relfrozenxid) AS xid_age,
    age(relfrozenxid) > 2_000_000_000 AS approaching_wraparound
FROM pg_class
WHERE relkind = 'r' AND relnamespace = 'public'::regnamespace
ORDER BY age(relfrozenxid) DESC LIMIT 20;

Index usage stats

SELECT
    schemaname, tablename, indexname,
    idx_scan, idx_tup_read, idx_tup_fetch
FROM pg_stat_user_indexes
ORDER BY idx_scan ASC LIMIT 20;

Identify unused indexes (idx_scan = 0).

Stats reset

SELECT pg_stat_reset();   -- whole DB
SELECT pg_stat_reset_single_table_counters('users'::regclass);

After a major schema change, reset to measure fresh.

Long-running queries

SELECT pid, age(clock_timestamp(), query_start), state, query
FROM pg_stat_activity
WHERE state = 'active' AND query_start < now() - interval '5 minutes'
ORDER BY query_start;

Kill if needed:

SELECT pg_cancel_backend(<pid>);    -- gentle
SELECT pg_terminate_backend(<pid>); -- forceful

Locks held

SELECT
    bl.locktype, bl.relation::regclass,
    bl.transactionid, bl.virtualtransaction, bl.pid,
    bl.mode, bl.granted,
    a.query, a.state, age(clock_timestamp(), a.query_start) AS age
FROM pg_locks bl
JOIN pg_stat_activity a ON a.pid = bl.pid
WHERE NOT bl.granted
ORDER BY age DESC;

Find blocked queries and their blockers.

Maintenance window practices

  • Low-traffic window (off-hours).
  • Communicate to ops.
  • Monitor replica lag during the op.
  • Have rollback / abort plan.

Automation

-- pg_cron schedules
SELECT cron.schedule('vacuum-tables', '0 3 * * *', $$VACUUM ANALYZE users$$);
SELECT cron.schedule('reindex-weekly', '0 4 * * 0', $$REINDEX TABLE CONCURRENTLY users$$);

Or via cron + psql.

Common mistakes

  • VACUUM FULL in production — exclusive lock; pg_repack instead.
  • Disabling autovacuum — wraparound risk; bloat.
  • REINDEX without CONCURRENTLY on hot tables.
  • Not monitoring xid_age — surprise wraparound.

Read this next

If you want my maintenance runbook + pg_cron schedule template, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .