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-phpConstruindo 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
QueryDepthrejeita consultas com aninhamento patológico. - Complexidade da consulta —
QueryComplexityatribui 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-phpreú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
- GraphQL versus REST
- Construindo um esquema com graphql-php
- Resolutores, mutações e assinaturas
- Desempenho: N+1 e DataLoader