Индексы в 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 = 1WHERE 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
Классические ошибки
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).