Skip to content

What pg_stat_statements tells you (and what it does not)

Last updated

View as Markdown

Quick answer: pg_stat_statements records one row per normalized query (per database, user, and top-level/nested context). the query text with constants replaced by placeholders, plus cumulative calls, 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.

It ships with PostgreSQL but is not active by default, and it needs shared memory, which means a restart:

  1. Add it to shared_preload_libraries in postgresql.conf:
shared_preload_libraries = 'pg_stat_statements'
  1. Restart PostgreSQL (a reload is not enough, the module loads at server start).
  2. 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.

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:

ColumnWhat it tells you
queryThe normalized SQL, literals and parameters replaced with $1, $2, …
queryidA hash identifying that normalized statement, stable within a server, but not guaranteed across major versions or machine architectures
callsHow many times it ran
total_exec_time / mean_exec_timeCumulative and average execution time (ms)
min_exec_time / max_exec_time / stddev_exec_timeSpread of execution time
rowsTotal rows returned or affected across all calls
shared_blks_hitBlocks served from the shared buffer cache
shared_blks_readShared-buffer misses (OS read(), page cache or disk; Postgres cannot tell)
wal_records / wal_bytesWrite-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.”

This is where people get misled:

  • Individual parameter values. Normalization is the whole point: WHERE id = 42 and WHERE id = 99 collapse into WHERE id = $1. You learn the shape costs a lot; you cannot learn that id = 42 specifically was slow.
  • Query plans. There is no EXPLAIN output here. A statement can be slow because the planner chose a sequential scan, and pg_stat_statements will not tell you that. Use the auto_explain module to log plans, or run EXPLAIN yourself.
  • A point-in-time snapshot. Every number is a cumulative total since the last reset (or server start). A big total_exec_time might be one bad week or three good months. Call pg_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.
  • Reset, wait a representative window, then read, so the totals map to a period you understand.
  • Rank by total_exec_time to find the biggest aggregate cost (see identifying slow queries).
  • When a shape looks slow, pull the plan separately with EXPLAIN (ANALYZE, BUFFERS).pg_stat_statements tells 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. Use auto_explain to log plans automatically, or run EXPLAIN on 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 use pg_stat_activity.