Cheatsheet for finding slow queries.
pg_stat_statements (top-N by time)
Enable:
-- postgresql.conf
shared_preload_libraries = 'pg_stat_statements'
CREATE EXTENSION pg_stat_statements;
Query:
SELECT
substring(query, 1, 100) AS query,
calls,
total_exec_time / 1000 AS total_sec,
mean_exec_time AS mean_ms,
rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;
The 80/20 of total DB time. Targets for optimization.
Reset
SELECT pg_stat_statements_reset();
Reset after deploying a fix; re-measure.
auto_explain
-- postgresql.conf
shared_preload_libraries = 'auto_explain'
auto_explain.log_min_duration = '500ms'
auto_explain.log_analyze = on
auto_explain.log_buffers = on
auto_explain.log_format = 'json'
Queries > 500ms get full EXPLAIN ANALYZE in the log. Investigate.
log_min_duration_statement
ALTER SYSTEM SET log_min_duration_statement = '500ms';
SELECT pg_reload_conf();
Logs all queries > 500ms with their SQL.
EXPLAIN
explain = await session.execute(
text("EXPLAIN (ANALYZE, BUFFERS) " + str(stmt.compile(compile_kwargs={"literal_binds": True})))
)
for row in explain:
print(row[0])
ANALYZE runs the query; BUFFERS shows IO.
Slow query log via SQLAlchemy events
import time
from sqlalchemy import event
@event.listens_for(engine.sync_engine, "before_cursor_execute")
def before(conn, cur, stmt, params, ctx, executemany):
ctx._t = time.time()
@event.listens_for(engine.sync_engine, "after_cursor_execute")
def after(conn, cur, stmt, params, ctx, executemany):
dur = time.time() - ctx._t
if dur > 0.5:
log.warning("slow_query", sql=stmt[:500], ms=dur*1000)
App-side; complements DB-side logging.
Find long-running queries (live)
SELECT
pid, usename, application_name,
state, age(now(), query_start) AS duration,
substring(query, 1, 80) AS query
FROM pg_stat_activity
WHERE state = 'active'
AND query_start < now() - interval '5 minutes'
ORDER BY query_start;
Find queries waiting on locks
SELECT
blocked.pid AS blocked_pid,
substring(blocked.query, 1, 60) AS blocked_query,
blocking.pid AS blocking_pid,
substring(blocking.query, 1, 60) AS blocking_query,
age(now(), blocked.query_start) AS blocked_duration
FROM pg_stat_activity blocked
JOIN pg_locks bl ON bl.pid = blocked.pid AND NOT bl.granted
JOIN pg_locks bg ON bg.relation = bl.relation AND bg.granted AND bg.pid != bl.pid
JOIN pg_stat_activity blocking ON blocking.pid = bg.pid
WHERE blocked.state = 'active';
Who’s holding the lock that’s blocking who.
Index usage on a query
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM users WHERE email = '[email protected]';
-- Look for: "Index Scan using ix_users_email" (good)
-- Vs: "Seq Scan" (probably missing index)
Why isn’t my index used?
Common causes:
- Function on column:
WHERE lower(email) = ...(need expression index). - Type cast mismatch:
WHERE id::text = '1'. - LIKE without anchor:
'%foo%'(need trigram). - Small table: planner chooses seq scan.
- Stats stale: run
ANALYZE. - Statistics distribution: query value is too common; index unhelpful.
pg_buffercache (what’s in cache)
CREATE EXTENSION pg_buffercache;
SELECT c.relname, count(*) AS buffers
FROM pg_buffercache b JOIN pg_class c ON b.relfilenode = pg_relation_filenode(c.oid)
GROUP BY c.relname
ORDER BY buffers DESC LIMIT 10;
Hot tables in shared_buffers.
Connection-level explain
async with session.begin():
await session.execute(text("SET LOCAL log_min_duration_statement = '0'"))
# All queries in this transaction logged
await session.execute(stmt)
EXPLAIN settings
EXPLAIN (
ANALYZE, -- actually run
BUFFERS, -- show IO
FORMAT JSON, -- machine readable
VERBOSE, -- column-level info
COSTS, TIMING, SUMMARY
) SELECT ...
When to optimize
- Top 10 in pg_stat_statements by
total_exec_time. - Anything > 200ms in p99.
- Queries with > 100k rows scanned but few rows returned.
- Anything showing
Seq Scanon a large table.
Common mistakes
- pg_stat_statements not enabled — blind.
- Running
ANALYZErarely — stale stats. - Optimizing slow queries one-off without measuring relative impact.
- Adding indexes without removing unused — write amplification.
Read this next
If you want my pg_stat_statements + Prometheus exporter, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .