Skip to main content
RunBook Academy

← All checklists in PostgreSQL

As neededpg-pre-maintenance

PostgreSQL Pre-Maintenance Checklist

17 items ·12 critical ·5 warn ·0 info

How to use this checklist

Before any planned change: a configuration parameter, a schema migration, a vacuum, an index rebuild, a restart.

It takes about ten minutes and most of its value is in two items — the written rollback and the lock_timeout.

Classify the change before you plan the window

The lock queue is the most common way a change becomes an outage

Check what will collide

A scheduled job taking ACCESS EXCLUSIVE during your window will cancel whatever is running. For a vacuum that is expensive in a specific way:

For vacuum work, check the horizon first

SELECT 'slot' AS holder, slot_name, greatest(age(xmin), age(catalog_xmin)) AS xid_age
FROM pg_replication_slots WHERE xmin IS NOT NULL OR catalog_xmin IS NOT NULL
UNION ALL
SELECT 'session', pid::text, age(backend_xmin)
FROM pg_stat_activity WHERE backend_xmin IS NOT NULL
UNION ALL
SELECT 'prepared', gid, age(transaction) FROM pg_prepared_xacts
ORDER BY 3 DESC NULLS LAST;

A vacuum against a pinned horizon runs for hours, exits zero, and removes nothing. The VERBOSE output says so in a specific way: 0 removed, a large not yet removable count, and no new relfrozenxid line at all.

Resolve the holder first, or the window is spent for nothing.

Define the verification before the change

What will be checked, and what result counts as success. Deciding afterwards invites confirmation of whatever happened, and “it seems fine” is not a state anybody can act on later.

Same for the abort criteria. A change with none continues until somebody loses their nerve, which is reliably later than it should be.

Where the numbers come from

The pre-change state is captured, not remembered: pg_settings for the non-default parameters, row counts on the significant tables, EXPLAIN output for the significant queries, and the replication topology with each standby’s position. None of these can be reconstructed afterwards, and they are what the post-maintenance review compares against.

The window size comes from a rehearsal on production-scale data. The vacuum horizon comes from pg_stat_activity, pg_replication_slots and pg_prepared_xacts, checked before any vacuum work, because tuning autovacuum harder achieves nothing while a snapshot is pinned.

Access this needs

Read access to the change record, the runbook the change follows, and the rollback it names. A role holding pg_monitor on every affected node for the pre-change state, and pg_read_all_settings for the configuration this change touches.

Read access to the most recent backup’s record and to the date it was last proven by restore — not the date it was taken.

Nothing in this checklist requires the authority to make the change. It is the gate the change passes, and it is completed by somebody who can say no.

What the review produces

A dated record naming the reviewer, the change, the window, the rollback and its cost, and the disposition of every item — completed before the window opens, not written up afterwards.

Attach the captured pre-change state, because it is the only thing that makes the post-maintenance verification possible. A change whose verification was not defined before it started cannot be verified after it, and that is a blocker rather than an observation.

Sign-off

  • Reviewer: ________________ Date: ___________
  • Change owner: _____________ Date: ___________
  • Service owner: ____________ Date: ___________

Every critical item must pass before the window opens. A failing critical item postpones the window; it is not a risk to carry into it. Record the date, the reviewer, and the name of whoever authorised proceeding with any item unmet.

Critical12 items

  1. psql -c "SELECT name, setting, context, pending_restart FROM pg_settings WHERE name = 'work_mem';"
  2. psql -c "SELECT name, setting FROM pg_settings WHERE pending_restart;"
  3. psql -c "SET lock_timeout = '5s';"
  4. psql -c "SELECT pid, usename, application_name, now() - state_change AS idle_for, left(query,60) FROM pg_stat_activity WHERE state = 'idle in transaction' ORDER BY state_change;"
  5. psql -c "SELECT 'slot' AS holder, slot_name, greatest(age(xmin), age(catalog_xmin)) AS xid_age FROM pg_replication_slots WHERE xmin IS NOT NULL OR catalog_xmin IS NOT NULL UNION ALL SELECT 'session', pid::text, age(backend_xmin) FROM pg_stat_activity WHERE backend_xmin IS NOT NULL ORDER BY 3 DESC NULLS LAST;"
  6. psql -c "SHOW synchronous_standby_names;"

Warning5 items