Как неиндексированный столбец убил нашу базу данных
Один пропущенный индекс превратил API с откликом в 8 мс в восьмисекундный кошмар.
Перед крупным релизом мы провели нагрузочное тестирование. В локальной среде всё работало отлично на 100 записях. Затем мы симулировали 500 пользователей на 1,5 миллионах строк, соответствующих объему реальных данных.
Результаты были плохими:
- Время отклика API подскочило с 45 мс до 8 000 мс.
- Загрузка процессора базы данных достигла 100 %.
- Пулы соединений исчерпаны, что привело к ошибкам по таймауту.
Мы проверили лог медленных запросов (slow-query log) и обнаружили эндпоинт, который извлекает историю заказов по ID пользователя и статусу.
Запуск EXPLAIN ANALYZE в PostgreSQL показал Sequential Scan. Движок считывал все 1,5 миллиона строк для каждого запроса, так как для user_id не было индекса.
При 100 одновременных запросах система сканировала 150 миллионов строк за раз.
Исправление заняло пять минут.
Вместо простого индекса по user_id мы создали составной индекс на (user_id, status, created_at DESC). Это позволило базе данных:
- Фильтровать по
user_id. - Фильтровать по
status. - Мгновенно возвращать самые новые строки.
- Пропускать лишние этапы сортировки.
Мы использовали CREATE INDEX CONCURRENTLY, чтобы таблица оставалась доступной (не блокировалась) во время операции.
После исправления:
- Время выполнения запроса упало с 8 150 мс до 0,14 мс.
- Задержка API снизилась с 8 секунд до 12 мс.
- Загрузка процессора упала со 100 % до менее чем 8 %.
Извлеченные уроки:
- Локальное тестирование может вводить в заблуждение: 100 строк не заменят миллион.
- Индексируйте внешние ключи — большинство ORM этого не делают.
- Запускайте
EXPLAIN ANALYZE, прежде чем покупать новое оборудование. - Проектируйте индексы под те запросы, которые вы действительно выполняете.
Не масштабируйте серверы в первую очередь. Масштабируйте запросы.
