C04 — Long-Running Queries & Lock Contention¶
Sessions that have been doing something for too long, and the contention that causes. A query running for hours, a transaction left idle-in-transaction (which blocks vacuum from cleaning up dead rows it might still need to see), and chains of sessions blocking each other on the same locks — all things that degrade or stall an application well before they show up anywhere else.
11 checks, PGHF04-001 through PGHF04-011. Every check here is PGHF-namespace and built-in — see Metadata Columns Explained for what each field below actually means, and Resetting to Defaults for why these definitions can't be hand-edited in place.
PGHF04-001 — Longest-running query age¶
A long-running query holds its locks and its snapshot open for as long as it runs — the longer it runs, the more it blocks autovacuum progress and the more resources it monopolizes; catching it early gives an operator time to cancel it before it does real damage.
How to fix
- Identify it and its plan:
SELECT pid, now() - query_start AS age, state, query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY age DESC;
- If it's genuinely stuck or no longer needed, cancel it (graceful) or terminate it (forceful):
SELECT pg_cancel_backend(<pid>); -- ROLLBACK, query stops
SELECT pg_terminate_backend(<pid>); -- connection dropped outright
- To prevent recurrence, set
statement_timeout(PGHF04-005) as a backstop.
PGHF04-002 — Idle-in-transaction age¶
An idle-in-transaction session holds locks and an open snapshot while doing nothing — it silently prevents autovacuum from reclaiming dead tuples for as long as it sits there, same underlying risk as PGHF01-004.
How to fix
See PGHF01-004's remediation — terminate the offending session and set idle_in_transaction_session_timeout so this can't recur unbounded.
PGHF04-003 — Lock blocker chains¶
A blocker chain means one session is stalling a whole queue of others waiting on the same lock — the blocked sessions are often invisible in a simple activity count, but they're real, growing latency the application is absorbing right now.
How to fix
- Find the head of the chain — the session that's blocking but not itself blocked:
SELECT blocked_locks.pid AS blocked_pid, blocking_locks.pid AS blocking_pid,
blocked_activity.query AS blocked_query, blocking_activity.query AS blocking_query
FROM pg_locks blocked_locks
JOIN pg_locks blocking_locks
ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
AND blocking_locks.pid != blocked_locks.pid
JOIN pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;
- Decide whether the blocker is legitimate (a long DDL/maintenance operation) or a bug (a forgotten open transaction) — terminate it only in the latter case.
- Set
lock_timeout(PGHF04-010) so a future chain resolves itself instead of piling up indefinitely.
PGHF04-004 — Deadlock count¶
Every deadlock means PostgreSQL had to forcibly abort a transaction to break a cycle — a nonzero count points at inconsistent lock-acquisition order somewhere in the application, not a database-level problem that tuning alone fixes.
How to fix
This is an application fix, not a server-tuning one:
- Set
log_lock_waits = onand check the log for the deadlock detail message — it names the exact two queries and the objects each was waiting on. - Change the application so every transaction touching the same set of tables acquires locks on them in the same order.
- Where lock order genuinely can't be made consistent, consider
SELECT ... FOR UPDATEup front to acquire row locks explicitly, in a fixed order, rather than letting them be acquired incidentally mid-transaction.
PGHF04-005 — statement_timeout¶
With no statement_timeout, a single runaway query can hold its locks and resources indefinitely — there's nothing left to protect the server from a badly-written or hung query once it starts.
How to fix
A cluster-wide default is a safety net; if some workloads (batch jobs, reporting) legitimately need longer, override it per-role instead of raising the global default:
PGHF04-006 — idle_in_transaction_session_timeout¶
Without this timeout, a client that opens a transaction and never commits or rolls back — a forgotten connection, an app-level bug — can sit idle-in-transaction forever, with all the PGHF04-002/PGHF01-004 consequences that implies.
How to fix
PGHF04-007 — pg_stat_statements extension¶
Without pg_stat_statements, query-level performance analysis is severely limited — several other checks in this framework (PGHF04-008/PGHF04-011) and most real-world query-tuning workflows depend on it being installed and preloaded.
How to fix
It must be preloaded, not just CREATE EXTENSION'd — add it to shared_preload_libraries and restart:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
SELECT pghf.seed_data(); -- registers PGHF04-008/PGHF04-011, gated on this extension being present
PGHF04-008 — Top queries by total_exec_time¶
The queries consuming the most cumulative execution time are, by definition, where optimization effort pays off the most — this ranked list is where a DBA should start looking, rather than guessing at which query is worth tuning.
How to fix
For each top offender: run EXPLAIN (ANALYZE, BUFFERS) against it, check for a missing index (PGHF06-005 for FK columns specifically), a sequential scan that should be an index scan (see PGHF03-010's random_page_cost note), or stale statistics (PGHF03-014/ANALYZE). Since this is cumulative time (calls × mean time), also check whether it's one slow query or a fast query called an extreme number of times — the fix differs (query tuning vs. reducing call volume, e.g. batching or caching in the app).
PGHF04-009 — log_min_duration_statement¶
With slow-query logging disabled, identifying a performance regression after the fact means having no record of which queries were actually slow at the time — this is cheap, low-overhead insurance worth having on.
How to fix
Pick a threshold above normal query latency for this workload so the log doesn't fill with routine noise — start conservative (e.g. 1s) and lower it only if it stays quiet.
PGHF04-010 — lock_timeout¶
Without lock_timeout, a session waiting on a contended lock waits indefinitely — under real contention this turns into a growing pile-up of stuck connections rather than a bounded, recoverable wait.
How to fix
Keep it shorter than statement_timeout (PGHF04-005) — it should fire while a session is still just waiting for a lock, not after it's already been granted and started real work.
PGHF04-011 — Worst mean exec time (statistically-significant queries)¶
A high count of statistically-significant queries with a slow mean execution time is a direct signal of a performance regression — not a one-off slow run, but a query pattern that's consistently expensive every time it executes.
How to fix
Same investigation as PGHF04-008, but this ranking is already filtered to consistently-slow patterns rather than merely high-cumulative-time ones — EXPLAIN (ANALYZE, BUFFERS) each one and look for a plan that's bad on every run, not just occasionally, which usually means a missing/unused index or genuinely poor statistics rather than transient contention.
Continue to C05 — Vacuum & Autovacuum Health.