🗄️ Розділ 1 · Питання #4

Коли варто створювати індекс?

Створюйте індекс, коли пошук по таблиці став повільним і ви розумієте, які поля використовуються для пошуку.


🟢 Junior Level

Створюйте індекс, коли пошук по таблиці став повільним і ви розумієте, які поля використовуються для пошуку.

Проста аналогія: Якщо ви часто шукаєте контакт у телефонній книзі за номером телефону, має сенс створити окремий покажчик «за номерами».

Основна ідея:

  • Індекс потрібен для прискорення пошуку
  • Створюйте індекси для полів у WHERE, JOIN, ORDER BY
  • Не створюйте індекси «про всяк випадок»

Приклад:

-- Повільний запит без індексу
SELECT * FROM users WHERE email = 'test@example.com';

-- Створюємо індекс
CREATE INDEX idx_users_email ON users(email);

-- Тепер запит швидкий
SELECT * FROM users WHERE email = 'test@example.com';

Коли створювати:

  • Поле часто використовується у WHERE
  • Поле використовується у JOIN для зв’язку таблиць
  • Поле використовується в ORDER BY для сортування
  • Таблиця велика (понад 10 000 рядків)

🟡 Middle Level

Як виявити потребу в індексах

1. Аналіз повільних запитів

-- Вмикаємо логування повільних запитів
-- postgresql.conf:
-- log_min_duration_statement = 1000  -- логує запити > 1 секунди

2. Розширення pg_stat_statements

-- Вмикаємо розширення
CREATE EXTENSION pg_stat_statements;

-- Знаходимо найповільніші запити
SELECT query, 
       calls,
       mean_exec_time as avg_time_ms,
       total_exec_time as total_time_ms
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

Основні критерії для створення

1. Поля у WHERE з високою селективністю

-- ✅ Висока селективність (багато унікальних значень)
CREATE INDEX idx_users_email ON users(email);

-- ❌ Низька селективність (мало унікальних значень)
CREATE INDEX idx_users_gender ON users(gender); -- M/F

2. Зовнішні ключі (Foreign Keys)

-- Рекомендується індексувати FK!
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id)  -- ← індексуйте це!
);

-- Створюємо індекс на FK
CREATE INDEX idx_orders_user_id ON orders(user_id);

-- Навіщо? При DELETE користувача PG перевіряє відсутність замовлень
-- Без індексу → Seq Scan по orders (дуже повільно на великих таблицях)

Винятки: FK-таблиця < 1000 рядків (Seq Scan швидкий), або ви ніколи не видаляєте/не оновлюєте батьківські записи.

3. Поля для JOIN

-- Для Nested Loop Join критичні індекси на полях зв'язку
SELECT * FROM orders o
JOIN users u ON o.user_id = u.id;

-- Індекси:
-- orders(user_id) ← для пошуку замовлень користувача
-- users(id) ← вже є (PRIMARY KEY)

4. Поля для ORDER BY / GROUP BY

-- Індекс дозволяє уникнути сортування
CREATE INDEX idx_orders_created ON orders(created_at DESC);

-- Запит без Sort (дані вже відсортовані)
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10;

Типові помилки

  1. Створення індексу без аналізу навантаження
    • ❌ Створили індекс «про всяк випадок»
    • ✅ Спочатку проаналізуйте запити через EXPLAIN
  2. Дублювання індексів
    -- ❌ Два однакових індекси
    CREATE INDEX idx1 ON users(email);
    CREATE INDEX idx2 ON users(email);
    
  3. Індекси на маленьких таблицях
    • Таблиця < 1000 рядків → Seq Scan швидший
    • Індекс тільки займає місце

Вартість індексу

Кожен індекс — це:

  1. Сповільнення INSERT/UPDATE/DELETE (дерево потрібно оновлювати)
  2. Місце на диску (від 10% до 100%+ від розміру таблиці)
  3. Навантаження на RAM (індекс має бути в пам’яті для ефективності)

Практичне правило

-- Перед створенням індексу перевірте:
EXPLAIN (ANALYZE, BUFFERS) 
SELECT * FROM users WHERE email = 'test@example.com';

-- Якщо бачите Seq Scan і buffer hits низький → індекс допоможе
-- Якщо вже Index Scan → можливо, індекс уже є

🔴 Senior Level

Як виявити потребу: Workload Analysis

pg_stat_statements — основний інструмент:

-- Знаходимо запити з найбільшим total_exec_time
SELECT 
    query,
    calls,
    mean_exec_time,
    total_exec_time,
    rows,
    shared_blks_read,    -- Читання з диска (погано)
    shared_blks_hit      -- Читання з кешу (добре)
FROM pg_stat_statements
WHERE dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
ORDER BY total_exec_time DESC
LIMIT 20;

-- Ключова метрика: shared_blks_read / calls > 100
-- → Запит читає багато сторінок із диска → потрібен індекс

HypoPG — гіпотетичні індекси:

-- Розширення для тестування "що якщо"
CREATE EXTENSION hypopg;

-- Створюємо віртуальний індекс (НЕ займає місце!)
SELECT * FROM hypopg_create_index(
    'CREATE INDEX idx_orders_user_date ON orders(user_id, created_at DESC)'
);

-- Перевіряємо, чи обере його планувальник
EXPLAIN SELECT * FROM orders WHERE user_id = 123 ORDER BY created_at DESC LIMIT 10;
-- У плані побачите: Index Scan using <hypothetical>...

-- Видаляємо гіпотетичний індекс
SELECT * FROM hypopg_drop_index(indexrelid);

Архітектурні Trade-offs

1. Write Amplification

Кожен новий індекс = 
  + Сповільнення INSERT/UPDATE/DELETE (дерево потрібно оновлювати)
  + Навантаження на WAL (Write Ahead Log — журнал попереднього запису: PostgreSQL спочатку логує всі зміни у WAL-файли, а потім застосовує їх до основних даних. Кожен індекс збільшує обсяг WAL, тому що зміни індексу теж потрібно залогувати)
  + Збільшення Replica Lag (репліка не встигає)
  + Bloat (роздування індексу з часом)

2. Консолідація індексів

-- ❌ Було: 3 окремі індекси
CREATE INDEX idx_a ON t(a);              -- 2 ГБ
CREATE INDEX idx_a_b ON t(a, b);         -- 5 ГБ
CREATE INDEX idx_a_b_c ON t(a, b, c);    -- 8 ГБ
-- Разом: 15 ГБ, сповільнення запису x3

-- ✅ Стало: 1 індекс покриває всі префікси
CREATE INDEX idx_a_b_c ON t(a, b, c);    -- 8 ГБ
-- Покриває: WHERE a, WHERE a,b, WHERE a,b,c
-- Економія: 7 ГБ, запис у 3 рази швидший

3. Partial Index для рідкісних значень

-- Індексуємо тільки помилки (5% від усіх записів)
CREATE INDEX idx_orders_errors ON orders(id, error_message)
WHERE status = 'ERROR';

-- Економія місця: на 95% менший за повний індекс
-- Прискорення: тільки для критичних запитів

Edge Cases

  1. Створення індексу в Production
    -- ❌ Блокує запис (ShareLock)
    CREATE INDEX idx ON orders(user_id);
       
    -- ✅ Не блокує, але виконується довше
    CREATE INDEX CONCURRENTLY idx ON orders(user_id);
       
    -- ⚠️ У разі переривання залишається INVALID
    SELECT indexrelid::regclass, indisvalid 
    FROM pg_index 
    WHERE NOT indisvalid;
    
  2. Bulk Load оптимізація
    -- При вставці мільйонів рядків:
    -- 1. Видалити індекси
    DROP INDEX idx_orders_user_id;
       
    -- 2. Вставити дані
    COPY orders FROM '/data/orders.csv';
       
    -- 3. Перестворити індекси (швидше, ніж оновлювати при кожній вставці)
    CREATE INDEX idx_orders_user_id ON orders(user_id);
    
  3. Запобігання Index Bloat
    -- Для часто оновлюваних індексів
    CREATE INDEX idx_orders_status ON orders(status) 
    WITH (fillfactor = 70);
       
    -- 30% сторінки вільно → менше сплітів
    

Продуктивність

Вплив на запис (приблизні цифри):

Кількість індексів INSERT latency WAL volume
1–2 +5–10% Базовий
5–7 +50–100% x2–x3
10–15 +200–500% x5–x10

Правило 10–15%:

  • Якщо індекс використовується < 10–15% від загальної кількості запитів → скоріш за все, не потрібен
  • Seq Scan на таблиці, яка повністю в кеші, може бути швидшим за Index Scan

Production Experience

Реальний сценарій #1:

  • Фінтех стартап: 50 млн транзакцій
  • Проблема: Запити на звіти > 30 секунд
  • Аналіз через pg_stat_statements:
    • Топ-3 запити забирають 80% часу
    • Усі 3 використовують WHERE user_id = ? AND created_at > ?
  • Рішення:
    • CREATE INDEX CONCURRENTLY idx_transactions_user_date ON transactions(user_id, created_at DESC)
    • Додали INCLUDE (amount, type) для Index-Only Scan
  • Результат: Запити стали < 100ms (прискорення в 300 разів)

Реальний сценарій #2:

  • E-commerce: 200 млн замовлень
  • Проблема: INSERT сповільнився до 2 секунд, Replica Lag > 60 секунд
  • Аналіз:
    • 18 індексів на таблиці orders
    • 12 із них використовувалися < 5 разів за місяць
    • WAL volume: 500 МБ/хвилину
  • Рішення:
    • Видалили 12 невикористовуваних індексів
    • Консолідували решту до 6
    • Результат: INSERT < 100ms, Replica Lag < 5 секунд

Monitoring

-- 1. Перевірка використання індексів
SELECT 
    schemaname,
    relname as table_name,
    indexrelname as index_name,
    idx_scan,
    idx_tup_read,
    idx_tup_fetch,
    pg_size_pretty(pg_relation_size(indexrelid)) as size
FROM pg_stat_user_indexes
WHERE schemaname = 'public'
ORDER BY idx_scan ASC  -- Невикористовувані внизу
LIMIT 20;

-- 2. Перевірка Foreign Keys без індексів
SELECT 
    conrelid::regclass AS table_with_fk,
    conname AS fk_constraint,
    confrelid::regclass AS referenced_table
FROM pg_constraint
WHERE contype = 'f'
  AND NOT EXISTS (
    SELECT 1 FROM pg_index
    WHERE indrelid = conrelid
      AND indkey[0] = ANY(conkey)
  );

-- 3. Перевірка дублікатів
-- (індекси з однаковим набором колонок)
SELECT 
    a.indexrelid::regclass as index1,
    b.indexrelid::regclass as index2,
    pg_get_indexdef(a.indexrelid) as definition
FROM pg_index a
JOIN pg_index b ON a.indrelid = b.indrelid 
  AND a.indexrelid < b.indexrelid
  AND a.indkey::text = b.indkey::text;

-- 4. HypoPG для тестування
SELECT * FROM hypopg_list_indexes();

Best Practices

  1. Аналізуйте workload через pg_stat_statements перед створенням
  2. Тестуйте гіпотези через HypoPG
  3. Рекомендується індексувати FK
  4. Використовуйте CONCURRENTLY у production
  5. Консолідуйте індекси за префіксами
  6. Видаляйте невикористовувані індекси регулярно
  7. Моніторте Replica Lag при додаванні нових індексів
  8. Для Bulk Load — видаляйте та перестворюйте індекси

Резюме для Senior

  • Використовуйте pg_stat_statements для пошуку bottleneck запитів
  • Перевіряйте гіпотези через HypoPG перед створенням реальних індексів
  • Рекомендується індексувати Foreign Keys — це часта причина Seq Scan
  • У production — тільки CONCURRENTLY
  • Консолідуйте індекси: (a), (a,b), (a,b,c) → один (a,b,c)
  • Моніторте Replica Lag та WAL volume після додавання індексів
  • Видаляйте невикористовувані індекси — кожен індекс це податок на запис

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • Аналізуйте workload через pg_stat_statements перед створенням
  • Рекомендується індексувати Foreign Keys — це часта причина Seq Scan
  • CONCURRENTLY у production (не блокує запис, але довше)
  • HypoPG — тестування «що якщо» без створення реального індексу
  • Write Amplification: кожен індекс = +INSERT/UPDATE/DELETE + WAL volume
  • Bulk Load: видаляйте індекси → вставляйте → перестворюйте (швидше)
  • fillfactor = 70-80 для часто оновлюваних індексів

Часті уточнюючі питання:

  • «Як зрозуміти, що індекс потрібен?» → shared_blks_read / calls > 100 у pg_stat_statements
  • «Що робити перед масовою вставкою?» → Видалити індекси → COPY → перестворити
  • «Як перевірити, чи допоможе індекс?» → HypoPG + EXPLAIN
  • «Що таке Replica Lag і як індекси впливають?» → Більше індексів → більше WAL → репліка відстає

Червоні прапорці (НЕ говорити):

  • ❌ «Створю індекс і подивлюся» (аналізуйте workload спочатку!)
  • ❌ «Індекси про всяк випадок не зашкодять» (Write Amplification!)
  • ❌ «У production створю індекс без CONCURRENTLY» (блокування запису!)

Пов’язані теми: