Два одинаковых с виду запроса: один отвечает мгновенно, другой — висит и кладёт стенд. Разница почти всегда в одном слове: индекс. Понимать индексы «на пальцах» полезно каждому, кто пишет запросы: это объясняет и медленный поиск в приложении, и то, почему ваш собственный невинный SELECT вдруг стал проблемой.
Полный перебор: как база ищет без индекса
Выполняя SELECT * FROM orders WHERE customer_id = 42, база по умолчанию делает ровно то, что делали бы вы с бумажной книгой без оглавления: читает всё подряд и откладывает подходящее. Это называется полным перебором (sequential scan). На тысяче строк он незаметен, на ста миллионах — это минуты работы дисков.
Важное следствие: на маленьком тестовом стенде всё летает всегда. Медленные запросы — болезнь больших объёмов, поэтому проблемы производительности не видны на стенде с сотней записей и внезапно расцветают на проде.
Индекс — оглавление таблицы
Индекс — дополнительная структура рядом с таблицей: отсортированный список значений столбца со ссылками на строки. Точная аналогия — предметный указатель в конце книги: не листать все страницы, а найти слово в алфавитном списке и перейти сразу на нужную.
Есть индекс по customer_id — запрос WHERE customer_id = 42 находит строки за доли секунды независимо от размера таблицы. Нет — полный перебор.
Почему бы не проиндексировать всё? У индекса есть цена: он занимает место и замедляет запись — каждый INSERT и UPDATE должен обновить не только таблицу, но и все её индексы. Поэтому индексы заводят точечно: на первичном ключе он есть всегда, на внешних ключах и полях частого поиска — по решению разработчиков.
Когда индекс есть, но не работает
Коварный класс ситуаций — индекс существует, а запрос всё равно перебирает всю таблицу:
- Функция над столбцом.
WHERE lower(email) = 'anna@...'— индекс построен поemail, а сравниваетсяlower(email), для базы это другое выражение. Тот же эффект у арифметики:WHERE amount * 100 > 5000. - LIKE с процентом в начале.
LIKE 'anna%'индексом пользуется (как поиск по первым буквам в алфавитном указателе), аLIKE '%@gmail.com'— нет: по «окончанию слова» оглавление не работает. - Слабая избирательность.
WHERE status = 'paid', когда оплачены 90 % заказов: проще прочитать всю таблицу, чем прыгать по ссылкам на почти каждую строку. База сама выбирает перебор — и это правильное решение.
EXPLAIN: спросить базу, как она будет искать
Гадать не нужно — базу можно спросить. Добавьте перед запросом слово EXPLAIN:
EXPLAIN SELECT * FROM orders WHERE customer_id = 42;
Вместо данных база покажет план: как она собирается выполнять запрос. Читать планы глубоко — отдельное искусство, но первый уровень доступен сразу: увидели Seq Scan (последовательный перебор) на большой таблице — вот причина тормозов; Index Scan — работает индекс. Этого достаточно, чтобы разговор с разработчиком начался не с «что-то тормозит», а с «поиск заказов по номеру телефона идёт полным перебором, таблица 40 миллионов строк».
Где это применяется
Это знание работает в обе стороны. Внутрь — про ваши запросы: тяжёлый SELECT без ограничений по огромной таблице на общем стенде мешает всем; добавьте LIMIT и условия по индексированным полям. Наружу — про продукт: «поиск в приложении отвечает 40 секунд» — это дефект производительности, и после EXPLAIN вы опишете его на уровне причины, а не симптома. А фраза разработчика «там нет индекса» перестанет быть заклинанием — вы понимаете, что она значит и чем грозит.
Где спотыкаются начинающие:
- Меряют скорость на пустом стенде. Сто строк летают без всяких индексов; выводы о производительности с маленькой базы не переносятся на большую.
- Считают, что индекс ускоряет всё. Индекс помогает конкретным условиям поиска — и берёт плату замедлением записи. «Добавьте индексов» — не универсальный рецепт.
- Пишут условия, убивающие индекс, — функции над столбцом, LIKE с процентом в начале — и удивляются перебору.
Что учить дальше. Цикл «SQL с нуля» пройден. Практическое применение в работе тестировщика — в статье SQL для тестировщика. А когда захочется глубины — раздел PostgreSQL: транзакции и изоляция, устройство индексов, EXPLAIN всерьёз.