← назад к разделу

ORM генерирует SQL, но не выбирает план за вас. Медленный запрос читают вместе с параметрами, индексами и количеством возвращаемых строк.

Сначала SQL и EXPLAIN

Включают лог запросов в тестовом окружении или собирают SQL через QueryBuilder. Затем выполняют EXPLAIN (ANALYZE, BUFFERS) на реалистичных данных.

Смотрят последовательное чтение большой таблицы, число строк на каждом узле, сортировку и реальное время. CREATE INDEX по каждой колонке не является решением: порядок и селективность зависят от запроса.

N+1 виден не в одном EXPLAIN, а в серии одинаковых запросов с разными ID. Поэтому отдельно считают SQL-вызовы на сценарий.

Offset против keyset

OFFSET 100000 заставляет базу прочитать и отбросить сто тысяч строк. Для ленты используют keyset:

const page = await repo.createQueryBuilder('product')
  .where('product.id > :afterId', { afterId: 100000 })
  .orderBy('product.id', 'ASC')
  .limit(50)
  .getMany();

Порядок должен быть детерминированным и поддерживаться индексом. Для сложной сортировки в курсор включают все поля порядка, а не только ID.

Типичные ошибки

  • getMany с join на большую коллекцию раздувает результат.
  • getManyAndCount повторяет тяжелый запрос ради общего числа.
  • Несоответствие типа параметра не дает использовать индекс.
  • Множество параллельных запросов исчерпывает пул соединений.
  • Долгая транзакция держит блокировки и соединение.

Исправление начинают с уменьшения данных и числа запросов, а не с увеличения пула или сервера.

Коротко

  • SQL и план проверяют на реалистичном объеме.
  • N+1 считают по числу запросов на сценарий.
  • Keyset работает с детерминированным порядком и подходящим индексом.
  • Join и count способны доминировать стоимость.
  • Сначала уменьшают работу, затем увеличивают ресурсы.

Что почитать дальше