Які недоліки є у індексів?
Кожен індекс збільшує кількість операцій запису:
🟢 Junior Level
Індекси мають зворотний бік: вони прискорюють читання, але сповільнюють запис.
Проста аналогія: Уявіть, що у вас є книга зі змістом. Коли ви додаєте новий розділ, вам потрібно не тільки вписати його в книгу, але й оновити зміст. Якщо змістів кілька — оновлювати потрібно кожен.
Основні недоліки:
- ⚠️ Сповільнення INSERT/UPDATE/DELETE — кожен індекс потрібно оновлювати
- ⚠️ Займають місце на диску — іноді більше, ніж сама таблиця
- ⚠️ Потребують обслуговування — потрібно слідкувати за їх станом
Приклад:
-- Таблиця з 5 індексами
-- При вставці 1 рядка:
-- 1. Записуємо рядок в таблицю
-- 2. Оновлюємо індекс #1
-- 3. Оновлюємо індекс #2
-- 4. Оновлюємо індекс #3
-- 5. Оновлюємо індекс #4
-- 6. Оновлюємо індекс #5
-- Разом: 6 операцій замість 1!
Правило: Створюйте індекси лише тоді, коли вони дійсно потрібні.
🟡 Middle Level
Write Amplification (Підсилення запису)
Кожен індекс збільшує кількість операцій запису:
1 INSERT в таблицю
= 1 запис в таблицю
+ N записів в індекси
+ Запис в WAL (Write Ahead Log)
Приклад впливу на продуктивність:
| Кількість індексів | INSERT latency | WAL об’єм |
|---|---|---|
| 1 | 1 ms | 1x |
| 5 | 5-10 ms | 5x |
| 10 | 20-50 ms | 10x |
Блокування HOT-оновлень
HOT (Heap Only Tuple) — оптимізація PostgreSQL, що дозволяє оновлювати рядок без оновлення індексів.
Heap (купа) — основне сховище таблиці, де лежать самі рядки даних. Індекс містить лише ключі + посилання (TID) на Heap. При HOT нова версія рядка записується на ту ж сторінку Heap з тим самим TID — індекси досі валідні, їх не потрібно чіпати.
-- Таблиця з індексом на status
CREATE INDEX idx_orders_status ON orders(status);
-- ❌ Цей UPDATE повинен оновити індекс
UPDATE orders SET status = 'SHIPPED' WHERE id = 123;
-- ✅ Цей UPDATE може використати HOT (status не індексований)
UPDATE orders SET note = 'New note' WHERE id = 123;
Умова для HOT:
- Змінювані колонки не індексовані
- На сторінці Heap є вільне місце
Роздування (Index Bloat)
Індекси схильні до фрагментації більше, ніж таблиці:
-- Проблема: Page Splits при вставці в середину
-- Індекс по email: 'A...', 'B...', 'C...'
-- Вставляємо 'B...' → сторінка переповнюється → Split
-- Рішення: fillfactor
CREATE INDEX idx_orders_status ON orders(status)
WITH (fillfactor = 70);
-- 30% сторінки вільно → менше сплітів
VACUUM не повертає місце ОС:
VACUUMпозначає місце як вільне для нових записів- Фізичний розмір файлу не зменшується
- Для стиснення потрібен
REINDEX
Конкуренція за RAM
-- Перевірка розміру індексів
SELECT
indexrelname,
pg_size_pretty(pg_relation_size(indexrelid)) as size
FROM pg_stat_user_indexes
WHERE relname = 'orders'
ORDER BY pg_relation_size(indexrelid) DESC;
-- Якщо сумарний розмір індексів > доступної RAM
-- → Cache Misses → Random I/O → падіння продуктивності
Час планування запиту
Оптимізатор розглядає всі доступні індекси:
- 5-10 індексів → планування < 1 ms
- 20-30 індексів → планування 10-50 ms
- 100+ індексів → планування > 100 ms!
Коли НЕ варто створювати індекс
- Таблиця < 10 000 рядків (Seq Scan швидше)
- Колонка з низькою кардинальністю (наприклад, стать: M/F)
- Поле, яке рідко в WHERE/JOIN
- Таблиця з дуже інтенсивним записом
🔴 Senior Level
Write Amplification та WAL-логі
Детальний вплив на I/O:
Операція INSERT без індексів:
├── Heap page записів: 1
├── WAL записів: 1 page
└── I/O операцій: 1-2
Операція INSERT з 10 індексами:
├── Heap page записів: 1
├── Index page записів: 10
├── WAL записів: 11 pages
├── Можливі Page Splits: +10-20 записів
└── I/O операцій: 15-30
Вплив на Replica Lag:
- Кожен індекс генерує WAL-записи
- Репліка повинна програти ВСІ записи
- При 10+ індексах: WAL volume збільшується в 10-15 разів
- Репліка не встигає → Replica Lag росте
- Критично для систем з Read Replicas
Блокування HOT-оновлень: Глибокий аналіз
Механізм HOT:
-- Сторінка Heap (fillfactor = 100, немає місця)
┌─────────────────────────────────┐
│ Row1 | Row2 | Row3 | ... | Full│
└─────────────────────────────────┘
-- UPDATE Row2 без HOT:
-- 1. Створюємо нову версію Row2 в іншій сторінці
-- 2. Оновлюємо ВСІ індекси (новий TID)
-- 3. Стару версію позначаємо dead
-- UPDATE Row2 з HOT (є місце на сторінці):
-- 1. Створюємо нову версію Row2 поруч
-- 2. Оновлюємо лише Heap (індекси не чіпаємо!)
-- 3. Стару версію позначаємо dead
-- 4. Індекси досі валідні (TID не змінився)
Моніторинг HOT:
-- Перевірка ефективності HOT
SELECT
schemaname,
relname,
n_tup_hot_update, -- HOT оновлення
n_tup_upd, -- Всього оновлень
ROUND(100.0 * n_tup_hot_update / NULLIF(n_tup_upd, 0), 2) as hot_ratio
FROM pg_stat_user_tables
WHERE n_tup_upd > 0
ORDER BY hot_ratio DESC;
-- hot_ratio > 70% → відмінно (типове значення для здорової OLTP-нагрузки)
-- hot_ratio < 20% → проблема: більшість оновлень змушені оновлювати індекси,
-- що вказує на надмірне індексування або занадто маленький fillfactor
Index Bloat: Діагностика та лікування
Причини Bloat:
- Page Splits — вставка в середину відсортованого списку
- Видалення — сторінки залишаються напівпорожніми
- Оновлення індексованих колонок — нові версії, старі dead
Діагностика:
-- 1. Перевірка через pgstattuple
CREATE EXTENSION pgstattuple;
SELECT
index_name,
avg_leaf_density, -- Норма: 70-90%
leaf_fragmentation -- Норма: < 20%
FROM pgstatindex('idx_orders_email');
-- 2. Порівняння розміру з очікуваним
SELECT
schemaname,
relname as table_name,
indexrelname as index_name,
pg_size_pretty(pg_relation_size(indexrelid)) as actual_size,
idx_scan,
-- Якщо розмір підозріло великий → Bloat
CASE
WHEN pg_relation_size(indexrelid) > 1000000000 -- > 1 ГБ
AND idx_scan < 100
THEN 'POSSIBLE BLOAT'
ELSE 'OK'
END as status
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY pg_relation_size(indexrelid) DESC;
Лікування:
-- 1. REINDEX (блокує запис!)
REINDEX INDEX idx_orders_email;
-- 2. REINDEX CONCURRENTLY (не блокує, PG 12+)
REINDEX INDEX CONCURRENTLY idx_orders_email;
-- 3. pg_repack (без блокувань, розширення)
-- Встановлення: CREATE EXTENSION pg_repack;
-- Запуск: pg_repack -t orders -d mydb
Конкуренція за Shared Buffers
Математика:
Дано:
- RAM: 64 ГБ
- shared_buffers: 16 ГБ (25%)
- Таблиця: 10 ГБ
- Індекси: 20 ГБ
Проблема:
- Таблиця + Індекси = 30 ГБ > 16 ГБ shared_buffers
- Частина індексів витісняється на диск
- Cache Miss rate > 40%
- Random I/O замість читання з RAM
Рішення:
- Видалити невикористовувані індекси → 8 ГБ
- Збільшити shared_buffers до 32 ГБ
- Cache Hit rate > 95%
Моніторинг Cache Hit:
-- Перевірка ефективності кешу
SELECT
sum(heap_blks_read) as heap_read,
sum(heap_blks_hit) as heap_hit,
ROUND(100.0 * sum(heap_blks_hit) /
NULLIF(sum(heap_blks_read) + sum(heap_blks_hit), 0), 2)
as cache_hit_pct
FROM pg_statio_user_tables;
-- > 99% → відмінно
-- 95-99% → нормально
-- < 95% → проблема (мало RAM або занадто багато індексів)
Planning Time Overhead
-- Вмикаємо логування часу планування
SET log_min_duration_statement = 0;
SET log_parser_stats = on;
-- У логах побачимо:
-- duration: 0.123 ms parse
-- duration: 0.456 ms plan
-- duration: 12.345 ms execute
-- Якщо plan > execute → занадто багато індексів/таблиць
geqo_threshold: При > 12 таблиць в запиті вмикається генетичний алгоритм (GEQO), який може обрати неоптимальний план.
Production Experience
Реальний сценарій #1: Bloat вбиває продуктивність
- SaaS платформа: 100 млн користувачів
- Індекс на email: 8 ГБ (має бути 2 ГБ)
- Симптом: Пошук по email > 500ms, Cache Hit rate 60%
- Причина: 2 роки оновлень без REINDEX → Bloat 75%
- Рішення:
REINDEX INDEX CONCURRENTLY idx_users_email - Результат: Індекс став 2.1 ГБ, пошук < 5ms, Cache Hit 98%
Реальний сценарій #2: Replica Lag через індекси
- E-commerce: 15 індексів на orders (200 млн рядків)
- Проблема: Replica Lag > 120 секунд
- Аналіз: WAL volume 1 ГБ/хвилину, репліка не встигає
- Рішення:
- Видалили 10 невикористовуваних індексів
- WAL volume впав до 200 МБ/хвилину
- Replica Lag < 5 секунд
Monitoring Dashboard
-- 1. Top індекси за розміром
SELECT
indexrelname,
pg_size_pretty(pg_relation_size(indexrelid)) as size,
idx_scan
FROM pg_stat_user_indexes
ORDER BY pg_relation_size(indexrelid) DESC
LIMIT 10;
-- 2. Невикористовувані індекси (кандидати на видалення)
SELECT
relname as table_name,
indexrelname as index_name,
pg_size_pretty(pg_relation_size(indexrelid)) as size,
idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND NOT indisunique -- не UNIQUE!
ORDER BY pg_relation_size(indexrelid) DESC;
-- 3. HOT ratio по таблицях
SELECT
relname,
n_tup_hot_update,
n_tup_upd,
ROUND(100.0 * n_tup_hot_update / NULLIF(n_tup_upd, 0), 2) as hot_pct
FROM pg_stat_user_tables
WHERE n_tup_upd > 1000
ORDER BY hot_pct ASC
LIMIT 10;
-- 4. Fill factor перевірка
SELECT
c.relname as index_name,
c.reloptions
FROM pg_class c
JOIN pg_index i ON i.indexrelid = c.oid
WHERE c.reloptions::text LIKE '%fillfactor%'
AND c.relkind = 'i';
Best Practices
- Моніторте HOT ratio — ціль > 70%
- Перевіряйте Bloat регулярно через
pgstattuple - REINDEX CONCURRENTLY раз на квартал для гарячих індексів
- Видаляйте невикористовувані —
idx_scan = 0за місяць → кандидат на видалення - fillfactor = 70-80 для часто оновлюваних індексів
- Слідкуйте за Replica Lag після додавання індексів
- Обмежте кількість — максимум 5-7 індексів на таблицю
- Cache Hit Rate > 95% — якщо менше, зменшуйте індекси або додавайте RAM
Резюме для Senior
- Індекси — це податок на запис (Write Amplification, WAL, Replica Lag)
- HOT-update ratio < 20% → занадто багато індексів
- Bloat неминучий — лікуйте через
REINDEX CONCURRENTLY - Shared Buffers конкуренція — індекси витісняють дані з кешу
- Planning Time росте з кількістю індексів
- Моніторте:
pgstattuple,pg_stat_user_indexes, HOT ratio, Cache Hit rate - Видаляйте безжально — якщо індекс не використовується місяць, він не потрібен
🎯 Шпаргалка для співбесіди
Обов’язково знати:
- Write Amplification: 1 INSERT = 1 запис в таблицю + N записів в індекси + WAL
- HOT (Heap Only Tuple): оптимізація UPDATE без оновлення індексів (якщо колонка не індексована)
- HOT ratio > 70% → відмінно, < 20% → занадто багато індексів
- Index Bloat: Page Splits → фрагментація →
REINDEX CONCURRENTLY - VACUUM НЕ повертає місце ОС → REINDEX для стиснення
- Shared Buffers конкуренція: індекси витісняють дані → Cache Miss → Random I/O
- Planning Time: 5-10 індексів < 1ms, 20-30 → 10-50ms, 100+ → > 100ms
Часті уточнюючі питання:
- «Чому 15 індексів — це погано?» → Replica Lag, Write Amplification, Cache Miss
- «Як перевірити Bloat індекса?» →
pgstattuple, avg_leaf_density < 50% → REINDEX - «Що таке HOT і чому це важливо?» → UPDATE без оновлення індексів → швидше
- «Чому Cache Hit Rate падає з ростом індексів?» → Індекси витісняють дані з RAM
Червоні прапорці (НЕ говорити):
- ❌ «Чим більше індексів, тим краще» (Write Amplification!)
- ❌ «VACUUM стискає файл індекса» (ні, REINDEX)
- ❌ «HOT — це якась магія» (UPDATE без оновлення індексів!)
- ❌ «Planning Time не важливий» (100+ індексів → > 100ms планування)
Пов’язані теми:
- [[Для чого потрібні індекси]] → коли створювати
- [[Як працює B-tree індекс]] → Bloat, Page Splits
- [[Що таке складений індекс]] → консолідація індексів
- [[Як оптимізувати повільні запити]] → баланс індексів