The statements collector tracks query performance metrics from PostgreSQL's pg_stat_statements extension. It's one of the most powerful tools for identifying and optimizing slow queries in production.
pg_exporter supports PostgreSQL 14 and newer, so all metrics documented here assume PostgreSQL 14+.
- Find slow queries during incidents - "What query is causing high load?"
- Detect N+1 query problems - Before they scale and impact production
- Identify performance regressions - After deployments or configuration changes
- Optimize based on real data - Use actual production query patterns, not guesses
- Track resource-intensive queries - I/O, WAL generation, temp files
This collector complements other collectors:
default- System-wide metrics (cache hit ratio, checkpoints, connections)stat.user_tables- Table-level metrics (bloat, vacuum, DML rates)statements- Query-level metrics (execution time, frequency, I/O)
Together, they provide complete visibility from system → table → query level.
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;Add to postgresql.conf:
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all
pg_stat_statements.max = 10000Restart PostgreSQL after modifying postgresql.conf.
Enable the collector:
pg_exporter --dsn postgresql:///postgres?user=postgres_exporter --collector.statementsBy default, it tracks the top 25 queries by total execution time.
Configure the number of queries to track:
# Track top 10 queries
pg_exporter --dsn postgresql://... --collector.statements --statements.top-n=10
# Environment variable form
PG_EXPORTER_STATEMENTS_TOP_N=50 pg_exporter --dsn postgresql://... --collector.statementspg_stat_statements_total_exec_time_seconds- Total time spent in this querypg_stat_statements_mean_exec_time_seconds- Average time per executionpg_stat_statements_max_exec_time_seconds- Slowest executionpg_stat_statements_stddev_exec_time_seconds- Execution time variance
pg_stat_statements_calls_total- How many times the query was executedpg_stat_statements_rows_total- Total rows returned/affected
pg_stat_statements_shared_blks_hit_total- Cache hits (fast)pg_stat_statements_shared_blks_read_total- Disk reads (slow)pg_stat_statements_temp_blks_written_total- Queries spilling to diskpg_stat_statements_cache_hit_ratio- Query cache effectiveness (0.0-1.0)
pg_stat_statements_wal_bytes_total- WAL generation
PromQL query to find queries with highest total time:
topk(10,
pg_stat_statements_total_exec_time_seconds
)
Queries executed many times with low row counts:
pg_stat_statements_calls_total > 1000
and
pg_stat_statements_rows_total / pg_stat_statements_calls_total < 10
Queries with poor cache hit ratios:
pg_stat_statements_cache_hit_ratio < 0.9
and
pg_stat_statements_calls_total > 100
Queries writing to disk (needs more work_mem):
rate(pg_stat_statements_temp_blks_written_total[5m]) > 0
Alert on queries getting slower:
increase(pg_stat_statements_mean_exec_time_seconds[1h]) > 0.5
All metrics include these labels:
queryid- Unique query identifierdatname- Database nameusename- User/role namequery_short- First 80 characters of the query (or<utility>for VACUUM/ANALYZE)
query_short is intentionally capped at 80 characters to keep Prometheus label
cardinality and label size under control. It is meant for fast identification in
Prometheus and Grafana, not as a full SQL text export.
When you need the full normalized statement text, use the queryid label from
the metric and query pg_stat_statements directly.
Example:
SELECT
queryid::text,
d.datname,
r.rolname,
s.query
FROM pg_stat_statements s
JOIN pg_database d ON d.oid = s.dbid
LEFT JOIN pg_roles r ON r.oid = s.userid
WHERE queryid::text = '<queryid-from-metric>';If you want to narrow the search further, also filter by datname:
SELECT
queryid::text,
d.datname,
r.rolname,
s.query
FROM pg_stat_statements s
JOIN pg_database d ON d.oid = s.dbid
LEFT JOIN pg_roles r ON r.oid = s.userid
WHERE queryid::text = '<queryid-from-metric>'
AND d.datname = '<database-from-metric>';PostgreSQL normalizes queries by replacing constants with placeholders:
-- These are the same query:
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE id = 2;
-- Tracked as:
SELECT * FROM users WHERE id = $1;Utility statements (VACUUM, ANALYZE, CREATE INDEX, etc.) may appear as <utility> in the query_short label since PostgreSQL doesn't always track their full text.
The collector tracks the top N queries by total execution time. This means:
- Long-running infrequent queries appear at the top
- Fast but frequent queries also appear if their total time is high
- Adjust
--statements.top-nbased on your query diversity and scrape budget
pg_stat_statements has minimal overhead (typically <1% CPU). However:
- Higher
pg_stat_statements.maxvalues use more memory - The collector queries
pg_stat_statementson each scrape - For high-traffic databases, consider longer scrape intervals
ERROR: extension "pg_stat_statements" is not available
Solution: Install the extension package:
# Debian/Ubuntu
apt-get install postgresql-contrib
# RHEL/CentOS
yum install postgresql-contribPossible causes:
- Extension not loaded - Check
SHOW shared_preload_libraries; - Extension not created - Run
CREATE EXTENSION pg_stat_statements; - No queries executed yet - Run some queries to populate stats
- Collector not enabled - Use
--collector.statements
This is normal for:
- Utility statements (VACUUM, ANALYZE, etc.)
- Queries from other monitoring tools
- Internal PostgreSQL operations
- Enable in production - Query-level insights are essential for troubleshooting
- Monitor the top 50-100 queries - Balance coverage vs cardinality
- Reset stats after major changes -
SELECT pg_stat_statements_reset();after schema migrations - Set appropriate scrape intervals - 30-60 seconds is usually sufficient
- Combine with other collectors - Use
activity,stat, andvacuumcollectors together for complete visibility
Track your slowest queries:
# Panel 1: Top 10 Slowest Queries (by total time)
topk(10, pg_stat_statements_total_exec_time_seconds)
# Panel 2: Most Called Queries
topk(10, rate(pg_stat_statements_calls_total[5m]))
# Panel 3: Cache Hit Ratio Heatmap
pg_stat_statements_cache_hit_ratio
# Panel 4: Queries Writing Temp Files
sum by (query_short) (
rate(pg_stat_statements_temp_blks_written_total[5m])
)