Що таке кардинальність індексу?
Ці поняття пов'язані, але різняться:
🟢 Junior Level
Кардинальність — це кількість унікальних значень у колонці таблиці.
Навіщо це знати: планувальник PostgreSQL використовує кардинальність, щоб вирішити — використати індекс чи прочитати таблицю цілком. Висока кардинальність = багато унікальних значень = індекс ефективний. Низька = мало унікальних значень = швидше за все, Seq Scan буде швидшим за індекс.
Проста аналогія: Уявіть мішок з кульками різних кольорів. Кардинальність — це скільки в мішку кульок різних кольорів (не рахуючи кількість кожного кольору).
Основна ідея:
- Висока кардинальність = багато унікальних значень (наприклад, email)
- Низька кардинальність = мало унікальних значень (наприклад, стать: M/F)
- Індекси ефективніші на полях з високою кардинальністю
Приклад:
-- Таблиця users (1,000,000 рядків)
-- Висока кардинальність
SELECT COUNT(DISTINCT email) FROM users; -- ~1,000,000 (унікальні)
-- Низька кардинальність
SELECT COUNT(DISTINCT gender) FROM users; -- 2 (M та F)
-- Індекс на email — чудовий вибір ✅
-- Індекс на gender — поганий вибір ❌
Коли це важливо:
- При виборі полів для індексування
- При аналізі, чому індекс не використовується
- При оптимізації повільних запитів
🟡 Middle Level
Кардинальність vs Селективність
Ці поняття пов’язані, але різняться:
| Термін | Визначення | Приклад |
|---|---|---|
| Кардинальність | Кількість унікальних значень | COUNT(DISTINCT city) = 500 |
| Селективність | Кардинальність / Загальна кількість | 500 / 1,000,000 = 0.0005 |
Чим вища кардинальність → тим вища селективність → тим ефективніший індекс.
Чому: висока селективність означає, що запит поверне мало рядків. Index Scan читає 3-4 сторінки B-tree + 1 сторінку Heap на знайдений рядок. Якщо рядків 100 000 з 1 000 000 — це 100 000 випадкових читань Heap, що повільніше за одне послідовне читання всієї таблиці (Seq Scan).
Як PostgreSQL оцінює кардинальність
PostgreSQL збирає статистику через команду ANALYZE (виконується автоматично):
-- Подивитися статистику по колонках
SELECT
attname as column_name,
n_distinct, -- Оцінка унікальності
correlation -- Кореляція з фізичним порядком
FROM pg_stats
WHERE tablename = 'users'
AND attname = 'email';
n_distinct:
- Якщо > 0 — абсолютна кількість унікальних значень
- Якщо < 0 (наприклад, -1) — частка від загального числа (-1 = 100% унікальні)
Вплив на вибір плану виконання
-- Висока кардинальність (email)
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
-- Plan: Index Scan (знайде 1 рядок з 1 млн)
-- Низька кардинальність (status)
EXPLAIN SELECT * FROM users WHERE status = 'ACTIVE';
-- Plan: Seq Scan (поверне 500,000 рядків — індекс невигідний)
Поріг перемикання (орієнтовний):
| % рядків у вибірці | План |
|---|---|
| < 1-5% | Index Scan |
| 5-20% | Bitmap Index Scan (збирає TID в бітову карту, потім читає heap — менше випадкових I/O ніж Index Scan) |
| > 20% | Seq Scan |
Data Skew (Перекіс даних)
-- Таблиця orders: 1,000,000 рядків
SELECT status, COUNT(*)
FROM orders
GROUP BY status;
-- Результат:
-- ACTIVE: 950,000 (95%)
-- PENDING: 40,000 (4%)
-- ERROR: 10,000 (1%)
-- Запит 1: WHERE status = 'ERROR' → Index Scan (рідкісне значення)
-- Запит 2: WHERE status = 'ACTIVE' → Seq Scan (часте значення)
Чому різні плани: PostgreSQL зберігає MCV (Most Common Values) — список найчастіших значень з їх частотою. Для ‘ERROR’ (1%) планувальник знає, що знайде ~10 000 рядків з 1 млн — Index Scan вигідний. Для ‘ACTIVE’ (95%) — 950 000 рядків, що дешевше прочитати Seq Scan (одне послідовне читання проти 950 000 випадкових).
Типові помилки
- Індексування полів з низькою кардинальністю
-- ❌ Погано: всього 2 значення CREATE INDEX idx_users_gender ON users(gender); -- Seq Scan все одно буде обраний для більшості запитів - Ігнорування складної кардинальності
-- Окремо: city (500), age (80) — середня кардинальність -- Разом: (city, age) → висока кардинальність комбінації! CREATE INDEX idx_users_city_age ON users(city, age); - Неактуальна статистика
-- Після масової вставки статистика застаріла -- Планувальник може обрати неправильний план -- Рішення: оновити статистику ANALYZE users;
Практичний приклад
-- Перевірка кардинальності колонок
SELECT
attname as column_name,
n_distinct,
CASE
WHEN n_distinct < 0 THEN 'Висока (' || ABS(n_distinct) * 100 || '%)'
WHEN n_distinct < 100 THEN 'Низька'
WHEN n_distinct < 10000 THEN 'Середня'
ELSE 'Висока'
END as cardinality_level
FROM pg_stats
WHERE tablename = 'users';
🔴 Senior Level
Як Postgres оцінює кардинальність: Deep Dive
Статистика зберігається в pg_statistic (доступ через pg_stats):
Ключові поля:
- n_distinct
-- Оцінка унікальності -- Приклади: -- -1.0 = 100% унікальних (кожне значення унікальне) -- -0.5 = 50% унікальних -- 100 = рівно 100 унікальних значень - most_common_vals (MCV)
-- Топ найчастіших значень та їх частота -- Приклад для status: -- MCV: ['ACTIVE', 'PENDING', 'ERROR'] -- Freq: [0.95, 0.04, 0.01] -- Планувальник використовує це для точної оцінки: -- WHERE status = 'ERROR' → Index Scan (1% рядків) -- WHERE status = 'ACTIVE' → Seq Scan (95% рядків) - histogram_bounds
-- Розподіл значень для не-MCV значень -- Розбиває діапазон на кошики (за замовчуванням 100) -- Використовується для оцінки селективності діапазонів - correlation
-- Число від -1 до 1 -- Показує, наскільки фізичний порядок рядків -- збігається з логічним порядком значень в індексі -- correlation = 1.0 → рядки на диску відсортовані так само, як в індексі -- correlation = 0.0 → повний хаос (Random I/O) -- correlation = -1.0 → відсортовані у зворотному порядку
Extended Statistics (PostgreSQL 10+)
Проблема: PostgreSQL припускає незалежність колонок.
-- Запит
SELECT * FROM cars
WHERE make = 'Honda' AND model = 'Civic';
-- Планувальник думає:
-- P(make='Honda') = 0.1
-- P(model='Civic') = 0.05
-- P(both) = 0.1 * 0.05 = 0.005 (0.5%)
-- Реальність:
-- Якщо make='Honda', то model='Civic' набагато ймовірніше!
-- P(both) = 0.03 (3%) — в 6 разів більше!
Рішення: Extended Statistics
-- 1. Functional Dependencies
-- Якщо знаючи модель, ми знаємо марку
CREATE STATISTICS s_make_model (dependencies)
ON make, model FROM cars;
-- 2. N-Distinct
-- Кардинальність комбінації колонок
CREATE STATISTICS s_city_age (ndistinct)
ON city, age FROM users;
-- 3. MCV для комбінацій (PG 13+)
-- Найчастіші комбінації значень
CREATE STATISTICS s_status_priority (mcv)
ON status, priority FROM orders;
-- Застосовуємо
ANALYZE cars;
ANALYZE users;
ANALYZE orders;
На PG 12 і нижче тип
mcvнедоступний — використовуйтеdependenciesтаndistinct.
Перевірка:
-- Подивитися розширену статистику
SELECT * FROM pg_stats_ext
WHERE tablename = 'cars';
-- Конкретно ndistinct
SELECT
stxname,
stxkeys,
stxndistinct
FROM pg_statistic_ext
WHERE stxname = 's_city_age';
Вплив statistics_target
-- За замовчуванням: 100 кошиків для гістограми
SHOW default_statistics_target;
-- Для колонок з «рваним» розподілом збільшуємо точність
ALTER TABLE users ALTER COLUMN email SET STATISTICS 1000;
ALTER TABLE orders ALTER COLUMN status SET STATISTICS 500;
-- Оновлюємо статистику
ANALYZE users;
ANALYZE orders;
-- ⚠️ Ціна: ANALYZE виконуватиметься довше
-- Але планування стане точнішим
Edge Cases
- Кореляція вбиває індекс
-- Перевірка SELECT attname, correlation FROM pg_stats WHERE tablename = 'orders' AND attname = 'created_at'; -- correlation = 0.05 → Random I/O -- Рішення: CLUSTER (пересортувати таблицю) CLUSTER orders USING idx_orders_created; -- ⚠️ Блокує таблицю! Робіть в maintenance window - Застаріла статистика
-- Перевірка: коли останній раз був ANALYZE SELECT relname, last_analyze, last_autoanalyze, n_mod_since_analyze -- Змін з останнього ANALYZE FROM pg_stat_user_tables WHERE n_mod_since_analyze > 100000; -- Багато змін! -- Рішення ANALYZE VERBOSE orders; - Partitioned Tables
-- Для партиційованих таблиць статистика збирається на кожній партиції -- Планувальник може помилятися в оцінці -- Рішення: збільшити statistics_target на ключових партиціях ALTER TABLE orders_y2024 ALTER COLUMN created_at SET STATISTICS 1000;
Продуктивність
Вплив на планування:
| statistics_target | Точність плану | Час ANALYZE |
|---|---|---|
| 10 (мінімум) | Низька | Швидко |
| 100 (за замовчуванням) | Хороша | Нормально |
| 1000 | Відмінна | В 10x довше |
| 10000 | Максимальна | В 100x довше |
Memory Footprint статистики:
- Кожен кошик гістограми: ~24 байта
- При statistics_target = 100: ~2.4 КБ на колонку
- При 1000: ~24 КБ на колонку
- Вплив на RAM: мінімальний
Production Experience
Реальний сценарій #1: Data Skew вбиває продуктивність
- Лог-платформа: 500 млн записів
- Таблиця:
level(DEBUG: 80%, INFO: 15%, ERROR: 5%) - Проблема: Запити по ERROR були повільними
- Причина: statistics_target = 100, MCV список не покривав усі рівні
- Рішення:
ALTER TABLE logs ALTER COLUMN level SET STATISTICS 1000; ANALYZE logs; - Результат: Планувальник став точно оцінювати селективність, Index Scan для ERROR
Реальний сценарій #2: Correlated Columns
- E-commerce: замовлення по
(country, city) - Проблема: Планувальник помилявся в оцінці на 2 порядки
- Причина: city сильно залежить від country (Москва тільки в Росії)
- Рішення:
CREATE STATISTICS s_country_city (ndistinct, dependencies) ON country, city FROM orders; ANALYZE orders; - Результат: Оцінка рядків стала точною, планувальник обрав правильний Join
Monitoring
-- 1. Перевірка актуальності статистики
SELECT
relname,
last_analyze,
last_autoanalyze,
n_mod_since_analyze,
CASE
WHEN n_mod_since_analyze > 100000 THEN 'NEEDS ANALYZE'
ELSE 'OK'
END as status
FROM pg_stat_user_tables
ORDER BY n_mod_since_analyze DESC;
-- 2. Перевірка correlation
SELECT
attname,
ROUND(correlation::numeric, 3) as correlation,
CASE
WHEN ABS(correlation) < 0.3 THEN 'LOW - Random I/O'
WHEN ABS(correlation) < 0.7 THEN 'MEDIUM'
ELSE 'HIGH - Sequential I/O'
END as assessment
FROM pg_stats
WHERE tablename = 'orders'
ORDER BY ABS(correlation) ASC;
-- 3. Перевірка n_distinct
SELECT
attname,
n_distinct,
CASE
WHEN n_distinct < 0 THEN 'High (' || ABS(n_distinct) * 100 || '%)'
WHEN n_distinct < 100 THEN 'Low'
WHEN n_distinct < 10000 THEN 'Medium'
ELSE 'High'
END as cardinality
FROM pg_stats
WHERE tablename = 'users';
-- 4. Перевірка extended statistics
SELECT
stxname,
stxkeys::regclass[],
stxndistinct,
stxdependencies
FROM pg_statistic_ext;
Best Practices
- Моніторте
n_mod_since_analyze— якщо > 10% таблиці, робіть ANALYZE - Збільшуйте
statistics_targetдля колонок з аномальним розподілом - Використовуйте Extended Statistics для корельованих колонок
- Перевіряйте correlation — < 0.3 означає Random I/O
- Не забувайте ANALYZE після масової вставки/видалення
- MCV список повинен покривати > 80% даних для точного планування
Резюме для Senior
- Кардинальність — ключовий вхідний параметр для оптимізатора
- n_distinct, MCV, histogram, correlation — 4 стовпи статистики
- Data Skew — часта причина неправильних планів; перевіряйте MCV
- Extended Statistics (PG 10+) вирішує проблему корельованих колонок
- correlation < 0.3 → Random I/O → подумайте про CLUSTER
- statistics_target — баланс між точністю та часом ANALYZE
- Регулярно перевіряйте
pg_statsтаpg_statistic_ext
🎯 Шпаргалка для співбесіди
Обов’язково знати:
- Кардинальність = кількість унікальних значень; Селективність = кардинальність / всього рядків
- n_distinct:
-1= 100% унікальні,500= рівно 500 унікальних - MCV (Most Common Values): топ частих значень + частота — для skewed даних
- Correlation:
-1..1— наскільки фізичний порядок збігається з логічним - correlation = 0 → Random I/O (хаос на диску), correlation = 1 → Sequential I/O
- Extended Statistics (PG 10+):
dependencies,ndistinct,mcvдля корельованих колонок - statistics_target = 100 (за замовчуванням), 1000 — для skewed колонок (в 10x довше ANALYZE)
Часті уточнюючі питання:
- «Чому індекс на status (ACTIVE/PENDING) не використовується?» → Низька кардинальність, Seq Scan швидший
- «Що буде, якщо statistics_target занадто низький?» → Планувальник помиляється в оцінці rows
- «Як вирішити проблему корельованих колонок (country, city)?» →
CREATE STATISTICS ... (dependencies) - «Що означає correlation = 0.05?» → Майже хаотичний порядок → CLUSTER або новий індекс
Червоні прапорці (НЕ говорити):
- ❌ «Індекс на полі з 2 значеннями (M/F) прискорить пошук» (Seq Scan все одно швидший)
- ❌ «n_distinct = -1 значить немає унікальних значень» (навпаки — 100% унікальні!)
- ❌ «statistics_target не впливає на продуктивність» (впливає на час ANALYZE та точність планів)
Пов’язані теми:
- [[Для чого потрібні індекси]] → селективність та кардинальність
- [[Навіщо потрібен ANALYZE]] → збір статистики, n_distinct, MCV, histogram
- [[Що таке складений індекс]] → складна кардинальність
- [[Як оптимізувати повільні запити]] → вплив статистики на план