Проблема N+1 и DataLoader

↑ 5.3 GraphQL на бэкенде и Federation · ← Предыдущая · → Следующая

Обязательно~3 мин чтенияУровень: middle

Зачем это на собесе

Главный вопрос по GraphQL-производительности; DataLoader — стандартный ответ.

Объяснение

Запрос orders { customer { name } }: resolver customer вызывается для каждого заказа — 1 запрос на список + N запросов за клиентами.

DataLoader собирает все ключи, запрошенные в рамках одного «тика» выполнения, и выполняет один пакетный запрос; результат кэшируется в рамках запроса.

public class CustomerByIdDataLoader(IBatchScheduler scheduler, IDbContextFactory<AppDbContext> factory, DataLoaderOptions? options = null)
    : BatchDataLoader<Guid, Customer>(scheduler, options ?? new DataLoaderOptions())
{
    protected override async Task<IReadOnlyDictionary<Guid, Customer>> LoadBatchAsync(IReadOnlyList<Guid> keys, CancellationToken ct)
    {
        await using var db = await factory.CreateDbContextAsync(ct);
        return await db.Customers.Where(c => keys.Contains(c.Id)).ToDictionaryAsync(c => c.Id, ct);
    }
}
 
public async Task<Customer> GetCustomer([Parent] Order o, CustomerByIdDataLoader loader, CancellationToken ct)
    => await loader.LoadAsync(o.CustomerId, ct);

Итог: 2 запроса вместо N+1: orders и customers where id in (...).

ВидСмысл
BatchDataLoaderодин ключ → одно значение
GroupedDataLoaderодин ключ → коллекция (заказы клиента)
CacheDataLoaderкэш без пакетирования

Нюансы и подводные камни

  • DbContext не потокобезопасен: используйте IDbContextFactory в DataLoader.
  • Слишком большие IN — ограничивайте размер батча.
  • При UseProjection часть N+1 решается самим EF, DataLoader нужен для связей между сервисами и не-EF источников.
  • DataLoader кэширует в рамках запроса; не путайте с межзапросным кэшем.

Практика

  1. Включите логирование SQL и увидите N+1, затем внедрите DataLoader.
  2. Сделайте GroupedDataLoader для заказов клиента.
  3. Сравните число запросов до и после.

Вопросы с ответами

Связанные темы