Hook: Indexes are the road signs; statistics are the traffic report that tells the database which road is fast right now.
Question: What are statistics in SQL databases, and why do they matter for index performance?
Answer: Statistics are small pieces of metadata about table data, such as how many rows exist, how many values are distinct, which values are common, and how the values are spread out. The query planner uses them to estimate cardinality meaning the number of rows a query is likely to touch. Those estimates help it choose between a sequential scan, an index scan, and different join orders.
Interview-Ready Answer: In SQL databases, statistics are the optimizer’s facts about the data distribution. I think of them as the database’s “guessing engine” inputs: the planner uses them to estimate how many rows match a filter, which index is worth using, and which join order will be cheapest. They do not speed up a query directly; instead, they help the database pick a better plan. A big gotcha is stale statistics: if the data changes a lot and ANALYZE has not run, the optimizer can make a very bad choice even when the right index exists.
Detailed Explanation: Statistics are not the same thing as indexes. An index is a data structure that helps the engine find rows quickly; statistics are facts about the data that help the engine predict how expensive a plan will be. In PostgreSQL, for example, the planner reads column statistics such as null_frac, n_distinct, most_common_vals, histogram bounds, and correlation. Other databases store similar ideas under different names.
ANALYZE or auto-analyze collects fresh facts. Most systems sample the table rather than scanning every row, so this is much cheaper than a full read of the table. In PostgreSQL, the default statistics target is 100, and auto-analyze is usually triggered after roughly 50 + 10% of a table has changed, though exact behavior is database-specific.open, 1% have closed, and values are spread mostly within a certain range.| Topic | Statistics | Indexes |
|---|---|---|
| Purpose | Estimate rows | Find rows fast |
| Main use | Plan choice | Execution speed |
| Built from | Samples/distributions | Key values and pointers |
| Maintenance cost | Low to medium | Write overhead |
| Bad outcome | Wrong plan | Slower writes, storage use |
If a table used to be 99% open and suddenly becomes 30% closed, the planner may still believe closed is rare. It then may pick an index scan that touches far more rows than expected, or choose the wrong join order. This is why large batch jobs, bulk loads, or sudden traffic shifts often need a manual ANALYZE afterward.
country and currency move together, single-column stats may assume they are independent and undercount the result set. Some databases support extended statistics for this.Memory angle: The planner is a detective; statistics are its witness statements. If the witnesses are old or misleading, the detective picks the wrong route.
Real-World Example: Imagine an e-commerce checkout service that queries orders for pending payments. During a flash sale, a background job flips huge numbers of rows to pending. For a while, the planner still thinks pending orders are rare, so it chooses a plan that looks cheap on paper but actually touches tens of thousands of rows. The symptom is a sudden jump in P95 latency, CPU spikes on the database, and logs that show estimates like rows=50 when the query actually returns rows=20000.
The fix is usually to refresh stats with ANALYZE, let auto-analyze catch up, and sometimes add extended statistics or a better index if the access pattern is stable. The important lesson is that a good index can still underperform if the optimizer is making decisions with stale information.
-- PostgreSQL demo: statistics help the planner estimate row counts.
-- This script shows three stages:
-- 1) fresh table with no stats yet
-- 2) analyzed table with useful stats
-- 3) stale stats after the data distribution changes
DROP TABLE IF EXISTS order_events;
CREATE TEMP TABLE order_events (
order_id bigserial PRIMARY KEY,
status text NOT NULL,
amount numeric(10,2) NOT NULL
);
-- Build a skewed dataset:
-- Most rows are 'open', a small slice are 'closed'.
INSERT INTO order_events (status, amount)
SELECT CASE WHEN gs <= 49500 THEN 'open' ELSE 'closed' END,
CASE WHEN gs % 100 = 0 THEN 999.99 ELSE 19.99 END
FROM generate_series(1, 50000) AS gs;
CREATE INDEX idx_order_events_status ON order_events(status);
-- Before ANALYZE, the optimizer has little distribution information.
-- The exact plan can vary by PostgreSQL version and settings.
EXPLAIN
SELECT *
FROM order_events
WHERE status = 'closed';
-- Refresh column statistics.
ANALYZE order_events;
-- Inspect the collected statistics.
SELECT attname,
null_frac,
n_distinct,
most_common_vals,
most_common_freqs,
histogram_bounds,
correlation
FROM pg_stats
WHERE schemaname LIKE 'pg_temp%'
AND tablename = 'order_events'
ORDER BY attname;
-- After ANALYZE, the planner has a better estimate for the skewed status column.
EXPLAIN
SELECT *
FROM order_events
WHERE status = 'closed';
-- Failure path: a large data shift happens, but stats are now stale.
-- Now 'closed' is no longer rare, so the old estimate is misleading.
INSERT INTO order_events (status, amount)
SELECT 'closed', 42.00
FROM generate_series(1, 20000);
-- This plan may still reflect the old, too-optimistic picture.
EXPLAIN
SELECT *
FROM order_events
WHERE status = 'closed';
-- Fix the stale stats so the optimizer can re-estimate correctly.
ANALYZE order_events;
EXPLAIN
SELECT *
FROM order_events
WHERE status = 'closed';Follow-up & Tricky Questions:
ANALYZE different from VACUUM? ANALYZE collects planner statistics. VACUUM reclaims dead tuples and keeps storage healthy. They are related, but they solve different problems.Tricky / gotcha questions:
Common Mistakes:
ANALYZE or let auto-analyze catch up before judging performance.Memory Hook: “Index = shortcut. Statistics = live traffic.” The shortcut only helps if the traffic report is current.
Cheat Sheet:
ANALYZE refreshes stats; auto-analyze usually does it in the background.Practice Tasks:
EXPLAIN before and after ANALYZE.pg_stats, and identify the most common values and histogram fields.