Background processes
Cumulative statistics
Every process counts what it does (rows read, rows changed, blocks read, checkpoints …) and from time to time adds its counts to a shared-memory statistics area that the pg_stat_* views read. Since PostgreSQL 15 there is no separate statistics collector process.
In plain words
PostgreSQL keeps running totals of what happens: how many rows each table returned, inserted, updated and deleted, how many blocks were read from disk or found in cache, how many transactions committed, how often the checkpointer ran. You read them in the pg_stat_* views. Autovacuum also uses them to decide which tables need cleaning.
Counting must be cheap, so each process first accumulates many cumulative counters locally and periodically flushes them to the shared statistics area, normally just before going idle and not more often than about once per second. Cumulative totals can therefore lag while a query or transaction is still running. Current activity is separate: pg_stat_activity (with track_activities) reports the command and state independently of those cumulative counters.
Until PostgreSQL 14 a separate "stats collector" process received the counts over a UDP socket and wrote files. Since PostgreSQL 15 the counters live in shared memory and there is no such process. They are saved to the pg_stat/ directory only on a clean shutdown; after a crash they start from zero.
Pick a stats_fetch_consistency value and watch step 3: it decides whether repeated reads inside one transaction see fresh or frozen numbers.
For DBAs and operators
Variable- and fixed-numbered stats
The shared area is a dynamic shared-memory hash table with one entry per object: databases, tables and indexes (pg_stat_database, pg_stat_all_tables, pg_statio_*), functions (only with track_functions = pl|all), replication slots and subscriptions (pg_stat_subscription_stats), and since PostgreSQL 18 per-backend I/O and WAL (pg_stat_get_backend_io(pid), pg_stat_get_backend_wal(pid)). Cluster-wide "fixed" counters sit next to it: pg_stat_archiver, pg_stat_bgwriter, pg_stat_checkpointer, pg_stat_io, pg_stat_slru, pg_stat_wal.
When counts become visible
Each process accumulates "pending" stats locally and flushes them just before going idle, but not more often than once per second (PGSTAT_MIN_INTERVAL). If an entry is locked by another flusher it retries later instead of waiting. So a transaction still in progress does not change the totals. Current activity (pg_stat_activity, needs track_activities) is different: it is updated immediately. In tests, SELECT pg_stat_force_next_flush(); makes the next flush happen at once.
Reading: stats_fetch_consistency
| Value | Inside one transaction |
|---|---|
none | Every access reads shared memory again: freshest, but two queries can disagree. |
cache (default) | An object's values are cached on first access until the transaction ends. |
snapshot | The first access copies all stats of the current database: consistent, more memory and CPU. |
SELECT pg_stat_clear_snapshot(); drops the cache/snapshot without ending the transaction. Monitoring tools that sit in one long transaction and see frozen numbers usually hit exactly this.
Persistence and resets
On a clean shutdown the checkpointer writes everything to pg_stat/pgstat.stat; the next start reads it back and removes the file. After an immediate shutdown, a crash, a restore from base backup or PITR, all counters are reset. The stats_temp_directory parameter was removed in 15. Resets: pg_stat_reset() (current database), pg_stat_reset_shared('bgwriter'|'checkpointer'|'io'|'wal'|'archiver'|'recovery_prefetch'|'slru'), pg_stat_reset_single_table_counters(oid), pg_stat_reset_backend_stats(pid) (18) and others; see the stats_reset columns.
-- which tables does autovacuum think are dirty?
SELECT relname, n_live_tup, n_dead_tup, n_mod_since_analyze,
last_autovacuum, last_autoanalyze
FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10;
-- when were the counters last reset?
SELECT datname, stats_reset FROM pg_stat_database;Support notes
- After a crash
n_dead_tupandn_mod_since_analyzestart at 0, so autovacuum and autoanalyze may wait a long time before touching big tables. Run a manualVACUUM (ANALYZE)on busy tables after crash recovery or a failover to a promoted standby. track_counts = offdisables the table counters and with them autovacuum's decisions. Leave it on.track_io_timingadds timings topg_stat_io,pg_stat_databaseand EXPLAIN (BUFFERS); check the clock overhead withpg_test_timingfirst.- Rates need two samples: store snapshots of the views and compute deltas, the views are cumulative since
stats_reset. - There is no process to see in
psorpg_stat_activity; every process (backends, autovacuum, checkpointer, io workers …) reports for itself.
For developers: inside the source
Where it lives
All in src/backend/utils/activity/: pgstat.c (core, flush, snapshot, file I/O; its header comment is the design document), pgstat_shmem.c (shared hash table and entry references), and one file per kind: pgstat_relation.c, pgstat_database.c, pgstat_io.c, pgstat_wal.c, pgstat_backend.c, pgstat_checkpointer.c … Internal types are in pgstat_internal.h, kind IDs in pgstat_kind.h. Note that pg_stat_activity is separate: backend_status.c.
Data structures
PgStat_Kind: built-in IDs fromPGSTAT_KIND_DATABASE(1) toPGSTAT_KIND_WAL(12); each is described by aPgStat_KindInfoinpgstat_kind_builtin_infos[]withfixed_amount,write_to_file,shared_sizeand callbacks such asflush_pending_cb,flush_static_cb,reset_all_cb,snapshot_cb.- Variable-numbered stats sit in a
dshashtable on DSA, keyed byPgStat_HashKey(kind, dboid, objid); eachPgStatShared_HashEntrypoints to a body that starts withPgStatShared_Common, whoseLWLockprotects the counters. - A backend keeps
PgStat_EntryRefs (cached pointers to shared entries) whosependingfield holds local, not-yet-flushed counts. - Fixed-numbered stats (archiver, bgwriter, checkpointer, io, slru, wal) live in
PgStat_ShmemControl.
Call flow
- Executor and AM code call functions such as
pgstat_count_heap_insert(), which only bump the backend-localPgStat_TableStatusinrel->pgstat_info(set up throughpgstat_prep_pending_entry()); per-transaction counts are folded in at commit or abort. No shared memory is touched. - Before going idle,
PostgresMain()callspgstat_report_stat(false). It returns early unlessPGSTAT_MIN_INTERVAL(1000 ms) has passed, then runspgstat_flush_pending_entries(nowait): each entry is locked withpgstat_lock_entry(…, nowait); if busy it stays pending. AfterPGSTAT_MAX_INTERVAL(60 s) it flushes with waiting. The returned timeout re-arms an idle timer (PGSTAT_IDLE_INTERVAL, 10 s). - Readers:
pgstat_fetch_entry()honourspgstat_fetch_consistency(PGSTAT_FETCH_CONSISTENCY_NONE/CACHE/SNAPSHOT); snapshot mode callspgstat_build_snapshot(). - Shutdown: the checkpointer registers
pgstat_before_server_shutdown(), which callspgstat_write_statsfile()→pg_stat/pgstat.stat. At start-up the startup process runspgstat_restore_stats(), orpgstat_discard_stats()if the previous run crashed (seeStartupXLOG()).
Extension hooks
An extension loaded via shared_preload_libraries can add its own kind with pgstat_register_kind() (IDs from PGSTAT_KIND_CUSTOM_MIN; PGSTAT_KIND_EXPERIMENTAL = 24 for development). Working examples: injection_stats.c (variable-numbered) and injection_stats_fixed.c (fixed). The API is documented in xfunc.sgml.
Experimenting
- Tests:
stats.sql,stats.spec(visibility across sessions and consistency modes) and029_stats_restart.pl(persistence across clean and crash restarts). pg_stat_force_next_flush()andpg_stat_have_stats(kind, dboid, objid)are handy in tests.
Parameters that shape this
stats_fetch_consistency track_counts track_activities track_functions track_io_timing track_wal_io_timing track_activity_query_size track_cost_delay_timing
Related topics
Autovacuum: when tables get vacuumed Autovacuum launcher and workers Checkpointer Background writer Memory architecture Crash recovery ANALYZE and statistics Process model