pg_stat_monitor vs pg_stat_statements: Which Query Monitor Should You Use?
Two illustrators hired to monitor the same subject. One is reliable. The other captures richer detail.
Two extensions, one job
Both pg_stat_statements and pg_stat_monitor answer the same fundamental question: which queries are consuming your database's resources? They track query execution statistics — call counts, execution times, row counts, I/O metrics — and surface the data through queryable views. Both are well-engineered tools. The difference lies in how much context they capture and where they are available.
pg_stat_statements has been part of PostgreSQL's contrib package since version 8.4. It is universally available, lightweight, and battle-tested across millions of installations over more than 15 years. It is the industry standard for PostgreSQL query monitoring — every major monitoring vendor reads from it.
pg_stat_monitor is Percona's alternative, first released in 2020. It provides a richer feature set — time-based bucketing, query plan capture, histogram data, application-level attribution — but with narrower availability on managed services and a smaller ecosystem.
The two extensions cannot run simultaneously in most configurations. They share the same query instrumentation infrastructure, and pg_stat_monitor is designed as a drop-in replacement, not a complement. Enabling both wastes shared memory and can produce conflicts.
This comparison evaluates both extensions on their merits, with honesty about the tradeoffs involved. You deserve a fair accounting before choosing.
What pg_stat_statements provides
pg_stat_statements tracks execution statistics for every normalized query. Query text is normalized — parameters are replaced with $1, $2, etc. — so SELECT * FROM orders WHERE id = 42 and SELECT * FROM orders WHERE id = 99 appear as a single entry: SELECT * FROM orders WHERE id = $1.
For each normalized query, pg_stat_statements records:
| Metric | Description |
|---|---|
calls | Number of times the query has been executed |
total_exec_time | Cumulative execution time (ms) |
min_exec_time / max_exec_time | Fastest and slowest individual executions |
mean_exec_time | Average execution time |
rows | Total rows returned across all executions |
shared_blks_hit | Shared buffer cache hits |
shared_blks_read | Blocks read from disk (or OS cache) |
shared_blks_dirtied / shared_blks_written | Blocks modified and written |
temp_blks_read / temp_blks_written | Temporary blocks (spill to disk) |
PostgreSQL 14 added separate planning time tracking (total_plan_time, min_plan_time, etc.), WAL statistics, and JIT compilation metrics. PostgreSQL 16 further refined execution time tracking with additional granularity.
Statistics are cumulative — they accumulate from the last call to pg_stat_statements_reset(). The extension maintains a fixed-size hash table controlled by pg_stat_statements.max (default 5,000 entries). When the table fills, the least-executed queries are evicted.
Strengths
Universal availability. pg_stat_statements is included in PostgreSQL's contrib package and is available on every managed provider: Amazon RDS, Google Cloud SQL, Azure Flexible Server, Supabase, Neon, and others. There is no managed PostgreSQL service where it is unavailable.
Minimal overhead. Widely benchmarked at less than 1-2% overhead across diverse workloads. The extension adds a lightweight hook to query execution that records statistics in shared memory — no disk writes, no logging, no external communication.
Battle-tested. 15+ years in production across millions of PostgreSQL installations. Edge cases have been found and fixed. Behavior under extreme load is well-understood.
Ecosystem integration. Every major PostgreSQL monitoring vendor — pganalyze, Datadog, New Relic, Grafana Cloud, OpsRamp — reads from pg_stat_statements. It is the lingua franca of PostgreSQL query monitoring.
Simple mental model. One row per unique normalized query. Cumulative counters. Reset when you want a fresh baseline. There is something to be said for an extension you can explain to a colleague in two sentences.
Limitations
No time-based bucketing. Statistics accumulate from the last reset. Answering "what was the top query between 2pm and 3pm?" requires external snapshotting — a separate system that periodically captures the pg_stat_statements view and computes deltas between snapshots.
No query plan capture. pg_stat_statements tells you which queries are slow but not why. The execution plan — which reveals whether a query is doing sequential scans, missing indexes, or spilling to disk — is not captured. Plan capture requires a separate extension like auto_explain.
No histogram data. The extension records min, max, and mean execution times, but not the distribution. A query that executes in 1ms 99% of the time but takes 5 seconds once an hour appears as a 50ms average — the outlier behavior is entirely invisible without percentile data. Averages, I'm afraid, can be misleading company.
No application-level attribution. pg_stat_statements does not track application_name or client_ip. When multiple microservices connect to the same database, there is no built-in way to determine which service is responsible for a particular query's load.
What pg_stat_monitor provides
pg_stat_monitor provides everything pg_stat_statements provides, plus several capabilities that address its limitations:
Time-based bucketing. Statistics are grouped into configurable time buckets (default 60 seconds). Each bucket contains complete query statistics for that time period. This enables time-windowed analysis without external tooling.
-- Top queries in the last 5 minutes
SELECT
bucket_start_time,
query,
calls,
total_exec_time,
mean_exec_time
FROM pg_stat_monitor
WHERE bucket_start_time > now() - INTERVAL '5 minutes'
ORDER BY total_exec_time DESC
LIMIT 10; Query plan capture. pg_stat_monitor can store the actual execution plan for tracked queries. This eliminates the need for auto_explain in many use cases.
-- View the plan for the most expensive query
SELECT
query,
query_plan,
calls,
mean_exec_time
FROM pg_stat_monitor
WHERE query_plan IS NOT NULL
ORDER BY total_exec_time DESC
LIMIT 1; Histogram data. Response time distributions reveal percentile behavior that averages hide. Instead of just min/max/mean, pg_stat_monitor provides bucketed response time counts showing how many executions fell into each latency range.
application_name tracking. Filter statistics by application or service name, answering "which microservice is generating the most load?"
-- Query load by application
SELECT
application_name,
SUM(calls) AS total_calls,
SUM(total_exec_time) AS total_time,
COUNT(DISTINCT queryid) AS unique_queries
FROM pg_stat_monitor
WHERE bucket_start_time > now() - INTERVAL '1 hour'
GROUP BY application_name
ORDER BY total_time DESC; client_ip tracking. Identify which client hosts are generating load — useful for diagnosing connection pool imbalances or identifying unexpected traffic sources.
Table relationship tracking. pg_stat_monitor links queries to the tables they access, enabling table-level attribution of query load.
SQL comment extraction. Parses comments in SQL queries, which is useful for ORMs and frameworks that tag queries with metadata.
Strengths
Time-windowed analysis without external tooling. The built-in bucketing eliminates the need for a separate snapshotting system. Incident investigation ("what changed at 2pm?") works directly from the extension's view.
Query plan storage. Capturing plans alongside query statistics provides a complete picture — what is slow and why — in a single extension.
Histogram data reveals tail latency. The response time distribution exposes 95th and 99th percentile behavior that averages mask.
Application-level attribution. In microservice architectures, knowing which service generates each query is essential for debugging. pg_stat_monitor provides this natively.
Drop-in replacement. pg_stat_monitor uses the same queryid system and similar column names. Migration from pg_stat_statements requires changing the shared_preload_libraries entry and updating queries to reference the new view.
Limitations
Managed provider availability. pg_stat_monitor is not available on most major managed PostgreSQL services. As of early 2026, it is not offered on Amazon RDS, Google Cloud SQL, Supabase, or Neon. I should be direct about this: it is the single largest practical constraint.
Higher memory footprint. Time bucketing and plan storage consume more shared memory than pg_stat_statements' single cumulative hash table. Each time bucket maintains its own set of query entries.
Smaller community and ecosystem. Fewer monitoring vendor integrations, fewer Stack Overflow answers, less third-party documentation.
Less battle-tested. Approximately 6 years of production use compared to pg_stat_statements' 15+. The extension is actively maintained and stable, but it has not yet been exercised across the same breadth of edge cases and scale scenarios.
Cannot coexist with pg_stat_statements. Choosing pg_stat_monitor means giving up pg_stat_statements. Any tooling, scripts, or vendor integrations that read from pg_stat_statements must be updated.
Feature comparison table
| Feature | pg_stat_statements | pg_stat_monitor |
|---|---|---|
| Query text normalization | Yes | Yes |
| Execution statistics (calls, time, rows, blocks) | Yes | Yes |
| Planning time tracking | Yes (PG 14+) | Yes |
| WAL statistics | Yes (PG 13+) | Yes |
| JIT compilation stats | Yes (PG 15+) | Yes |
| Time-based buckets | No | Yes (configurable, default 60s) |
| Query plan capture | No | Yes |
| Response time histograms | No | Yes |
| application_name tracking | No | Yes |
| client_ip tracking | No | Yes |
| Table relationship tracking | No | Yes |
| SQL comment extraction | No | Yes |
| Managed provider support | Universal | Limited (self-managed, Percona) |
| Memory overhead | Low (~2KB per entry) | Moderate (per bucket per entry) |
| Requires server restart | Yes | Yes |
| Community / ecosystem | Massive (15+ years) | Growing (~6 years) |
| Can coexist with the other | No (not recommended) | No (designed as replacement) |
Performance overhead — honest numbers
pg_stat_statements has been widely benchmarked at under 1-2% overhead across typical workloads. The extension hooks into query execution to record statistics in shared memory — an operation that adds microseconds per query.
pg_stat_monitor incurs modestly higher overhead, benchmarked by Percona at 1-3% on representative workloads. The additional cost comes from maintaining time-bucketed statistics and (when enabled) capturing query plans. Plan capture is the most variable factor.
For most workloads, the performance difference between the two extensions is negligible. Neither extension is a performance concern on databases executing up to tens of thousands of queries per second.
Where overhead matters: Extremely high-throughput OLTP systems executing 100,000+ queries per second may observe a measurable difference. At this scale, benchmark both extensions under your specific workload before committing.
Memory consumption: pg_stat_monitor requires more shared memory than pg_stat_statements due to time bucketing. Each time bucket maintains its own set of query entries. With 10 buckets (the default) and 5,000 queries tracked, the memory footprint is roughly 10x that of pg_stat_statements tracking the same 5,000 queries. Configure pg_stat_monitor.pgsm_max and pg_stat_monitor.pgsm_bucket_time based on available shared memory.
Which should you choose?
Choose pg_stat_statements when
You are on a managed provider. If your database runs on Amazon RDS, Google Cloud SQL, Supabase, Neon, or most other managed services, pg_stat_monitor is simply not available. pg_stat_statements is universally supported.
You use a monitoring vendor that reads from pg_stat_statements. pganalyze, Datadog, New Relic, and other monitoring platforms are built to consume pg_stat_statements data. Switching to pg_stat_monitor may break these integrations.
You want the simplest, most battle-tested option. pg_stat_statements has the largest community, the most documentation, and the most proven track record. When in doubt, it is the sensible default.
You already have auto_explain for plan capture and external snapshotting for time-windowed analysis. If your monitoring infrastructure already provides these capabilities, pg_stat_monitor's additional features add less incremental value.
Universal compatibility matters. If you manage multiple PostgreSQL instances across different providers, pg_stat_statements works everywhere.
Choose pg_stat_monitor when
You are self-managed and want richer built-in analysis. If you run your own PostgreSQL instances and want advanced features without building external snapshotting infrastructure, pg_stat_monitor provides substantial value in a single extension.
Time-windowed analysis is a key requirement. If incident response frequently requires answering "what changed in the last 5 minutes?" and you do not have a snapshotting system, pg_stat_monitor's built-in bucketing is a significant advantage.
Query plan capture is important and you prefer not to manage auto_explain separately. pg_stat_monitor's integrated plan capture simplifies the monitoring stack.
Application-level attribution is essential. In microservice architectures where multiple services share a database, knowing which application_name generates each query is critical for debugging and capacity planning.
You are comfortable with a smaller ecosystem. Fewer third-party integrations, less community documentation, and a shorter track record are tradeoffs you are willing to accept.
The third option — pg_stat_statements + auto_explain
Many production PostgreSQL deployments pair pg_stat_statements with auto_explain to achieve query statistics and plan capture together. This combination covers most of what pg_stat_monitor provides, using two extensions that are both widely available on managed services.
# postgresql.conf
shared_preload_libraries = 'pg_stat_statements, auto_explain'
# auto_explain configuration
auto_explain.log_min_duration = 100 # log plans for queries taking > 100ms
auto_explain.log_analyze = on # include actual row counts and timing
auto_explain.log_buffers = on # include buffer usage
auto_explain.log_format = json # structured output for parsing This pairing provides:
- Query execution statistics (from pg_stat_statements)
- Execution plans for slow queries (from auto_explain)
- Universal availability on managed providers
What it does not provide:
- Time-based bucketing (requires external snapshotting)
- Response time histograms
application_nameorclient_iptracking- Integrated plan-to-query correlation (auto_explain logs to the PostgreSQL log, not to a queryable view)
For the complete setup guide, see the PostgreSQL monitoring stack guide. For auto_explain configuration details, see the auto_explain query optimization guide.
The pg_stat_statements + auto_explain combination is the most common production monitoring stack for PostgreSQL. It trades pg_stat_monitor's elegance — everything in one extension — for broader availability and ecosystem support. For many teams, that is the right tradeoff.
Where Gold Lapel fits
Gold Lapel operates as a PostgreSQL proxy, capturing query statistics, execution plans, and workload patterns by observing traffic in real time. It does not depend on either pg_stat_statements or pg_stat_monitor — but it reads from them when available to enrich its understanding of the workload.
This comparison is about foundational PostgreSQL observability. Regardless of which extension you choose, or what additional tooling you layer on top, pg_stat_statements or pg_stat_monitor provides the base layer of query monitoring. Understanding what these extensions capture and how to query them is knowledge that pays dividends across your entire operations practice.