N+1 é o tipo de bug de performance mais comum que já encontrei em código Laravel — e o mais fácil de não perceber, porque tudo funciona perfeitamente bem localmente com dez registros de teste. Só quebra em produção quando o cliente tem duas mil linhas na tabela. Já toquei nesse assunto de leve em índices MySQL além do básico, mas o problema de N+1 nem sempre é falta de índice — é o ORM disparando query demais.
Como esse bug nasce sem ninguém perceber
O padrão clássico é um foreach percorrendo uma coleção e acessando uma relação dentro do loop, tipo $pedido->cliente->nome pra cada pedido de uma lista. Cada acesso dispara uma query nova pro banco, então uma tela que lista 50 pedidos vira 51 queries em vez de 2. O código passa no code review porque parece limpo e o Eloquent esconde a query por trás da sintaxe de propriedade — é exatamente essa transparência que faz o problema ser invisível até alguém medir.
Como eu caço isso na prática
Uso o Laravel Debugbar ou o Telescope em ambiente de desenvolvimento pra ver a contagem de queries por página — se um endpoint que deveria fazer 3 queries está fazendo 40, já sei que tem N+1 escondido. A correção na maioria dos casos é with() ou load() pra eager loading, carregando a relação de uma vez antes do loop. Pra casos onde só preciso de uma coluna da relação, prefiro withCount() ou uma query direta com join, porque carregar o model inteiro só pra ler um campo também é desperdício.
Outros vícios de ORM que valem atenção
Além do N+1, vejo bastante gente usando get() e filtrando resultado em PHP quando dava pra fazer o banco filtrar, ou carregando model completo pra um select que só precisa de dois campos. Regra prática que sigo: se a lógica pode ser expressa em SQL, deixo o banco fazer — ele foi construído pra isso e normalmente é ordens de magnitude mais rápido que iterar em PHP. Isso conecta direto com o que já falei sobre cache em Laravel: antes de cachear uma query lenta, vale garantir que ela não está lenta por um vício evitável.



