Statistics Basics
PostgreSQL stores column histograms and distinct-value counts in the catalog. The planner multiplies selectivities to guess how many rows each plan node will process on PostgreSQL 18.4.
Search across all documentation pages
PostgreSQL stores column histograms and distinct-value counts in the catalog. The planner multiplies selectivities to guess how many rows each plan node will process on PostgreSQL 18.4.
Quick-reference recipe card - copy-paste ready.
SELECT schemaname, tablename, attname, n_distinct, most_common_vals, histogram_bounds
FROM pg_stats
WHERE tablename = 'orders' AND attname = 'status';When to reach for this: EXPLAIN row estimates diverge from actual rows on filtered scans or joins.
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
status text NOT NULL,
customer_id bigint NOT NULL
);
INSERT INTO orders (status, customer_id)
SELECT
CASE WHEN i % 50 = 0 THEN 'shipped' ELSE 'pending' END,
(i % 1000)::bigint
FROM generate_series(1, 100000) AS i;
ANALYZE orders;
EXPLAIN (ANALYZE)
SELECT count(*) FROM orders WHERE status = 'shipped';
SELECT attname, n_distinct, most_common_vals, most_common_freqs
FROM pg_stats
WHERE tablename = 'orders' AND attname IN ('status', 'customer_id');What this demonstrates:
most_common_vals for skewed enumsn_distinct for cardinality hintspg_stats content to plan row estimatesANALYZE samples rows (default default_statistics_target = 100) and builds per-column stats.1/n_distinct or MCV frequency when the literal appears in most_common_vals.histogram_bounds for non-uniform distributions.| Source | Shows |
|---|---|
pg_stats | Histograms, MCV lists, null fraction |
pg_stat_user_tables | last_analyze, live/dead tuples |
pg_class.reltuples | Table-level row estimate |
ALTER TABLE orders ALTER COLUMN status SET STATISTICS 500;
ANALYZE orders (status);ANALYZE. Fix: Analyze after initial load thresholds.WHERE lower(email) = 'x' ignores plain column stats. Fix: Index on expression or extended stats on expression.country + postal_code multiply incorrectly. Fix: CREATE STATISTICS ... dependencies.reltuples stale for days. Fix: Manual ANALYZE post bulk DML.
| Alternative | Use When | Don't Use When |
|---|---|---|
| Partial index | Stable selective predicate | Predicate varies per user |
| Partition pruning | Time/tenant pruning | Single small table |
| Materialized counts | Dashboard denominators | Real-time need |
Controls histogram detail and MCV count. Higher is slower analyze, better plans on skew.
Autovacuum triggers autoanalyze based on change thresholds. Bulk loads need manual analyze.
Negative values are fractions of table size (PostgreSQL scaled distinct estimator).
Yes via pg_stats on indexed columns and pg_stat_user_indexes for usage, not histograms.
IS NULL selectivity uses null_frac from pg_stats.
Per-partition stats matter; planner combines partition bounds and local stats.
Very small tables analyze fully; large tables sample. Sudden data shape changes need re-analyze.
dependencies, ndistinct, mcv extended stat kinds cover different correlation patterns.
Readable by default; sensitive data distributions may leak. Restrict roles if needed.
ANALYZE and autoanalyze for operational cadence.
Stack versions: This page was written for PostgreSQL 18.4 (stable 18, maintenance 17), pgvector 0.8+, PgBouncer 1.x, Patroni 3.x, and PostGIS 3.5+.
Reviewed by Chris St. John·Last updated Jul 19, 2026