Как оптимизировать запросы Prisma, чтобы избежать проблемы N+1?
Проблему N+1 в Prisma решают через include/select для eager loading, DataLoader для батчинга в GraphQL, findMany с оператором in вместо findUnique в цикле. Диагностика — через DEBUG=prisma:query.
Проблема N+1 в Prisma и способы её решения
Проблема N+1 возникает, когда для получения N связанных записей выполняется N отдельных запросов вместо одного. В Prisma она чаще всего проявляется в GraphQL-резолверах или при ручном обходе массивов результатов с обращением к БД внутри цикла.
Пример проблемы
// ПЛОХО: N+1 — для каждого поста отдельный запрос за автором
const posts = await prisma.post.findMany();
for (const post of posts) {
const author = await prisma.user.findUnique({
where: { id: post.authorId },
});
console.log(post.title, author?.name);
}
Решение 1: include
Самый простой способ — использовать include для eager loading связанных данных. Prisma выполнит JOIN или отдельный IN-запрос:
// ХОРОШО: один запрос с JOIN
const posts = await prisma.post.findMany({
include: {
author: true,
tags: true,
},
});
Решение 2: select — только нужные поля
select даёт точечный контроль над полями и экономит трафик:
const posts = await prisma.post.findMany({
select: {
id: true,
title: true,
author: {
select: { name: true, email: true },
},
},
});
Решение 3: Prisma DataLoader (для GraphQL)
В GraphQL-резолверах include не всегда помогает, потому что каждый резолвер вызывается отдельно. Используйте пакет @paljs/plugins или напишите собственный DataLoader с findMany + where: { id: { in: ids } }:
import DataLoader from 'dataloader';
import { prisma } from './prisma';
// DataLoader батчирует запросы в рамках одного event-loop тика
const userLoader = new DataLoader(async (ids: readonly number[]) => {
const users = await prisma.user.findMany({
where: { id: { in: [...ids] } },
});
// DataLoader требует порядок в точности как ids
const userMap = new Map(users.map(u => [u.id, u]));
return ids.map(id => userMap.get(id) ?? null);
});
// В резолвере:
const author = await userLoader.load(post.authorId);
Решение 4: $transaction для批量 операций
Когда нужно выполнить несколько независимых запросов, объедините их в $transaction — они выполнятся в одном round-trip:
const [posts, users] = await prisma.$transaction([
prisma.post.findMany(),
prisma.user.findMany(),
]);
Решение 5: findMany с фильтром вместо findUnique в цикле
// Собрать все ID авторов, потом один запрос
const posts = await prisma.post.findMany();
const authorIds = [...new Set(posts.map(p => p.authorId))];
const authors = await prisma.user.findMany({
where: { id: { in: authorIds } },
});
const authorMap = new Map(authors.map(a => [a.id, a]));
const result = posts.map(p => ({
...p,
author: authorMap.get(p.authorId),
}));
Диагностика
# включить логирование всех SQL-запросов
DEBUG="prisma:query" node server.js
Или через конструктор клиента:
const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error'],
});
Подводные камни
includeс глубокой вложенностью (3+ уровня) может генерировать медленные JOIN-ы. Иногда два отдельных запроса быстрее одного сложного.- Prisma при
includeна many-to-many делает отдельный IN-запрос, а не JOIN — это нормально, но объём данных может быть неожиданно большим. - DataLoader работает только в рамках одного тика event loop. Если резолверы выполняются асинхронно с
awaitмежду вызовами — батчинг не сработает. $transactionс массивом запросов не поддерживает интерактивные транзакции — для условной логики нужен callback-вариант$transaction(async (tx) => { ... }).- Использование
selectиincludeодновременно на одном уровне запрещено в Prisma — нужно выбрать одно из двух. - Логирование
DEBUG=prisma:queryв production снижает производительность — включайте только при отладке. - Без мониторинга запросов (APM, slow query log) N+1 легко пропустить на ревью кода — добавьте алерт на количество запросов за HTTP-запрос.
Common mistakes
- Путает Prisma Client API с гарантиями базы данных: индексы, блокировки и isolation level не создаются магически.
- Не объясняет, где в lifecycle находится оптимизация N+1.
- Не разделяет validation, authorization, business logic и persistence.
- Игнорирует ошибки, лимиты входных данных, observability и тестирование.
What the interviewer is testing
- Может объяснить оптимизация N+1 на примере кода.
- Называет ключевые API: include, relationLoadStrategy.
- Отделяет ORM/query builder поведение от реального поведения СУБД.
- Видит production-риски: безопасность, отказоустойчивость, логирование и тесты.