169 похожих чатов

Хорошо поставленный вопрос и содержит уже сам в себе ответ. В

реализации самой спецификации GraphQL нет проблемы N+1. Графкуэль берет и выполняет запрос который прислал клиент. Проблема N+1 возникает непосредственно в вашем коде резолверов. В коде который вы написали сами. Если чутка “умнее” написать свой код, например с помощью DataLoader’а, то проблема решается.

Описание проблемы N+1: эта проблема возникает, когда вы запрашиваете Список элементов и к каждому элементу этого списка запрашиваете связные ресурсы. В графкуэль-запросе попросили 100 статей, и к каждой статье запросили данные автора.

Чтобы выполнить такой запрос клиента, необходимо (вариант ИЗИ):
- получить 100 статей из базы (1 запрос в БД)
- из каждой статьи взять id автора
- сделать отдельный запрос на получение данных автора по полученному id шагом выше (N запросов в БД)

Как такая проблема решается с DataLoader’ом (вариант НОРМ)
- получить 100 статей из базы (1 запрос в БД)
- из каждой статьи взять id автора
- положили id автора в DataLoader, который возвращает promise (N операций отложи ID)
- на следующий тик eventLoop’а выполнить один запрос findMany (вместо findById) по всем собранным id-шникам авторов (1 запрос в БД)
- полученных авторов отдать в DataLoader в правильном порядке, которые разрезолвит промисы на 3-ем шаге.

Как решается проблема по другому без DataLoader (вариант ХАРД)
- Вы получили запрос от клиента, и можете в резолвере на первом (верхнем) уровне получить из 4-го аргумента info AST-дерово запроса. Согласно этого AST’a сразу сделать большой запрос с JOIN’ами в базу, и полченные данные трансформировать в форму которую запросил клиент. Т.е. у вас резолверы только на верхнем уровне. По такому пути, Hasura работает.

——

Как же мы незаметно попадаем на проблему N+1?
- 1) Решили описать тип Post
- 2) Потом описали тип Author
- 3) Описываем связь 1 к 1 - добавляем поле Post.author и в нем просто по authorId дергаем запись автора из базы через findById.
…некоторое время спустя…
- 4) А давай-ка прикртим в Query.postMany поле, которое будет возвращать список Post.
…бац, и у вас незаметно появилась проблема N+1…

На шаге 4 вы ничего страшного не сделали, просто запросили список статей. Корень проблемы в шаге 3 - когда вы его реализовывали, вы даже и не думали что создавая такую простую связь по id через findById, она может быть использована как то “неоптимально”. А именно – будет запрошана кучу раз в одном запросе от клиента.

Итог прост: если вы “связываете” между собой два типа (Post и Author), то старайтесь сделать резолвер подготовленным к множественному вызову в рамках одного запроса. например как написано здесь: https://github.com/nodkz/conf-talks/tree/master/articles/graphql/dataloader

——

Ах, да. Проблема N+1 также существует и с REST API, просто она менее очевидна, но смысл тот же. В тупую дернули список статей, а потом дергаем описание авторов по одному (нагрузка на бэк такая же как и с GraphQL). Как решаете эту проблему с REST API:
- новая агрегационная ручка (похоже на вариант ХАРД выше)
- на клиенте собираем список авторов в какой-нить масивчик, а потом через метод findMany дергаем всех за один запрос (похоже на вариант НОРМ выше)
- нифига не делаем, бэкендеры там все кэшируют (похоже на вариант ИЗИ выше)

3 ответов

43 просмотра

Спасибо большое, очень развёрнутый и понятный ответ.

Как раз у себя реализовал через один запрос, парсю аст

Отлично. Но хочу дополнить, что проблема N+1 была и до GraphQL, это понятие хорошо известно в мире баз данных. Благодаря более удачным решениям в коде, можно её нивелировать. У Павла Черторогова, замечательные примеры и разбор работы с dataloader, огонь.

Похожие вопросы

Обсуждают сегодня

30500 за редактор? )
Владимир
47
Недавно Google Project Zero нашёл багу в SQLite с помощью LLM, о чём достаточно было шумно в определённых интернетах, которые сопровождались рассказами, что скоро всех "ибешни...
Alex Sherbakov
5
вы делали что-то подобное и как? может есть либы готовые? увидел картинку нокода, где всё линиями соединено и стало интересно попробовать то же в ddl на lua сделать. решил с ч...
Victor
8
Подскажите пожалуйста, как в CustomDrawCell(Sender: TcxCustomGridTableView; ACanvas: TcxCanvas; AViewInfo: TcxGridTableDataCellViewInfo; var ADone: Boolean); получить наз...
A Z
7
Ребят в СИ можно реализовать ООП?
Николай
33
https://github.com/erlang/otp/blob/OTP-27.1/lib/kernel/src/logger_h_common.erl#L174 https://github.com/erlang/otp/blob/OTP-27.1/lib/kernel/src/logger_olp.erl#L76 15 лет назад...
Maksim Lapshin
20
Как передать управляющий символ в открытую через CreateProcess консоль? Собсна, есть процедура: procedure TRedirectThread.WriteData(Data: OEMString); var Written: Cardinal;...
Serjone
1
Он в одиночку это дело запилил или была какая-то команда?
Aquinary
12
~ 2m21s  nix shell github:nixos/nixpkgs#stack ~  stack ghc -- --version error: … while calling the 'derivationStrict' builtin at /builtin/derivation.nix:...
Rebuild your mind.
6
Всем привет, нужна как никогда, нужна помощь с IO в загрузчике. Пишу в code16 после установки сегментных регистров, пишу вывод символа. Пробовал 2 варианта: # 1 mov $0x0E, %a...
Shadow Akira
14
Карта сайта