Главная/Блог/Почему тормозит база данных: индексы, N+1-запросы и оптимизация PostgreSQL в NestJS и Prisma

Почему тормозит база данных: индексы, N+1-запросы и оптимизация PostgreSQL в NestJS и Prisma

Почему тормозит база данных: индексы, N+1-запросы и оптимизация PostgreSQL в NestJS и Prisma
Сайт открывается восемь секунд, каталог листается с подтормаживаниями, а выгрузка отчёта в админке успевает уйти на обед — знакомая картина для многих владельцев интернет-магазинов и сервисов. По данным Google, при увеличении времени загрузки страницы с одной до трёх секунд вероятность отказа пользователя растёт на 32%, а каждая потерянная секунда — это прямая потеря конверсии и денег. В проектах на NestJS и Prisma узким местом почти всегда оказывается не Node.js и не сам фреймворк, а база данных PostgreSQL — точнее, то, как к ней строятся запросы. Разберём две главные причины медленной БД — отсутствие индексов и проблему N+1 — и покажем, какие шаги оптимизации дают максимальный эффект при минимальных вложениях.

Как понять, что тормозит именно база данных

Симптомы обычно узнаваемые: медленно работают страницы со списками, фильтрами и отчётами, а всё остальное летает. Проблема нарастает по мере роста базы — год назад всё было быстро, а теперь каталог грузится по три секунды. Быстрый способ подтвердить диагноз — включить расширение pg_stat_statements: оно покажет топ запросов по суммарному времени выполнения. Дополнительно выставьте параметр log_min_duration_statement = 200 — сервер начнёт писать в лог все запросы медленнее 200 миллисекунд, и картина станет наглядной уже через день работы. В NestJS-проектах на Prisma мы также включаем логирование через prisma.$on('query') — это пара строк кода, зато сразу видно, какие запросы выполняются и сколько раз.

Отсутствие индексов: поиск по миллиону строк вместо двадцати

Индекс — это, по сути, оглавление таблицы. Без него PostgreSQL выполняет последовательное сканирование (Seq Scan): читает все строки подряд. В таблице на миллион записей это миллион операций, тогда как по B-tree индексу нужная строка находится примерно за 20 переходов — разница в тысячи раз. Типичные места, где забывают индексы: поиск по email и телефону, фильтр по статусу заказа и дате, сортировка по цене, внешние ключи для JOIN. Проверить просто: выполните EXPLAIN ANALYZE для медленного запроса — если видите Seq Scan по большой таблице, индекс нужен. В Prisma индексы описываются прямо в схеме через атрибут @@index, после чего применяется миграция: это занимает минуты, а эффект виден сразу.

N+1 запросов: как 100 товаров превращаются в 101 запрос к базе

Классическая ошибка: код делает один запрос за списком заказов, а затем в цикле — по одному запросу на каждый заказ, чтобы получить клиента, состав или статус. Итог — N+1: для страницы со 100 позициями выполняется 101 запрос вместо одного-двух. Каждый запрос — это сетевой round-trip: даже при 5 миллисекундах на запрос страница собирается полсекунды, а под нагрузкой пул соединений забивается, и время ответа растёт лавинообразно. В Prisma это случается, когда разработчик обращается к связанным данным в цикле — например, вызывает findUnique для каждого элемента списка вместо одной выборки с include. Хорошая новость: N+1 легко находится по логам и так же легко чинится.

Чек-лист: как найти и устранить N+1 в Prisma

Мы используем простой алгоритм, который работает в любом проекте на NestJS и Prisma:
  1. Включите логирование запросов в PrismaService при инициализации приложения — в консоли появятся все SQL-запросы.
  2. Откройте медленную страницу и посмотрите лог: серия одинаковых запросов, отличающихся только значением id, — это и есть N+1.
  3. Замените запросы в цикле на жадную загрузку через include — Prisma соберёт связанные данные за один-два запроса.
  4. Ограничьте выборку через select: не тяните все поля, особенно тяжёлые тексты и JSON, если они не нужны на странице.
  5. Проверьте результат на реалистичном объёме данных — на тестовой базе из 50 строк проблема незаметна в принципе.
После такого рефакторинга страницы списков обычно ускоряются в 5–10 раз.

Индексы в Prisma: какие бывают и когда их создавать

Схема Prisma позволяет описать все нужные индексы декларативно:
  • @@index([поле]) — базовый B-tree индекс для фильтрации, поиска и сортировки;
  • @unique и @@unique — гарантируют уникальность email или телефона и одновременно ускоряют поиск;
  • составной @@index([status, createdAt]) — для фильтров по нескольким полям сразу, порядок колонок в нём важен;
  • GIN-индекс — для поиска по JSONB и полнотекстового поиска.
Важно не переборщить: каждый индекс ускоряет чтение, но замедляет запись — INSERT и UPDATE выполняются дольше. Правило простое: индексируем поля, которые реально участвуют в WHERE, ORDER BY и JOIN, а неиспользуемые индексы отслеживаем через pg_stat_user_indexes.

Настройка PostgreSQL: быстрые победы без переписывания кода

Даже идеальные запросы работают плохо на неправильно настроенном сервере. Базовый минимум: shared_buffers около 25% оперативной памяти, work_mem с запасом для сортировок и отчётов, актуальная версия PostgreSQL — каждая мажорная версия даёт ощутимый прирост производительности. Отдельная боль современных проектов — соединения: если сервисов и serverless-функций много, они исчерпывают лимит подключений. Решение — пулер PgBouncer или managed-решения вроде Prisma Accelerate, плюс параметр connection_limit в строке подключения. На проектах с активной записью стоит проверить автовакуум: недонастроенный autovacuum раздувает таблицы и замедляет все запросы подряд.

Типичные ошибки в коде на NestJS и Prisma

Индексы и настройки — половина дела, вторая половина — как написаны сами сервисы. Самые частые проблемы:
  • запросы к базе внутри циклов forEach в NestJS-сервисах — прямой путь к N+1;
  • findMany() без пагинации take/skip: из базы выгружаются все строки, а фильтрация делается на стороне Node.js;
  • тяжёлые поля по умолчанию — без select тянутся все колонки, включая огромные тексты и JSON;
  • агрегации на каждый запрос — суммы и статистика пересчитываются заново вместо кэша или материализованных представлений;
  • offset-пагинация на больших выборках — база каждый раз вынуждена пропускать тысячи строк.
Каждая из этих ошибок не видна на демо-данных, но проявляется, когда база вырастает до десятков и сотен тысяч записей.

Правильный порядок работы: сначала измерить, потом ускорять

Главная ошибка при оптимизации — действовать наугад и «на всякий случай» добавить индексов. Дальше — точечные исправления: индексы под топовые фильтры, разворачивание N+1, пагинация, настройка пула соединений. Затем контрольный замер. В типичном интернет-магазине на Prisma первые три-четыре оптимизации сокращают время ответа API с 2–3 секунд до 150–300 миллисекунд — это видно и в Core Web Vitals, и в конверсии.
Оптимизация базы данных — не разовая акция, а регулярная практика: данные растут, новые фичи добавляют новые запросы, и то, что было быстрым год назад, сегодня упирается в Seq Scan. Закладывайте мониторинг медленных запросов с самого старта проекта и проводите аудит производительности хотя бы раз в полгода. Чаще всего два-три дня работы с индексами и запросами дают больший прирост скорости, чем апгрейд сервера за сотни тысяч рублей. Если каталог, поиск или админка в вашем проекте тормозит — начните с диагностики запросов: решение, скорее всего, ближе и дешевле, чем кажется.

Нужна разработка?

Оставьте заявку — обсудим ваш проект и предложим решение