← Назад к вопросам

Что такое N+1 проблема при работе с ORM и как её решить?

2.0 Middle🔥 221 комментариев
#Node.js и JavaScript#Фреймворки и библиотеки

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

N+1 проблема при работе с ORM

N+1 проблема - это ситуация, при которой для получения данных вместо одного оптимального SQL-запроса ORM выполняет 1 запрос для основной сущности и N дополнительных запросов для связанных данных.

Как возникает

Нужно вывести список постов с именами авторов:

// Prisma - N+1 проблема
const posts = await prisma.post.findMany(); // 1 запрос: SELECT * FROM posts

for (const post of posts) {
  const author = await prisma.user.findUnique({ // N запросов!
    where: { id: post.authorId }
  });
  console.log(`${post.title} by ${author.name}`);
}

Если постов 100, выполнится 101 SQL-запрос:

SELECT * FROM posts;
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE id = 2;
-- ... еще 98 запросов

Решение 1: Eager Loading (жадная загрузка)

Загружаем связанные данные сразу в одном запросе через JOIN:

// Prisma - include
const posts = await prisma.post.findMany({
  include: { author: true }
});

// TypeORM - relations
const posts = await postRepository.find({
  relations: ["author", "comments"]
});

// Sequelize - include
const posts = await Post.findAll({
  include: [{ model: User, as: "author" }]
});

Решение 2: Batch Loading (DataLoader)

DataLoader (от Facebook) собирает N отдельных запросов в один батч:

import DataLoader from "dataloader";

const userLoader = new DataLoader(async (userIds: readonly string[]) => {
  const users = await prisma.user.findMany({
    where: { id: { in: [...userIds] } }
  });

  const userMap = new Map(users.map(u => [u.id, u]));
  return userIds.map(id => userMap.get(id) ?? null);
});

// Использование - все вызовы в одном тике батчируются
const posts = await prisma.post.findMany();
const postsWithAuthors = await Promise.all(
  posts.map(async (post) => ({
    ...post,
    author: await userLoader.load(post.authorId) // батчируется!
  }))
);
// Результат: 2 запроса вместо N+1

DataLoader особенно полезен в GraphQL, где структура запроса определяется клиентом.

Решение 3: Ручной JOIN

const result = await pool.query(`
  SELECT p.id, p.title, p.content, u.name as author_name
  FROM posts p
  JOIN users u ON p.author_id = u.id
  WHERE p.status = $1
  ORDER BY p.created_at DESC
  LIMIT $2
`, ["published", 20]);

Решение 4: Подзапрос с IN

// Prisma автоматически оптимизирует через IN
const posts = await prisma.post.findMany({
  include: { author: true }
});
// Prisma выполнит:
// SELECT * FROM posts;
// SELECT * FROM users WHERE id IN (1, 2, 3, ...);

Как обнаружить N+1

1. Query logging:

// Prisma
const prisma = new PrismaClient({
  log: ["query"]
});

// TypeORM
{ logging: true }

2. Правило: если видите одинаковые запросы, отличающиеся только параметром (WHERE id = 1, WHERE id = 2...) - это N+1.

Рекомендации

  • Включайте логирование запросов на dev-окружении
  • По умолчанию используйте eager loading для известных связей
  • DataLoader для GraphQL и динамических связей
  • Мониторьте количество запросов на endpoint (цель: менее 10 запросов)
  • Используйте EXPLAIN ANALYZE для тяжелых запросов