상관된 열을 위한 다변량 통계
플래너가 독립적이라고 가정하는 의존 관계를 포착하도록 CREATE STATISTICS 객체를 만드는 방법을 배웁니다.
상관된 열을 위한 다변량 통계은(는) CoddyKit의 무료 PostgreSQL Performance & Query Optimization 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 PostgreSQL Performance & Query Optimization 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. PostgreSQL Performance & Query Optimization 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
The Independence Assumption
When PostgreSQL estimates how many rows a query will return, it leans on per-column statistics stored in pg_statistic. To combine predicates on multiple columns, the planner makes a crucial simplifying assumption: the columns are statistically independent.
Under independence, the selectivity of WHERE a = 1 AND b = 2 is computed as sel(a=1) * sel(b=2). That multiplication is fast and correct — but only when the columns truly are unrelated.
In real schemas, columns are frequently correlated: a city implies a postal code, a product implies a category, an order date implies a fiscal quarter. When the planner multiplies selectivities for correlated columns, its estimate collapses far below reality.
How Bad Estimates Hurt You
A row-count estimate that is off by orders of magnitude steers the planner toward the wrong plan:
- Underestimate → planner picks a nested loop expecting 3 rows, but 300,000 arrive → the loop executes its inner side hundreds of thousands of times.
- Underestimate → planner chooses an index scan + heap fetches instead of a single sequential scan that would have been cheaper.
- Bad join order → a large intermediate result is materialized early, blowing up memory and spilling to disk.
The symptom you see in EXPLAIN ANALYZE is a wide gap between rows= (estimated) and actual rows=. That gap is your signal that correlation may be the culprit.
EXPLAIN ANALYZE
SELECT * FROM addresses
WHERE city = 'New York'
AND state = 'NY';Seeing the Misestimate
Consider an addresses table where city functionally determines state — every row with city = 'New York' also has state = 'NY'. The two predicates select the same rows, so the combined selectivity equals sel(city) alone.
But the planner multiplies: sel(city) * sel(state), producing an estimate that can be 10× or 100× too small. In the EXPLAIN ANALYZE output below, watch the gap between the estimated and actual row counts on the scan node.
EXPLAIN ANALYZE
SELECT count(*) FROM addresses
WHERE city = 'New York'
AND state = 'NY';
-- Seq Scan ... (rows=12 ...) (actual ... rows=8400 ...)
-- ^estimate ^realityEnter CREATE STATISTICS
PostgreSQL 10+ lets you teach the planner about column relationships with extended statistics objects, created via CREATE STATISTICS.
An extended statistics object names a set of columns (or expressions) on one table and one or more kinds of statistics to gather over them. Once created and analyzed, the planner consults these multivariate statistics instead of blindly multiplying per-column selectivities.
The three kinds are:
- ndistinct — number of distinct combinations of the listed columns.
- dependencies — functional-dependency degrees between columns.
- mcv — most-common-value lists over the column group.
CREATE STATISTICS stat_addr_city_state
ON city, state
FROM addresses;
ANALYZE addresses;Functional Dependencies
The dependencies kind captures functional dependencies: how strongly the value of one column implies the value of another. PostgreSQL stores a degree between 0 and 1 for each direction.
For our table, city → state has a degree near 1.0 (knowing the city fully determines the state), while state → city is much lower (a state has many cities).
When the planner evaluates WHERE city = ? AND state = ? and finds a strong city → state dependency, it stops multiplying and instead keeps essentially the selectivity of the determining column.
CREATE STATISTICS stat_addr_deps (dependencies)
ON city, state
FROM addresses;
ANALYZE addresses;Inspecting the Stored Dependencies
After ANALYZE, the computed values live in the catalog view pg_stats_ext (raw form) and pg_stats_ext_exprs for expression stats. The dependencies column shows each directional degree.
A degree at or near 1.000000 for "1 => 2" (column 1 implies column 2) confirms a near-perfect functional dependency — exactly the case where the independence assumption was hurting you.
SELECT statistics_name,
attnames,
dependencies
FROM pg_stats_ext
WHERE statistics_name = 'stat_addr_deps';
-- dependencies: {"1 => 2": 1.000000, "2 => 1": 0.140000}ndistinct for GROUP BY and Joins
The ndistinct kind records the number of distinct combinations across the listed columns. Without it, the planner estimates distinct combos as the product of per-column distinct counts, which overshoots badly for correlated columns.
This matters most for GROUP BY a, b, c (estimating the number of groups) and for grouped aggregates feeding a hash aggregate. A wrong group estimate leads to under-sized hash tables and disk spills, or to a wrongly chosen sort-based aggregate.
CREATE STATISTICS stat_sales_ndist (ndistinct)
ON region, country, city
FROM sales;
ANALYZE sales;
EXPLAIN
SELECT region, country, city, count(*)
FROM sales
GROUP BY region, country, city;MCV for Skewed Combinations
Functional dependencies assume a uniform, table-wide relationship. But sometimes the correlation is value-specific — certain combinations are extremely common while others never occur. That is where the mcv (most-common-values) kind shines.
An MCV list over a column group stores the actual frequent combinations and their frequencies, so the planner can estimate predicates like WHERE category = 'A' AND status = 'shipped' using the real observed frequency of that pair rather than a derived approximation.
MCV is the most powerful but also the most storage-intensive kind; reach for it when dependencies alone do not fix the estimate.
CREATE STATISTICS stat_orders_mcv (mcv)
ON category, status
FROM orders;
ANALYZE orders;Combining Kinds in One Object
You can request multiple kinds in a single statistics object. If you omit the kind list entirely, PostgreSQL builds all applicable kinds for that column set.
A common, practical recipe is to list ndistinct, dependencies, mcv together for a set of columns that appear together in both WHERE and GROUP BY clauses. One ANALYZE then populates everything.
Note that mcv and dependencies support up to a limited number of columns, and you should keep statistics objects focused on columns that are actually queried together — not every column pair in the table.
CREATE STATISTICS stat_addr_all (ndistinct, dependencies, mcv)
ON city, state, zip
FROM addresses;
ANALYZE addresses;Statistics on Expressions
PostgreSQL 14+ extends CREATE STATISTICS to expressions, not just bare columns. If your queries filter on date_trunc('month', created_at) or lower(email), the planner normally has no statistics for that computed value and falls back to a generic guess.
A single-expression statistics object gives the planner per-expression stats; a multi-column object mixing expressions and columns captures correlation between a computed value and a stored column.
CREATE STATISTICS stat_login_expr
ON lower(email), date_trunc('day', created_at)
FROM logins;
ANALYZE logins;Workflow, Maintenance, and Cleanup
Extended statistics are not automatic — you create them deliberately based on observed misestimates. A reliable workflow:
- Find a query whose
EXPLAIN ANALYZEshows a large estimate-vs-actual gap on a multi-column predicate. - Create a statistics object over exactly those correlated columns.
- Run
ANALYZEon the table (or wait for autovacuum's analyze) to populate it. - Re-run
EXPLAIN ANALYZEand confirm the estimate now tracks reality.
Statistics objects are refreshed by every ANALYZE, so they stay current automatically once created. Drop ones you no longer need with DROP STATISTICS to avoid paying their ANALYZE cost.
DROP STATISTICS IF EXISTS stat_addr_deps;Quick Check: Choosing the Right Kind
You have a query SELECT count(*) FROM addresses WHERE city = $1 AND state = $2. EXPLAIN ANALYZE shows the planner estimates 15 rows but 9,000 actually match, because city fully determines state. Which extended statistics kind most directly fixes this estimate?
Recap
The planner assumes columns are independent and multiplies per-column selectivities — which underestimates rows when columns are correlated, leading to bad plans.
- CREATE STATISTICS teaches the planner about multi-column relationships.
- dependencies fixes equality-filter underestimates from functional dependencies (e.g. city → state).
- ndistinct fixes distinct-combination estimates for
GROUP BYand grouped aggregates. - mcv captures value-specific skewed combinations for the most accurate per-pair selectivity.
- Create on expressions (PG14+), refresh via
ANALYZE, verify withEXPLAIN ANALYZE, and inspect results inpg_stats_ext.
Target only columns truly queried together, then confirm the estimate-vs-actual gap closes.
자주 묻는 질문
“상관된 열을 위한 다변량 통계” 강의는 무료인가요?
네 — “상관된 열을 위한 다변량 통계” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 PostgreSQL Performance & Query Optimization 강의 전체를 잠금 해제할 수 있습니다. PostgreSQL Performance & Query Optimization 강의에는 총 4개의 강의가 포함되어 있습니다.
“상관된 열을 위한 다변량 통계”에서 뭘 배우나요?
플래너가 독립적이라고 가정하는 의존 관계를 포착하도록 CREATE STATISTICS 객체를 만드는 방법을 배웁니다. 브라우저에서 직접 실행하는 실습 코드로 PostgreSQL Performance & Query Optimization을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
PostgreSQL Performance & Query Optimization을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 PostgreSQL Performance & Query Optimization은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.
“상관된 열을 위한 다변량 통계” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 PostgreSQL Performance & Query Optimization 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 PostgreSQL Performance & Query Optimization 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 플래너가 행 수를 추정하는 방식
- 상관된 열을 위한 다변량 통계
- MCV 및 고유값 수 보정
- 실제 행 수와 추정치 검증