Вы идёте в магазин за десятью продуктами. Можно съездить десять раз, по одному продукту за поездку, а можно составить список и купить всё за один раз. Резолверы из прошлого урока ходят в базу «по одному продукту»: на каждый пост — отдельный запрос за комментариями. Это знаменитая проблема N+1. В этом уроке вы увидите её в логе Prisma и решите с помощью DataLoader — небольшой библиотеки, которая собирает запросы одного «такта» в один.

Как возникает N+1

Возьмём запрос { posts { title comments { text } } }. Query.posts выполняет один SQL-запрос и возвращает N постов. Затем для каждого поста вызывается Post.comments, и каждый вызов отправляет свой запрос WHERE "postId" = …. Итого 1 + N запросов — отсюда название. При 10 постах это 11 запросов, при 100 — 101.

N+1: на 10 постов приходится 11 запросов, а с DataLoader — 2
N+1: на 10 постов приходится 11 запросов, а с DataLoader — 2

Это плохо по двум причинам. Каждый запрос — поход по сети в базу, а пул подключений ограничен. И уровни перемножаются: если у каждого комментария ещё и автор, запросов станет 1 + N + N·M. Клиент же отправил всего один запрос.

Как увидеть N+1 в логе Prisma

Клиент создан с log: ['query'] (прошлый урок). Выполните запрос в Sandbox и посмотрите консоль сервера (вывод сокращён):

text
prisma:query SELECT ... FROM "public"."Post" ... ORDER BY ... DESC
prisma:query SELECT ... FROM "public"."Comment" WHERE ... "postId" = $1 ...
prisma:query SELECT ... FROM "public"."Comment" WHERE ... "postId" = $1 ...
prisma:query SELECT ... FROM "public"."Comment" WHERE ... "postId" = $1 ...
... 7 more queries, one for each remaining post

Запрос к Post один, а к Comment — по одному на каждый пост. Привыкайте считать запросы в логе: так проще всего найти N+1.

💡 Prisma умеет сама объединять вызовы findUnique, сделанные в одном такте, в один запрос с IN. Поэтому Post.author на findUniqueOrThrow в логе может выглядеть лучше, чем Post.comments: там findMany, и его Prisma не объединяет. Рассчитывать на это нельзя — приём работает только для findUnique. DataLoader же подходит для любых источников (другой REST API, Redis, сервис) и заодно кеширует результаты на время запроса.

Идея: собрать ключи и спросить один раз

Чтобы купить всё за одну поездку, нужен список. GraphQL вызывает все 10 резолверов Post.comments подряд, не дожидаясь ответов. Весь такой синхронный код укладывается в один такт (tick) цикла событий Node.js — одну порцию работы между паузами на ожидание. Значит, резолвер может не ходить в базу сразу, а записать «мне нужны комментарии поста 3» в очередь. К концу такта в очереди окажутся все 10 ключей — и можно сделать один запрос WHERE "postId" IN (…), а потом раздать каждому резолверу его часть. Вот игрушечная версия этой идеи: