The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Inspect estimated rows only | EXPLAIN SELECT * FROM app.orders WHERE status = 'open'; | View examples |
| Measure actual rows | EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM app.orders
WHERE status = 'open'; | View examples |
| Expose planning settings | EXPLAIN (ANALYZE, SETTINGS)
SELECT count(*)
FROM app.orders; | View examples |
| Analyze one table | ANALYZE app.orders; | View examples |
| Analyze selected columns | ANALYZE app.orders (tenant_id, status); | View examples |
| Inspect table statistics | SELECT *
FROM pg_stats
WHERE schemaname = 'app' AND tablename = 'orders'; | View examples |
| Check approximate table size | SELECT reltuples, relpages
FROM pg_class
WHERE oid = 'app.orders'::regclass; | View examples |
| Set a column target | ALTER TABLE app.orders ALTER COLUMN tenant_id
SET STATISTICS 500; | View examples |
| Restore the default target | ALTER TABLE app.orders ALTER COLUMN tenant_id
SET STATISTICS DEFAULT; | View examples |
| Create dependency statistics | CREATE STATISTICS app.orders_dep (dependencies)
ON tenant_id, region
FROM app.orders; | View examples |
| Create MCV statistics | CREATE STATISTICS app.orders_mcv (mcv)
ON tenant_id, status
FROM app.orders; | View examples |
| Inspect extended statistics | SELECT *
FROM pg_stats_ext
WHERE statistics_name = 'orders_tenant_status'; | View examples |
| Set a proportional distinct estimate | ALTER TABLE app.events ALTER COLUMN tenant_id
SET (n_distinct = -0.02); | View examples |
| Remove a distinct override | ALTER TABLE app.events ALTER COLUMN tenant_id RESET (n_distinct); | View examples |
| Analyze a partition hierarchy | ANALYZE app.events; | View examples |
| Inspect extended-stat data safely | SELECT statistics_name, kinds
FROM pg_stats_ext
WHERE schemaname = 'app'; | View examples |
PostgreSQL chooses scans, join orders, and algorithms from estimated row counts. Accurate estimates require representative statistics, appropriate per-column targets, and extended statistics for correlated predicates; tuning planner switches before fixing evidence often hides the real problem.
Step by step
Detailed examples
Compare estimated rows with measured rows
Start with EXPLAIN to inspect without executing. EXPLAIN ANALYZE really runs the statement, so use a transaction and rollback for data-changing statements, and remember volatile functions or external effects may not roll back. Large estimate-to-actual ratios near the first divergence usually point to stale or insufficient statistics.
EXPLAIN (ANALYZE, BUFFERS, SETTINGS, FORMAT TEXT)
SELECT * FROM app.orders
WHERE tenant_id = 42 AND status = 'open'; Note: Test on representative data and load; instrumentation and cold-cache effects can change timings.
Refresh statistics after meaningful data changes
ANALYZE samples data under a lock compatible with ordinary reads and writes, though it conflicts with some maintenance and DDL and consumes I/O and CPU. Autovacuum normally analyzes changed tables, but bulk loads and skew shifts can justify a targeted manual run. Results remain approximate and can vary between samples.
ANALYZE VERBOSE app.orders (tenant_id, status, created_at); Note: Run after a bulk load or major distribution change, not reflexively after every small write.
Read the security-filtered pg_stats view
pg_stats exposes human-readable statistics only for tables the current user can read, unlike restricted pg_statistic. Most-common values, histogram bounds, null fraction, correlation, and distinct estimates explain many selectivity decisions. Arrays may be omitted for types whose operators are not readable by the user.
SELECT attname, null_frac, n_distinct, correlation, most_common_vals, most_common_freqs
FROM pg_stats
WHERE schemaname = 'app' AND tablename = 'orders'
AND attname IN ('tenant_id', 'status'); Raise detail only for columns that need it
A higher target increases the sample and the maximum sizes of MCV and histogram arrays, improving irregular distributions at added analyze time and catalog space. It does not make estimates exact. Use a focused target and re-run ANALYZE; target zero disables column statistics and is rarely appropriate for filter, join, grouping, or ordering columns.
ALTER TABLE app.orders ALTER COLUMN tenant_id SET STATISTICS 500;
ANALYZE app.orders (tenant_id); Note: ALTER TABLE requires ownership or suitable maintenance privileges and takes a relation lock; validate the benefit before standardizing the value.
Model correlation across columns
The independence assumption can badly underestimate predicates such as city plus postal code. Extended statistics support dependencies, multivariate distinct counts, and multivariate MCV lists, but they are not currently used to improve join selectivity between tables. CREATE STATISTICS does not collect data until ANALYZE runs.
CREATE STATISTICS app.orders_tenant_status (dependencies, mcv, ndistinct)
ON tenant_id, status FROM app.orders;
ANALYZE app.orders; Note: The creator must own the table; the statistics object then has independent ownership. Inspect impact with representative plans.
Use n_distinct overrides only with strong evidence
For huge tables, sampling can misestimate distinct values. A positive override is an absolute count; a negative value is the negative fraction of table rows, with -1 meaning unique. Overrides affect subsequent ANALYZE results and can outlive the workload assumption, so document and periodically validate them.
ALTER TABLE app.events ALTER COLUMN tenant_id SET (n_distinct = -0.02);
ANALYZE app.events (tenant_id); Note: This asserts distinct tenants are approximately two percent of row count; use only after measurement.
Account for partitions and statistics confidentiality
PostgreSQL 18 ANALYZE can sample a partitioned hierarchy, but parent-level statistics can become stale when only leaf partitions change; schedule parent ANALYZE where workload needs it. pg_statistic is restricted because values may reveal data. Use pg_stats and built-in operators, and avoid SECURITY DEFINER diagnostics with an unsafe search_path.
ANALYZE app.events; Note: This recursively analyzes partitions by default and gathers hierarchy statistics; schedule it after partition-only loads when needed.
Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
- PostgreSQL Global Development GroupStatistics Used by the Plannerpostgresql.org
- PostgreSQL Global Development GroupHow the Planner Uses Statisticspostgresql.org
- PostgreSQL Global Development GroupANALYZEpostgresql.org
- PostgreSQL Global Development GroupCREATE STATISTICSpostgresql.org
- PostgreSQL Global Development Grouppg_statspostgresql.org
- PostgreSQL Global Development Grouppg_stats_extpostgresql.org
- PostgreSQL Global Development GroupEXPLAINpostgresql.org
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



