# What pg_stat_statements tells you (and what it does not)

> **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.

## 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:

1.  Add it to `shared_preload_libraries` in `postgresql.conf`:
   ```
   shared_preload_libraries = 'pg_stat_statements'
   ```
2. **Restart** PostgreSQL (a reload is not enough, the module loads at server
   start).
3.  Create the extension in each database you want to inspect:
   ```sql
   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

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

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.

## 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_time` to find the biggest aggregate cost (see
  [identifying slow queries](https://www.dbgorilla.com/learn/postgres/how-to-identify-slow-queries-in-postgresql/)).
- 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?

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.

## How DBGorilla helps

DBGorilla connects read-only and hands your AI coding agent (Claude Code, Cursor)
the real `pg_stat_statements` rows, the normalized shapes, call counts, timing,
and cache-hit numbers, so it can reason about production cost instead of the
source alone, and tell you which shape to pull a plan for next.  It surfaces and
explains; it never writes to your database. [Get started free →](https://app.dbgorilla.com/signup)

## FAQ

**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`.

## Related

- [How to identify slow queries in PostgreSQL](https://www.dbgorilla.com/learn/postgres/how-to-identify-slow-queries-in-postgresql/)
- [How to read a PostgreSQL EXPLAIN ANALYZE plan](https://www.dbgorilla.com/learn/postgres/how-to-read-a-postgres-explain-analyze-plan/)
- [Why did my PostgreSQL query suddenly get slow?](https://www.dbgorilla.com/learn/postgres/why-did-my-postgres-query-suddenly-get-slow/)
- Product: [DBGorilla docs](https://www.dbgorilla.com/docs/)