0Pricing
PHP Academy · Aula

Desempenho: N+1 e DataLoader

Agrupe e armazene em cache a resolução de campos para manter a velocidade.

Desempenho: N+1 e DataLoader é uma aula grátis de PHP Academy no CoddyKit. Esta é a aula 4 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de PHP Academy, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de PHP Academy inclui 4 aulas no total.

O assassino silencioso: N+1

A maior armadilha de desempenho do GraphQL é o problema de consulta N+1. Ele é silencioso porque cada resolvedor parece inocente isoladamente — mas aninhe uma lista e um campo por item, e executará uma consulta para a lista mais uma consulta por item. Com 100 itens, são 101 viagens de ida e volta. Esta lição mostra como reduzi-las a um punhado de consultas em lote com DataLoader.

Vendo o N+1 na prática

Considere { posts { author { name } } }. O resolvedor de posts executa uma consulta. Depois, para cada publicação, o resolvedor de author executa sua própria consulta. O código ingênuo abaixo torna o custo evidente.

<?php
// 1 query for posts...
$posts = [['id'=>1,'author_id'=>7],['id'=>2,'author_id'=>7],['id'=>3,'author_id'=>9]];

$queries = 1;
foreach ($posts as $post) {
    // ...then 1 query PER post to fetch its author
    $queries++;
    // SELECT * FROM users WHERE id = $post['author_id']
}
echo "Total DB queries: {$queries}\n"; // Total DB queries: 4

A ideia central: agrupar por nível

O GraphQL resolve nível a nível. Todos os resolvedores de author da lista de publicações são executados no mesmo ciclo de execução. Se pudermos adiar cada busca de autor, coletar os IDs solicitados e então executar uma única WHERE id IN (...), transformaremos N consultas em uma. Esse adiamento é exatamente o que GraphQL\Deferred fornece.

<?php
$ids = [7, 7, 9];
$unique = array_values(array_unique($ids));
// One query instead of three:
echo 'SELECT * FROM users WHERE id IN (' . implode(',', $unique) . ")\n";
// SELECT * FROM users WHERE id IN (7,9)

Um acumulador/carregador mínimo

Eis a essência de um DataLoader: um acumulador que reúne IDs, carrega-os uma vez por meio de uma função de lote e fornece os resultados a partir de um cache. A mesma chave solicitada duas vezes é carregada uma única vez — deduplicação automática.

<?php
class UserLoader {
    private array $queue = [];
    private array $cache = [];
    public function __construct(private \Closure $batchFn) {}

    public function add(int $id): void { $this->queue[$id] = true; }

    public function loadOnce(): void {
        $missing = array_diff(array_keys($this->queue), array_keys($this->cache));
        if ($missing) {
            foreach (($this->batchFn)(array_values($missing)) as $id => $row) {
                $this->cache[$id] = $row;
            }
        }
        $this->queue = [];
    }

    public function get(int $id): mixed { return $this->cache[$id] ?? null; }
}

Integrando-o a um resolvedor

O resolvedor de author enfileira o ID e retorna um objeto adiado. O graphql-php executa todas as operações adiadas depois do nível atual; portanto, quando o fechamento for executado, todos os IDs de autores da lista inteira terão sido enfileirados. Uma chamada a loadOnce() dispara uma única consulta em lote.

<?php
use GraphQL\Deferred;

$authorField = [
    'type' => $userType,
    'resolve' => function ($post, $args, $context) {
        /** @var UserLoader $loader */
        $loader = $context['userLoader'];
        $loader->add($post['author_id']);
        return new Deferred(function () use ($loader, $post) {
            $loader->loadOnce();              // batches across all posts
            return $loader->get($post['author_id']);
        });
    },
];

Usando a biblioteca overblog/dataloader

Raramente você precisará criar isso manualmente. overblog/dataloader-php é a versão adaptada consolidada do DataLoader do Facebook. Você fornece uma função de lote que recebe uma matriz de chaves e deve retornar uma promessa de valores na mesma ordem. Ela gerencia o cache, a deduplicação e a resolução de promessas.

composer require overblog/dataloader-php

Construindo um DataLoader

O contrato da função de lote é rigoroso: dada [k1, k2, k3], ela deve resolver para [v1, v2, v3] posicionalmente. Indexe as linhas do banco de dados pela chave e remapeie a ordem de entrada para que as chaves ausentes se tornem null.

<?php
use Overblog\DataLoader\DataLoader;
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
use Overblog\PromiseAdapter\Adapter\WebonyxGraphQLSyncPromiseAdapter;

$adapter = new WebonyxGraphQLSyncPromiseAdapter(new SyncPromiseAdapter());

