Индексы в SQL: шпаргалка для аналитика

Зачем аналитику знать индексы

Аналитик не создаёт индексы (это работа DBA), но должен понимать:

  • Почему запрос работает 3 секунды вместо 0.03.
  • Когда просить у DBA добавить индекс.
  • Как читать EXPLAIN.

На собесе индексы спрашивают на middle-позициях, особенно если работа связана с большими хранилищами.

Что такое индекс

Индекс — отдельная структура, которая ускоряет поиск в таблице. Аналог алфавитного указателя в книге: вместо листания всех страниц вы идёте сразу на нужную.

Trade-off:

  • ✅ Чтение (SELECT) — быстрее.
  • ❌ Запись (INSERT, UPDATE, DELETE) — медленнее (нужно обновлять индекс).
  • ❌ Занимает место на диске.

Вывод: индексы ставят только там, где они реально нужны.

Типы индексов

B-tree (по умолчанию)

Самый распространённый. Поддерживает:

  • Равенство: WHERE id = 123
  • Диапазон: WHERE created_at BETWEEN '2026-01-01' AND '2026-02-01'
  • Сортировка: ORDER BY created_at
  • Префикс: WHERE name LIKE 'Иван%' (но не '%Иван')
CREATE INDEX idx_orders_user ON orders(user_id);
CREATE INDEX idx_orders_date ON orders(created_at);

Hash

Только для равенства (=). Не поддерживает диапазоны и сортировку. Используется редко, на специфичных задачах.

CREATE INDEX idx_users_email_hash ON users USING HASH (email);

GIN / GiST (PostgreSQL)

Для полнотекстового поиска, JSON, массивов.

CREATE INDEX idx_tags ON products USING GIN (tags);
-- Теперь быстро: SELECT * FROM products WHERE tags @> ARRAY['скидка'];

Composite (составной)

Индекс по нескольким столбцам:

CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);

Важно: порядок столбцов имеет значение. Индекс (user_id, created_at) работает для:

  • WHERE user_id = 1
  • WHERE user_id = 1 AND created_at > '2026-01-01'
  • WHERE user_id = 1 ORDER BY created_at

Но не работает для:

  • WHERE created_at > '2026-01-01' (нет user_id)

Правило: чаще используемый столбец — слева.

Partial (частичный)

Индекс только на часть таблицы:

CREATE INDEX idx_orders_pending ON orders(user_id) WHERE status = 'pending';

Полезно, когда 95% строк имеют status='paid', а ищем по pending. Индекс будет маленьким и быстрым.

Unique

Гарантирует уникальность:

CREATE UNIQUE INDEX idx_users_email ON users(email);

Заодно ускоряет поиск — используется для первичных и уникальных ключей.

Covering (покрывающий)

Индекс содержит все нужные столбцы, СУБД не лезет в таблицу:

CREATE INDEX idx_orders_covering ON orders(user_id) INCLUDE (amount, status);

Запрос SELECT amount, status FROM orders WHERE user_id = 1 отработает только по индексу.

Попробовать силы на подобных вопросах проще всего в тренажёре Карьерник — прямо в Telegram, без регистрации через сайт.

Когда индекс не помогает

1. Функции над индексированным столбцом

-- ❌ Индекс по email не используется
SELECT * FROM users WHERE LOWER(email) = 'ivan@example.com';

-- ✅ Использует индекс
SELECT * FROM users WHERE email = 'ivan@example.com';

-- ✅ Решение — функциональный индекс
CREATE INDEX idx_users_email_lower ON users(LOWER(email));

2. LIKE с префиксом %

-- ❌ Не использует индекс
WHERE name LIKE '%Иван%'

-- ✅ Использует
WHERE name LIKE 'Иван%'

3. OR с разными столбцами

-- Часто не использует индекс
WHERE user_id = 1 OR email = 'ivan@example.com'

-- Решение — UNION
SELECT * FROM users WHERE user_id = 1
UNION
SELECT * FROM users WHERE email = 'ivan@example.com';

4. NOT / !=

Оптимизатор обычно предпочитает full scan. Если неравенство критично — partial index поможет.

5. Маленькая таблица

Если в таблице 100 строк, full scan быстрее похода в индекс.

EXPLAIN — как проверить

EXPLAIN SELECT * FROM orders WHERE user_id = 1;
-- Smth like:
-- Index Scan using idx_orders_user ...  ← индекс работает
-- vs
-- Seq Scan on orders ...                ← full scan, плохо

EXPLAIN ANALYZE — с реальным выполнением и временем:

EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 1;
-- Execution Time: 0.234 ms

Подробнее про EXPLAIN.

Классические ошибки

1. Слишком много индексов

Каждый INSERT обновляет все индексы. На большой таблице с 10+ индексами запись становится очень медленной. Правило: 3-5 индексов на таблицу, редко больше.

2. Индекс на неуникальный столбец с плохой selectivity

Индекс на gender (2 значения) или country (10 значений) обычно бесполезен — СУБД всё равно читает 50% таблицы. Индексы работают там, где selectivity высокая (>10% уникальных значений).

3. Забытый индекс на FK

CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id)  -- нет индекса!
);

В PostgreSQL индекс на FK не создаётся автоматически. JOIN ... ON o.user_id = u.id будет работать через full scan.

4. Не использовать composite index

-- Два отдельных индекса
CREATE INDEX idx_user ON orders(user_id);
CREATE INDEX idx_date ON orders(created_at);

-- Запрос WHERE user_id=1 AND created_at>... использует только один
-- Композитный (user_id, created_at) отработает оба условия

10 задач с собесов

1. Какой индекс ставить на таблицу orders?

Стандартный набор:

  • PRIMARY KEY (id) — автоматический
  • INDEX(user_id) — для поиска по пользователю
  • INDEX(created_at) — для временных фильтров
  • INDEX(user_id, created_at) — для комбинированных запросов

2. Почему WHERE LOWER(email) = 'x' медленный?

Индекс на email не используется — нужен функциональный (LOWER(email)).

3. Как проверить, использует ли запрос индекс?

Через EXPLAIN. Ищем «Index Scan» — хорошо, «Seq Scan» — индекс не используется.

4. Composite index (a, b, c) работает для WHERE b=1?

Нет. Composite эффективен, только если используется leftmost prefix — (a), (a, b), (a, b, c). Только (b) или (c) не работает.

5. Какие индексы нужны для JOIN

По ключу JOIN: если ON a.user_id = u.id, индекс нужен на a.user_id (часто забывают).

6. Почему индекс на gender бесполезен?

Low selectivity: только 2-3 уникальных значения. Partial index полезнее: INDEX(user_id) WHERE gender = 'rare_value'.

7. Чем отличается UNIQUE от PRIMARY KEY?

PRIMARY KEY = UNIQUE + NOT NULL + только один на таблицу. UNIQUE может быть несколько, может содержать NULL.

8. Когда использовать covering index?

Когда запрос часто читает небольшой набор столбцов + фильтр: SELECT amount FROM orders WHERE user_id = X. Covering index (user_id) INCLUDE (amount) читает только индекс.

9. Индекс на created_at::date vs created_at?

Если в запросах WHERE created_at::date = '2026-04-15' — нужен функциональный индекс. Индекс просто на created_at не сработает из-за функции.

10. Как ускорить SELECT COUNT(*) FROM orders?

Никак, если нужен точный COUNT. B-tree не хранит количество в корне. Альтернативы:

  • Приближённый COUNT из статистики таблицы (быстро, но примерно).
  • Триггер на счётчик в отдельной таблице.
  • Partial index и COUNT по нему, если есть подходящий фильтр.

Пройти 30–50 задач по теме за вечер можно в Telegram-тренажёре. Это то, что отличает «знаю» от «уверенно отвечу на собесе».

Как тренироваться

Индексы учатся на практике — создайте таблицу с миллионом строк и измеряйте разницу между запросом с индексом и без. Это гораздо понятнее любой теории.

Тренажёр Карьерник содержит блок вопросов по производительности и индексам — что ставить, когда не использовать, как читать EXPLAIN.

Совет: на собесе, обсуждая производительность, всегда упоминайте «в зависимости от selectivity и размера таблицы». Универсальных правил мало — зрелость в том, чтобы видеть контекст.

Читайте также

FAQ

Нужно ли аналитику самому создавать индексы?

Обычно нет — это работа DBA. Но знать, что просить — обязательно. Фраза «этот запрос медленный, можно добавить индекс на orders(user_id, created_at)?» экономит DBA часы расследования.

Сколько индексов ставить на таблицу?

3-5 обычно достаточно. Больше 10 — почти всегда overkill и убивает запись. Аудит индексов на «неиспользуемые» — полезная практика.

Hash index или B-tree?

B-tree почти всегда. Hash поддерживает только =, не ORDER BY, не диапазоны. Редко даёт ощутимое преимущество. B-tree универсальнее.

Что быстрее — UNIQUE index или PRIMARY KEY?

Технически одно и то же — PRIMARY KEY создаёт UNIQUE index под капотом. Разница только семантическая и в констрейнтах (PK не может быть NULL).