Как я ускорил API в 20 раз одним индексом: разбор и замеры

AnnaStyle

New member
Привет, форумчане. Хочу рассказать историю, которая лишний раз доказала мне старую истину: самая дорогая оптимизация — та, которую делают наугад, а самая дешёвая — та, которую подсказывает лог медленных запросов. У нас был сервис заказов на Node и PostgreSQL, и в один прекрасный понедельник p95 на эндпоинте списка заказов подскочил до 1,2 секунды. Поддержка тонула в жалобах, дашборд горел красным, а я шёл копать.

Первым делом я не стал трогать код, а полез в pg_stat_statements. Это, пожалуй, лучший бесплатный детектив, который у меня был. Отсортировал по total_exec_time — и увидел запрос, который съедал почти 60 процентов времени базы: выборка последних заказов конкретного пользователя с фильтром по статусу оплаты. Запрос вызывался на каждом открытии личного кабинета, то есть десятки тысяч раз в час.

Дальше — EXPLAIN ANALYZE, и картина стала предельно ясной. Планировщик честно сказал: Seq Scan по таблице orders, 41 миллион строк, фильтр по user_id, а потом ещё и сортировка по created_at. То есть база каждый раз читала гигабайты с диска, чтобы отдать двадцать строк. Индексы на таблице были, но на user_id — одиночный. Этого хватало, чтобы найти строки, но не хватало, чтобы отдать их в нужном порядке без сортировки в памяти.

И тут случилась та самая магия, ради которой я и пишу этот пост. Я добавил один составной индекс по user_id и created_at в порядке убывания. Никакого рефакторинга, никакого кэша, никаких новых серверов. Просто одна строка в миграции — и время выполнения упало с 1,2 секунды до 58 миллисекунд. Планировщик перестал читать всю таблицу: Index Scan сразу отдавал нужные строки в правильном порядке, а сортировка исчезла как класс. По моим замерам это ровно те самые двадцать раз, о которых я написал в заголовке.

Отдельно расскажу про грабли, потому что без них такая история была бы слишком сладкой. В продакшене нельзя просто так взять и выполнить CREATE INDEX на горячей таблице — это блокировка и остановка записи. Я делал всё через CREATE INDEX CONCURRENTLY, вне транзакции, ночью, с контролем нагрузки. Плюс помнил, что индексы не бесплатны: каждый новый индекс замедляет вставки и обновления и занимает диск. Поэтому в тот же вечер я вычистил три неиспользуемых индекса, которые висели в базе просто по инерции.

Что я советую читателям, которые столкнутся с тем же. Сначала измеряйте, а потом оптимизируйте: без pg_stat_statements и EXPLAIN ANALYZE вы будете лечить симптомы, а не болезнь. Стройте составные индексы в порядке условий, а не наоборот. Помните про низкую кардинальность: индекс по полю вроде статуса с тремя значениями почти никогда не окупается. И обязательно сравнивайте p95 и p99 до и после, а не только среднее — оно врёт.

Отдельно добавлю про культуру работы с базой в команде. Мы завели простое правило: любая миграция с индексом идёт через ревью с обязательным EXPLAIN на копии продакшена. Это заняло у нас минут двадцать на настройку реплики, зато спасло от нескольких ночных деплоев с сюрпризами. И ещё один момент, который многие недооценивают: следите за тем, чтобы запросы в коде и в ORM реально попадали в индекс. Иногда индекс идеальный, а код добавляет лишний ORDER BY или функцию в WHERE — и всё летит в мусор.

А теперь вопрос к вам, коллеги: какой самый неожиданный и дешёвый по усилию ускор, который вы делали в своей практике, и что именно вас на него натолкнуло — метрика, жалоба пользователя или чистая случайность? Делитесь историями, уверен, у каждого есть такой свой маленький индекс на миллион.
 
Назад
Вверх