$userLoader = new DataLoader(function (array $ids) use ($adapter, $db) {
    $rows = $db->usersByIds($ids);          // SELECT ... WHERE id IN (...)
    $byId = [];
    foreach ($rows as $r) { $byId[$r['id']] = $r; }
    // MUST return values in the SAME ORDER as $ids
    $ordered = array_map(fn($id) => $byId[$id] ?? null, $ids);
    return $adapter->createFulfilled($ordered);
}, $adapter);

Resolvendo por meio do carregador

No resolvedor, basta chamar $loader->load($id), que retorna uma promessa. O graphql-php, por meio do adaptador, reúne essas promessas e dispara o lote automaticamente no fim do ciclo. Sem acumulação manual.

<?php
$authorField = [
    'type' => $userType,
    'resolve' => fn($post, $args, $context) =>
        $context['userLoader']->load($post['author_id']),
];

O ciclo de vida por requisição é essencial

Os DataLoaders armazenam dados em cache por chave, portanto devem ser criados do zero para cada requisição. Um carregador compartilhado entre requisições forneceria dados obsoletos e causaria vazamento de memória. Construa os carregadores ao montar o $context da requisição e descarte-os quando ela terminar.

<?php
// Per request: brand new loaders, attached to context
function buildContext($db, $currentUser): array {
    return [
        'db' => $db,
        'user' => $currentUser,
        'userLoader' => makeUserLoader($db),  // fresh, not a singleton
        'postLoader' => makePostLoader($db),
    ];
}

Outras proteções de desempenho

O DataLoader corrige as leituras N+1, mas uma consulta mal-intencionada ou descuidada ainda pode causar problemas. Adicione também:

  • Limitação da profundidade da consulta — a regra QueryDepth rejeita consultas com aninhamento patológico.
  • Complexidade da consulta — QueryComplexity atribui um orçamento de custo por campo.
  • Consultas persistidas — permita somente uma lista de permissões conhecida de operações.
  • Paginação — nunca resolva listas sem limite; use conexões por cursor.
<?php
use GraphQL\Validator\Rules\QueryDepth;
use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\DocumentValidator;

DocumentValidator::addRule(new QueryDepth(10));
DocumentValidator::addRule(new QueryComplexity(200));

Medindo o ganho

Sempre quantifique a melhoria. Envolva sua camada de banco de dados para contar consultas em um teste e, em seguida, verifique se a versão agrupada em lotes emite uma quantidade limitada, independentemente do tamanho da lista. Isso protege contra regressões nas quais alguém adiciona um resolvedor aninhado ingênuo e reintroduz silenciosamente o N+1.

<?php
class CountingDb {
    public int $queries = 0;
    public function usersByIds(array $ids): array {
        $this->queries++;            // one batched call
        return array_map(fn($id) => ['id' => $id], $ids);
    }
}

$db = new CountingDb();
$db->usersByIds([7, 9, 11, 13]);     // 4 authors
echo "Queries for 4 authors: {$db->queries}\n"; // Queries for 4 authors: 1

Verificação rápida

Por que os DataLoaders precisam ser criados por requisição?

Recapitulação

Você eliminou a pior armadilha de desempenho do GraphQL:

  • Campos de lista aninhados causam N+1: uma consulta por item.
  • O graphql-php resolve nível a nível; portanto, adiar permite agrupar todas as chaves em uma consulta IN (...).
  • GraphQL\Deferred é o recurso básico; overblog/dataloader-php reúne agrupamento em lotes, cache por chave e deduplicação.
  • A função de lote deve retornar os valores na mesma ordem das chaves de entrada.
  • Os carregadores são específicos de cada requisição; adicione limites de profundidade e complexidade, além de paginação, como proteções adicionais.

Perguntas Frequentes

A aula “Desempenho: N+1 e DataLoader” é grátis?

Sim — o texto completo de “Desempenho: N+1 e DataLoader” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de PHP Academy, atualize para CoddyKit PRO. O curso de PHP Academy inclui 4 aulas no total.

O que vou aprender em “Desempenho: N+1 e DataLoader”?

Agrupe e armazene em cache a resolução de campos para manter a velocidade. Você pratica PHP Academy com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.

Preciso ter experiência prévia para começar PHP Academy?

Nenhuma experiência prévia é necessária. PHP Academy no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 4 de 4.

Quanto tempo leva a aula “Desempenho: N+1 e DataLoader”?

A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.

Posso escrever e executar código nesta aula de PHP Academy?

Sim. Cada aula de PHP Academy inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.

Todas as aulas deste curso

  1. GraphQL versus REST
  2. Construindo um esquema com graphql-php
  3. Resolutores, mutações e assinaturas
  4. Desempenho: N+1 e DataLoader
← Voltar para PHP Academy