Enable and operate pg_stat_statements safely, understand normalized query fingerprints/query IDs, rank workload by time/I/O/WAL, account for resets/deallocations, and connect fingerprints to representative current plans.

pg_stat_statements, Query Fingerprints, Plan Variance, and Workload Ranking

Enable and operate pg_stat_statements safely, understand normalized query fingerprints/query IDs, rank workload by time/I/O/WAL, account for resets/deallocations, and connect fingerprints to representative current plans.

Intermediate → Advanced180–240 minutesPostgreSQL observability and incident diagnosisCurrent patched PostgreSQL 18.x; verify current minor at lab timeCore PostgreSQL; pg_stat_statements + auto_explain are PostgreSQL-supplied modulesServiceHub disposable objects: app.ch21_*Observer/admin role recommended; some statistics/signaling require pg_read_all_stats or pg_signal_backend/superuserFree local tooling; OS iostat/vmstat/top equivalents are optional corroborating evidenceLast reviewed: August 2026

Learning outcomes

Table counters tell us where work accumulated, but ServiceHub needs to know which statement shapes consumed database time, I/O, temporary blocks, and Write-Ahead Log (WAL). The PostgreSQL-supplied pg_stat_statements module aggregates normalized statement fingerprints across executions.

01

Enable pg_stat_statements with its shared_preload_libraries restart requirement and query-ID requirement.

02

Explain queryid normalization, representative query text, user/database/toplevel dimensions, and major-version stability limits.

03

Rank workload by total versus mean execution time, calls, I/O, temp blocks, and WAL instead of one universal score.

04

Use pg_stat_statements_info and stats_since/minmax_stats_since to interpret resets and entry deallocation.

05

Connect a fingerprint to a fresh representative EXPLAIN without pretending pg_stat_statements stores historical plans.

1. Preload is a server-start decision

sql · preflight
SELECT name, setting, context, source, pending_restartFROM pg_settingsWHERE name IN (  'shared_preload_libraries',  'compute_query_id')ORDER BY name;SELECT name, default_version, installed_versionFROM pg_available_extensionsWHERE name = 'pg_stat_statements';

pg_stat_statements allocates shared memory and hooks statement planning/execution across the server, so adding/removing it requires shared_preload_libraries and a server restart. CREATE EXTENSION alone only creates the SQL-facing views/functions in one database; it cannot retrofit the server preload.

2. Configure without clobbering existing preload libraries

conf · postgresql.conf example — preserve existing entries
# Merge with existing required libraries; do not overwrite blindly.shared_preload_libraries = 'pg_stat_statements'compute_query_id = auto# Start conservatively:pg_stat_statements.track = top# Planning timing is OFF by default and adds overhead:pg_stat_statements.track_planning = off

If other preload modules already exist, keep them in the comma-separated list. Restart PostgreSQL, then verify pending_restart=false. compute_query_id=auto lets modules such as pg_stat_statements enable query IDs.

sql · database registration after restart
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;SELECT extname, extversionFROM pg_extensionWHERE extname = 'pg_stat_statements';SELECT *FROM pg_stat_statements_info;

3. Generate one normalized query shape with different constants

Use the Chapter 21 workload table. Different literal tenant/status values should aggregate into a common parsed query shape when their analyzed structure is identical.

psql · execute several literal variants
SELECT format(  'SELECT count(*) FROM app.ch21_workload WHERE tenant_id = %s AND status = %L;',  (g % 10) + 1,  CASE WHEN g % 2 = 0 THEN 'open' ELSE 'queued' END)FROM generate_series(1,40) AS g\gexec
sql · find the normalized entry
SELECT r.rolname AS executed_by,       d.datname AS database_name,       s.toplevel,       s.queryid,       s.calls,       s.rows,       round(s.total_exec_time::numeric,2) AS total_exec_ms,       round(s.mean_exec_time::numeric,3) AS mean_exec_ms,       round(s.stddev_exec_time::numeric,3) AS stddev_exec_ms,       s.stats_since,       s.queryFROM pg_stat_statements AS sJOIN pg_database AS d ON d.oid = s.dbidLEFT JOIN pg_roles AS r ON r.oid = s.useridWHERE s.query LIKE 'SELECT count(*) FROM app.ch21_workload%'ORDER BY s.total_exec_time DESC;

Representative text replaces ignored constants with $1, $2, etc. The entry key also includes database/user/top-level dimensions. A query ID is a hash of the analyzed query structure, not a cryptographic identity and not guaranteed stable across PostgreSQL major versions.

4. Rank by the question you are asking

sql · top contributors by total execution time
SELECT queryid,       calls,       round(total_exec_time::numeric,1) AS total_exec_ms,       round(mean_exec_time::numeric,3) AS mean_exec_ms,       rows,       shared_blks_hit,       shared_blks_read,       temp_blks_written,       pg_size_pretty(wal_bytes::bigint) AS wal_generated,       left(query,120) AS representative_queryFROM pg_stat_statementsWHERE dbid = (SELECT oid FROM pg_database WHERE datname=current_database())ORDER BY total_exec_time DESCLIMIT 10;

