active, idle, idle in transaction, idle in transaction (aborted)
query
Current (or last, if idle) statement text
query_start / xact_start
When the statement / transaction began
wait_event_type / wait_event
What it is waiting on (Lock, IO, Client, LWLock)
backend_type
client backend, autovacuum worker, walsender, …
-- What's running right now, longest firstSELECT pid, usename, application_name, client_addr, state, now() - query_start AS running_for, wait_event_type || ':' || wait_event AS waiting_on, left(query, 80) AS queryFROM pg_stat_activityWHERE backend_type = 'client backend' AND pid <> pg_backend_pid()ORDER BY query_start NULLS LAST;
-- Connection count by state (are we near max_connections?)SELECT state, count(*) FROM pg_stat_activityWHERE backend_type = 'client backend'GROUP BY state ORDER BY count(*) DESC;SELECT current_setting('max_connections')::int AS max_conn, count(*) AS usedFROM pg_stat_activity;
-- Long-running queries (> 5 minutes) and old open transactionsSELECT pid, state, now() - query_start AS query_age, now() - xact_start AS xact_age, left(query, 80)FROM pg_stat_activityWHERE backend_type = 'client backend' AND (now() - query_start > interval '5 minutes' OR (state LIKE 'idle in transaction%' AND now() - xact_start > interval '5 minutes'));
Idle in Transaction: The worst state — holds locks and blocks vacuum from cleaning dead rows. Set idle_in_transaction_session_timeout
Cancelling and Killing Queries
Function
Effect
Use
pg_cancel_backend(pid)
Cancels the current query; session stays
First try — gentle (like Ctrl-C)
pg_terminate_backend(pid)
Kills the whole session/connection
Stuck idle-in-transaction, cancel didn’t work
pg_terminate_backend(pid, timeout_ms)
Waits for it to die (PG 14+)
Scripts
-- Cancel one query (replace 12345 with a pid from pg_stat_activity)-- SELECT pg_cancel_backend(12345);-- SELECT pg_terminate_backend(12345);-- Kill all sessions idle in a transaction for more than 10 minutesSELECT pid, pg_terminate_backend(pid) AS terminatedFROM pg_stat_activityWHERE state = 'idle in transaction' AND now() - state_change > interval '10 minutes' AND pid <> pg_backend_pid();-- Prevent runaway queries in the first placeSET statement_timeout = '30s'; -- per session; or ALTER ROLE app SET …SET idle_in_transaction_session_timeout = '60s';SET lock_timeout = '5s';RESET ALL;
Locks and Blocking
Who Is Blocking Whom
-- The one query to remember: blocked sessions and the pid(s) blocking themSELECT blocked.pid AS blocked_pid, left(blocked.query, 60) AS blocked_query, now() - blocked.query_start AS waiting_for, blocking.pid AS blocking_pid, blocking.state AS blocking_state, left(blocking.query, 60) AS blocking_queryFROM pg_stat_activity blockedJOIN LATERAL unnest(pg_blocking_pids(blocked.pid)) AS b(pid) ON trueJOIN pg_stat_activity blocking ON blocking.pid = b.pidORDER BY waiting_for DESC;
-- Raw lock table: which relations are locked, in which mode, granted or waitingSELECT l.pid, l.locktype, l.relation::regclass AS relation, l.mode, l.granted, a.state, left(a.query, 60) AS queryFROM pg_locks lJOIN pg_stat_activity a ON a.pid = l.pidWHERE l.relation IS NOT NULL AND a.pid <> pg_backend_pid()ORDER BY l.granted, l.pid;
granted = false: That session is waiting
Lock Tree: A waiting ALTER TABLE (AccessExclusiveLock) makes everything behind it wait too — see Transactions and Locking
Deadlocks: Logged automatically; count with SELECT deadlocks FROM pg_stat_database WHERE datname = current_database();
pg_stat_statements: Slowest and Most Expensive Queries
Setup
# postgresql.conf (requires restart)shared_preload_libraries = 'pg_stat_statements'pg_stat_statements.track = top
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
Top Queries
-- Top 10 by TOTAL time — what is the database spending its life onSELECT round(total_exec_time::numeric, 1) AS total_ms, calls, round(mean_exec_time::numeric, 2) AS mean_ms, round((100 * total_exec_time / sum(total_exec_time) OVER ())::numeric, 1) AS pct, rows, left(query, 80) AS queryFROM pg_stat_statementsORDER BY total_exec_time DESCLIMIT 10;
-- Slowest on average (called at least 10 times), plus cache hit ratio per querySELECT calls, round(mean_exec_time::numeric, 2) AS mean_ms, round(max_exec_time::numeric, 2) AS max_ms, round(100.0 * shared_blks_hit / NULLIF(shared_blks_hit + shared_blks_read, 0), 1) AS cache_hit_pct, left(query, 80) AS queryFROM pg_stat_statementsWHERE calls >= 10ORDER BY mean_exec_time DESCLIMIT 10;-- Reset counters after a deploy / tuning changeSELECT pg_stat_statements_reset();
Normalised Text: Literals become $1, $2 so the same query shape aggregates together
Total vs Mean: Optimise total time first — a 2 ms query called 10M times beats a 5 s report run twice
Sizes
-- Database sizesSELECT datname, pg_size_pretty(pg_database_size(datname)) AS sizeFROM pg_databaseORDER BY pg_database_size(datname) DESC;-- Biggest tables: heap, indexes, TOAST, totalSELECT c.relname AS table, pg_size_pretty(pg_relation_size(c.oid)) AS heap, pg_size_pretty(pg_indexes_size(c.oid)) AS indexes, pg_size_pretty(pg_total_relation_size(c.oid)) AS total, c.reltuples::bigint AS est_rowsFROM pg_class cJOIN pg_namespace n ON n.oid = c.relnamespaceWHERE c.relkind = 'r' AND n.nspname NOT IN ('pg_catalog', 'information_schema')ORDER BY pg_total_relation_size(c.oid) DESCLIMIT 10;-- Biggest indexesSELECT indexrelid::regclass AS index, relid::regclass AS table, pg_size_pretty(pg_relation_size(indexrelid)) AS sizeFROM pg_stat_user_indexesORDER BY pg_relation_size(indexrelid) DESCLIMIT 10;
Function
Measures
pg_relation_size(t)
Main heap only
pg_table_size(t)
Heap + TOAST + FSM/VM
pg_indexes_size(t)
All indexes of the table
pg_total_relation_size(t)
Everything
pg_database_size(db)
Whole database
Fast Row Count Estimate
-- count(*) scans the whole table under MVCC; the planner's estimate is instantSELECT reltuples::bigint AS estimated_rows FROM pg_class WHERE oid = 'orders'::regclass;
Index Usage
-- Unused indexes (idx_scan = 0 since stats reset) — candidates to drop, except PK/UNIQUESELECT s.relname AS table, s.indexrelname AS index, pg_size_pretty(pg_relation_size(s.indexrelid)) AS size, s.idx_scanFROM pg_stat_user_indexes sJOIN pg_index i ON i.indexrelid = s.indexrelidWHERE s.idx_scan = 0 AND NOT i.indisuniqueORDER BY pg_relation_size(s.indexrelid) DESC;-- Tables read mostly by sequential scans (maybe missing an index)SELECT relname, seq_scan, seq_tup_read, idx_scan, round(seq_tup_read::numeric / NULLIF(seq_scan, 0)) AS avg_rows_per_seq_scanFROM pg_stat_user_tablesWHERE seq_scan > 0ORDER BY seq_tup_read DESCLIMIT 10;-- Invalid indexes left by failed CREATE INDEX CONCURRENTLYSELECT indexrelid::regclass FROM pg_index WHERE NOT indisvalid;
Vacuum, Bloat and Health
-- Dead rows and last (auto)vacuum/analyze per tableSELECT relname, n_live_tup, n_dead_tup, round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 1) AS dead_pct, last_autovacuum, last_autoanalyzeFROM pg_stat_user_tablesORDER BY n_dead_tup DESCLIMIT 10;-- Transaction ID wraparound risk (alert well before 2 billion)SELECT datname, age(datfrozenxid) AS xid_age, round(100.0 * age(datfrozenxid) / 2000000000, 1) AS pct_to_wraparoundFROM pg_databaseORDER BY xid_age DESC;-- Buffer cache hit ratio (OLTP should be > 99%)SELECT round(100.0 * sum(blks_hit) / NULLIF(sum(blks_hit) + sum(blks_read), 0), 2) AS cache_hit_pctFROM pg_stat_database;-- Is autovacuum running right now?SELECT pid, now() - xact_start AS running_for, queryFROM pg_stat_activityWHERE backend_type = 'autovacuum worker';
Settings and Server Info
SELECT version();SELECT pg_postmaster_start_time(), now() - pg_postmaster_start_time() AS uptime;SELECT name, setting, unit, sourceFROM pg_settingsWHERE name IN ('max_connections', 'shared_buffers', 'work_mem', 'maintenance_work_mem', 'effective_cache_size', 'random_page_cost', 'statement_timeout', 'idle_in_transaction_session_timeout', 'autovacuum')ORDER BY name;-- Settings changed from defaultsSELECT name, setting, source FROM pg_settings WHERE source NOT IN ('default', 'override') ORDER BY name;
Replication Quick Checks
-- On the primary: connected standbys and lagSELECT application_name, client_addr, state, sync_state, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)) AS replay_lag_bytes, replay_lagFROM pg_stat_replication;-- On a standbySELECT pg_is_in_recovery(), now() - pg_last_xact_replay_timestamp() AS replay_delay;