When a database is slow, the question is which queries are consuming it. Guessing from application code, reading slow query logs, or asking developers what they think is expensive all take longer and produce worse answers than looking at cumulative statistics.
The mechanism is simple: track every query executed, normalised so that the same shape with different parameters groups together, and accumulate calls, total time, rows, and cache behaviour per fingerprint.
The canonical implementation is pg_stat_statements, the PostgreSQL extension that exposes exactly this as a view you can query with SQL. MySQL provides the equivalent through performance_schema digest tables, and managed platforms wrap both in query-insight dashboards. The interface differs; the columns and the reasoning below are the same everywhere.
What changed in 2026
- It became enabled by default on managed platforms. Query insights shipped as a standard feature rather than something to configure.
- Retention improved. More platforms stored historical snapshots, enabling trend analysis rather than point-in-time views.
- Plan capture appeared alongside. Some implementations began associating plans with statistics entries.
- The overhead question settled. Measured impact proved small enough that leaving it enabled became the default recommendation.
What the numbers mean
| Column |
Question it answers |
| Total time |
What consumes the database |
| Calls |
How often it runs |
| Mean time |
Whether an individual execution is slow |
| Max time |
Whether it is sometimes much worse |
| Rows per call |
Whether it returns more than expected |
| Cache hit ratio |
Whether it reads from disk |
Total time is the primary sort. It is calls multiplied by mean time, and it directly answers what is consuming your database. A query with a small mean and an enormous call count outranks a slow rare one, which is the case threshold-based logging misses entirely — see slow query logs.
Mean against max is the variability signal. A low mean with a very high maximum means the query is usually fine and occasionally terrible, which typically indicates lock waiting or a parameter matching far more rows than usual.
Rows per call explains a surprising number of slow queries immediately. A query averaging fifty thousand rows returned is slow because it is doing a lot, and the fix is in the application rather than the database.
Cache hit ratio distinguishes a query that is slow because it reads from disk from one that is slow because it does too much work in memory.
Reset to measure
The counters are cumulative since the last reset, which makes them poor at showing whether a change helped.
If a query has accumulated hours of total time over three weeks, deploying a fix and checking the next day shows almost no difference — the historical total dominates.
Reset the statistics after deploying the change, then compare over a period equivalent to your baseline. That gives a clean before-and-after.
The same applies to establishing a baseline: reset, let the system run through a representative period including peak, then snapshot. Comparing that snapshot against later ones shows what changed.
Where the platform stores historical snapshots, this is handled for you and the reset discipline is unnecessary.
Reading it productively
Sort by total time and look at the top ten. That is usually where the opportunity is, and it is frequently not where anyone expected.
Common findings:
A trivial query with an enormous call count. Usually a query inside a loop, or a missing cache — see GraphQL N+1 and ORM pitfalls.
A query returning far more rows than the application uses. Missing pagination or an over-broad select.
A query with a high maximum and low mean. Lock contention, or a parameter distribution issue producing a bad plan for some values — see query planners.
A maintenance or monitoring query in the top ten. Your own tooling consuming meaningful capacity, which is worth knowing.
Common mistakes
- Sorting by mean time. Finds slow queries, not expensive ones.
- Never resetting. Cannot measure the effect of changes.
- Enabling it and never looking. The collection is not the value.
- Ignoring rows per call. Explains many slow queries at a glance.
- Optimising the top query without checking the plan. Statistics say what; plans say why.
- Forgetting your own monitoring queries. They appear in the statistics too.
- Assuming normalised entries are one query. One fingerprint may come from several code paths.
FAQ
What is the overhead?
Small — measured impact is low enough that leaving it on permanently is the standard recommendation. The cost of not having it during an incident is far higher.
How many queries does it track?
A configurable maximum, with least-used entries evicted when full. On a system with very many distinct query shapes, raising the limit prevents useful entries being lost.
Does it capture parameters?
No, by design — that is what enables normalisation. To reproduce a specific slow execution you need the slow query log alongside.
Can I see plans?
Not from the statistics themselves in most implementations. Take the normalised query, supply representative parameters, and examine the plan separately.
Where to go next
For catching individual slow executions, read slow query logs. For diagnosing why a query is slow, query planners, and for the application patterns that produce high call counts, ORM pitfalls.