PostgreSQL in Practice #3 The Performance Diagnostics Routine: Finding Slow Queries with pg_stat_statements
Basics chapter 5’s EXPLAIN answers “why is this query slow.” But in production the question arrives one step earlier: “the database is slow — which query is the problem?” General bottleneck hunting when a whole server is slow was covered in the why servers get slow series; this chapter is the routine for drilling into the database layer with PostgreSQL’s own tools.
Right now: pg_stat_activity #
“What is happening in the database at this moment” is answered by the pg_stat_activity view. We already used it to count connection states last chapter.
SELECT pid, state, wait_event_type, wait_event,
now() - query_start AS running_for,
left(query, 60) AS query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY running_for DESC;Three things to read. Long-running queries (rows with large running_for), waiting queries (wait_event_type of Lock means a lock wait — practice #6’s territory), and the easy-to-miss idle in transaction: a session holding a transaction open while doing nothing. It may be sitting on locks, and it blocks VACUUM’s cleanup (practice #5 returns to this). The archetypal culprit is application code that opens a transaction and then waits on an external API.
Cumulative: pg_stat_statements #
Snapshots of the moment can’t catch “slow sometimes” problems. You need cumulative statistics, and the standard is the pg_stat_statements extension. It folds queries of the same shape (differing only in parameters) into one entry and keeps aggregating call counts and times.
-- postgresql.conf: shared_preload_libraries = 'pg_stat_statements', then restart
CREATE EXTENSION pg_stat_statements;
-- top 10 by total time
SELECT calls,
round(total_exec_time::numeric / 1000, 1) AS total_sec,
round(mean_exec_time::numeric, 2) AS mean_ms,
rows,
left(query, 60) AS query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;It needs a restart, so turn it on ahead of time — enable it after the incident and that day’s data doesn’t exist. On managed services it can usually be enabled through the parameter group.
Top-by-total and top-by-mean are different culprits #
Different orderings expose different problems.
- Top by total_exec_time: the queries consuming the most database resources. A 2ms-average query called hundreds of times per second can rank first. Start here to lower overall load. The fix is often not query tuning but “calling it less” (caching, fixing N+1 patterns).
- Top by mean_exec_time: queries slow per execution. What users feel as “this screen is slow” is usually these. Queries surfaced here are what you carry to EXPLAIN ANALYZE — the link back to the basics series.
rows / calls (rows per call) is worth a scan too. A query returning tens of thousands of rows per call may not be a “database is slow” problem but a “fetching too much” problem.
Logs: log_min_duration_statement #
The third tool is the slow query log.
log_min_duration_statement = 500 # log queries taking over 500msWhere pg_stat_statements aggregates by shape, the log records individual executions with their actual parameters. Data-skew problems — “slow only for this one user” — are caught here. Setting it to 0 logs everything and costs real overhead, so a threshold around 500ms〜1s is typical.
Assembling the routine #
Diagnosis now becomes a sequence. ① During an incident, read the present with pg_stat_activity (long runners, lock waits, idle in transaction). ② For steady-state improvement, pull pg_stat_statements by both orderings, total and mean. ③ Take the culprit query into EXPLAIN ANALYZE and run chapter 4’s index checklist. ④ After the fix, confirm the pg_stat_statements numbers actually dropped (resetting with SELECT pg_stat_statements_reset(); makes the comparison clean). The next step — when an index doesn’t solve it — is the advanced indexing strategy of the next chapter.
Summary #
- The diagnostic order is “which query” (this chapter) → “why slow” (EXPLAIN). Use the tools in that order.
- The present is pg_stat_activity: long-running queries, Lock waits, and idle in transaction are the three reads.
- The cumulative standard is pg_stat_statements. It needs a restart — enable it before the incident.
- Top-by-total is the load culprit; top-by-mean is the user-experience culprit. Pull both.
- The log (log_min_duration_statement) keeps actual parameters and catches data-skew problems. Next chapter: advanced index strategy.