What pg_stat_statements tells you (and what it does not)
Quick answer:
pg_stat_statementsrecords one row per normalized query (per database, user, and top-level/nested context). the query text with constants replaced by placeholders, plus cumulativecalls, execution time,rows, and shared-buffer cache hits since the last reset. It does not record individual parameter values, query plans, or a point-in-time snapshot. It tells you which query shapes cost the most, not which specific execution was slow.
How do I turn it on?
Section titled “How do I turn it on?”It ships with PostgreSQL but is not active by default, and it needs shared memory, which means a restart:
- Add it to
shared_preload_librariesinpostgresql.conf:
shared_preload_libraries = 'pg_stat_statements'- Restart PostgreSQL (a reload is not enough, the module loads at server start).
- Create the extension in each database you want to inspect:
CREATE EXTENSION pg_stat_statements;On managed platforms (RDS, Cloud SQL, Azure) it is often preloaded already. You may only need step 3.
What it captures
Section titled “What it captures”Each row aggregates every execution of one normalized statement by a given user in a given database, top-level and nested (in-function) calls are counted separately:
| Column | What it tells you |
|---|---|
query | The normalized SQL, literals and parameters replaced with $1, $2, … |
queryid | A hash identifying that normalized statement, stable within a server, but not guaranteed across major versions or machine architectures |
calls | How many times it ran |
total_exec_time / mean_exec_time | Cumulative and average execution time (ms) |
min_exec_time / max_exec_time / stddev_exec_time | Spread of execution time |
rows | Total rows returned or affected across all calls |
shared_blks_hit | Blocks served from the shared buffer cache |
shared_blks_read | Shared-buffer misses (OS read(), page cache or disk; Postgres cannot tell) |
wal_records / wal_bytes | Write-ahead-log volume generated (PostgreSQL 13+) |
(On PostgreSQL 12 and earlier the timing columns are total_time / mean_time;
13 renamed them to *_exec_time and added planning-time columns.)
The cache hit ratio for a statement is
shared_blks_hit / (shared_blks_hit + shared_blks_read), near 1.0 means the
data was already in shared buffers. A low ratio means lots of buffer misses;
treat that as “missed Postgres buffers,” not “hit the disk.”
What it does not capture
Section titled “What it does not capture”This is where people get misled:
- Individual parameter values. Normalization is the whole point:
WHERE id = 42andWHERE id = 99collapse intoWHERE id = $1. You learn the shape costs a lot; you cannot learn thatid = 42specifically was slow. - Query plans. There is no
EXPLAINoutput here. A statement can be slow because the planner chose a sequential scan, andpg_stat_statementswill not tell you that. Use theauto_explainmodule to log plans, or runEXPLAINyourself. - A point-in-time snapshot. Every number is a cumulative total since the
last reset (or server start). A big
total_exec_timemight be one bad week or three good months. Callpg_stat_statements_reset()and measure a known window if you need “right now.” - Currently-running queries. For live activity, what is executing this
second, what is blocked, use
pg_stat_activity, not this view.
How do I use it well?
Section titled “How do I use it well?”- Reset, wait a representative window, then read, so the totals map to a period you understand.
- Rank by
total_exec_timeto find the biggest aggregate cost (see identifying slow queries). - When a shape looks slow, pull the plan separately with
EXPLAIN (ANALYZE, BUFFERS).pg_stat_statementstells you which query to investigate; the plan tells you why.
Why does this matter for aI-generated code?
Section titled “Why does this matter for aI-generated code?”An AI coding agent reasons over your source, where every query is a concrete
string with real values. pg_stat_statements is the opposite view: aggregated
shapes, no values, cumulative over real traffic. The two never meet. The agent
cannot know that one normalized shape it wrote now accounts for 40% of database
time, that fact only exists in the aggregate, after the code has run against
production data at production scale.
Does pg_stat_statements show query plans?
No, it records execution statistics, not plans. Useauto_explainto log plans automatically, or runEXPLAINon the query yourself.Can pg_stat_statements show which parameter value was slow?
No. It normalizes queries (WHERE id = $1), so all calls of the same shape aggregate into one row. You get the shape and its totals, not the specific value.How do I compute the cache hit ratio?
Per statement,shared_blks_hit / (shared_blks_hit + shared_blks_read). Near 1.0 means served from shared buffers; low means buffer misses, not proof of disk I/O.Is pg_stat_statements a live view of running queries?
No. It is cumulative history since the last reset. For live queries usepg_stat_activity.