Monitoring

PostgreSQL Health Check: A DBA Checklist With Queries You Can Run Today

A PostgreSQL health check is not a dashboard screenshot. It is a short list of questions that predict the next outage. Here are the ones we ask first, the exact queries behind them, and how to read the answers on a large production database.

· 13 min read · AG Data Team

A PostgreSQL health check answers one question: what is most likely to take this database down, and how soon? Most problems we find in production were visible weeks earlier in the statistics views. They were just never looked at. This checklist is the order we go through on a new system, with queries that work on any modern PostgreSQL and notes on what to treat as urgent.

1. Version, uptime and wraparound risk

SELECT version(), pg_postmaster_start_time(), now() - pg_postmaster_start_time() AS uptime;

-- transaction ID age per database (forced anti-wraparound vacuum starts at autovacuum_freeze_max_age)
SELECT datname, age(datfrozenxid) AS xid_age,
       current_setting('autovacuum_freeze_max_age') AS freeze_max_age
FROM pg_database ORDER BY 2 DESC;

2. Connections and long transactions

SELECT state, count(*) FROM pg_stat_activity GROUP BY state;

SELECT pid, usename, state, now() - xact_start AS xact_age, left(query, 60)
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start LIMIT 10;

Look at two things. First, how many sessions are idle in transaction. Second, the age of the oldest open transaction. A transaction open for hours holds back vacuum: rows that became dead after its snapshot cannot be removed until it ends. On a busy database that alone produces bloat and slower queries. Find the application behind it, and set idle_in_transaction_session_timeout as a safety net.

3. Replication, slots and WAL archiving

SELECT application_name, state, sync_state, replay_lag FROM pg_stat_replication;

SELECT slot_name, slot_type, active, wal_status, safe_wal_size
FROM pg_replication_slots;

SELECT archived_count, failed_count, last_failed_wal, last_failed_time
FROM pg_stat_archiver;

4. Database-level counters

SELECT datname,
       round(100.0 * blks_hit / nullif(blks_hit + blks_read, 0), 2) AS cache_hit_pct,
       temp_files, pg_size_pretty(temp_bytes) AS temp_size,
       deadlocks, checksum_failures, stats_reset
FROM pg_stat_database WHERE datname IS NOT NULL;

5. Tables and indexes

-- dead tuples and autovacuum activity
SELECT relname, n_live_tup, n_dead_tup, last_autovacuum, last_autoanalyze
FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10;

-- large indexes never used since the last stats reset (skip unique ones)
SELECT s.relname, s.indexrelname, pg_size_pretty(pg_relation_size(s.indexrelid)) AS size
FROM pg_stat_user_indexes s JOIN pg_index i ON i.indexrelid = s.indexrelid
WHERE s.idx_scan = 0 AND NOT i.indisunique
ORDER BY pg_relation_size(s.indexrelid) DESC LIMIT 10;

-- invalid indexes, e.g. left behind by a failed CREATE INDEX CONCURRENTLY
SELECT indexrelid::regclass FROM pg_index WHERE NOT indisvalid;

6. Checkpoints and top queries

-- PostgreSQL 17+: checkpoints by cause
SELECT num_timed, num_requested, num_done FROM pg_stat_checkpointer;
-- PostgreSQL 16 and older: checkpoints_timed, checkpoints_req in pg_stat_bgwriter

-- top queries by total time (pg_stat_statements extension; column names from PostgreSQL 13)
SELECT calls, round(total_exec_time) AS total_ms, round(mean_exec_time, 1) AS mean_ms,
       left(query, 60)
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;

If requested checkpoints clearly outnumber timed ones, WAL fills max_wal_size before checkpoint_timeout expires, which means more I/O and more full-page writes. The pg_stat_checkpointer view exists from PostgreSQL 17. On older versions, the same counters are in pg_stat_bgwriter. For queries, sort pg_stat_statements by total time, not mean time: a 5 ms query called fifty million times a day usually matters more than one slow report.

7. Security basics

SHOW password_encryption;      -- scram-sha-256 is the default since PostgreSQL 14
SHOW data_checksums;
SHOW ssl;
SELECT rolname FROM pg_roles WHERE rolsuper;
SELECT rolname FROM pg_authid WHERE rolpassword LIKE 'md5%';
One snapshot is not a health check. Most of these counters are cumulative. Run the queries twice, a day or a week apart, and look at the difference. A table that gained 50 million dead rows in a day tells you far more than its total.

What changes on large databases

Key takeaways

PostgreSQL Health Check Monitoring DBA Checklist
Work with us

Want a second pair of eyes on your PostgreSQL?

We run a full PostgreSQL health check on your production cluster, explain every finding in plain language and give you a prioritised fix list.

Get in touch More from the blog