Total time finds workload-wide consumers. Mean/max time finds per-call latency problems. Calls finds chatty SQL. shared_blks_read/temp_blks_written suggests I/O or spill pressure. wal_bytes identifies write-heavy statement shapes. No single ordering answers every operational question.

sql · alternative rankings
SELECT queryid, calls,       round(mean_exec_time::numeric,3) AS mean_ms,       round(max_exec_time::numeric,3) AS max_ms,       temp_blks_read, temp_blks_written,       wal_records, wal_fpi, wal_bytesFROM pg_stat_statementsWHERE calls >= 5ORDER BY mean_exec_time DESCLIMIT 10;

5. Planning metrics are optional and have their own cost

sql · planning collection state
SHOW pg_stat_statements.track_planning;SELECT queryid, plans, calls,       total_plan_time, mean_plan_time,       total_exec_time, mean_exec_timeFROM pg_stat_statementsWHERE query LIKE '%app.ch21_workload%'ORDER BY total_exec_time DESC;

If track_planning=off, plan counters/times are zero. Enabling planning tracking can add noticeable contention/overhead on highly concurrent repeated statement shapes. Enable it deliberately when plan-time diagnosis is worth that cost.

6. “Plan variance” must be measured, not inferred from timing spread alone

min_exec_time, max_exec_time, and standard deviation show execution-time variability, but they do not prove the optimizer chose multiple plan shapes. Cache state, lock waits, parameter selectivity, parallel-worker availability, and I/O can change execution time without changing the plan.

Wrong approach

Calling max_exec_time/min_exec_time a 'plan variance ratio' confuses latency variance with plan-shape variance. pg_stat_statements does not retain each historical execution plan. Capture representative EXPLAIN/auto_explain evidence if plan changes matter.

7. Connect a fingerprint to a representative current plan

sql · fresh representative plan
EXPLAIN (ANALYZE, BUFFERS, WAL, SETTINGS, VERBOSE)SELECT count(*)FROM app.ch21_workloadWHERE tenant_id = 7  AND status = 'open';

With query IDs enabled, EXPLAIN can display a Query Identifier. Compare it with the fingerprint entry, then inspect the current plan. This plan is evidence for this representative execution now; it is not the historical plan that produced every cumulative sample.

8. Resets and deallocation change the baseline

sql · module baseline health
SELECT dealloc, stats_resetFROM pg_stat_statements_info;SELECT min(stats_since) AS oldest_entry_stats_since,       max(stats_since) AS newest_entry_stats_since,       count(*) AS tracked_entriesFROM pg_stat_statements;

If distinct statement shapes exceed pg_stat_statements.max, least-used entries are deallocated; dealloc shows how often this occurred. High deallocation means workload ranking can lose low-frequency fingerprints and representative query text behavior may become less stable. Reset events also break continuity with old dashboard rates.

9. Do not reset global history casually

sql · targeted reset pattern — admin only
-- Identify one exact entry first:SELECT userid, dbid, queryid, queryFROM pg_stat_statementsWHERE query LIKE 'SELECT count(*) FROM app.ch21_workload%'ORDER BY calls DESCLIMIT 1;-- Then, only if the lab/change procedure intentionally needs it:-- SELECT pg_stat_statements_reset(userid, dbid, queryid, false);

A global reset discards the evidence every database on the server has accumulated through the module. Production dashboards normally keep the counters and compute deltas externally.

10. Query text and security boundaries

Only superusers and roles with pg_read_all_stats can see other users' query text/query IDs in full. Normalization reduces literal variability but is not a secret-sanitization guarantee. Never put passwords/tokens into SQL text; utility commands and application-generated strings can still create sensitive observability data.

Production judgment

Keep pg_stat_statements always available when its overhead is acceptable, but interpret it as cumulative fingerprint economics—not traces. Pair it with live waits, plans, logs, and application request metrics; preserve reset/deallocation context and protect query text like operationally sensitive data.

Check your understanding

  1. Why does CREATE EXTENSION pg_stat_statements not remove the restart requirement?
  2. What dimensions besides queryid distinguish rows in pg_stat_statements?
  3. Why can total_exec_time and mean_exec_time produce very different priority lists?
  4. Why does execution-time standard deviation not prove multiple plan shapes?
  5. What does pg_stat_statements_info.dealloc tell you?
Review the answers

The server hook/shared memory must be preloaded at startup; CREATE EXTENSION only exposes SQL objects in a database. Entries include user, database, query ID and top-level status. Total time prioritizes aggregate load; mean time prioritizes per-call latency. Timing variance has many causes and plans are not stored historically. dealloc counts least-used entry evictions after exceeding pg_stat_statements.max.

Authoritative references

Statistics and logging fields evolve across PostgreSQL majors. These PostgreSQL 18 primary sources define the mechanisms used in this lesson.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.