Como nós otimizamos uma consulta de 16 segundos para 0.2 segundos no Laravel — com mais de 150 milhões de registros.
No Pcontrol, nosso CRM de vendas especializado em geração e gestão de leads, lidamos com um volume imenso de dados. São mais de 150 milhões de registros em uma única tabela. À medida que nosso sistema evolui e os clientes crescem, percebemos um desafio importante: consultas simples estavam demorando muito.
Em uma das telas do Pcontrol, uma conta com apenas 400 registros levava 26 segundos para carregar. Curiosamente, outra conta, com mais de 500 registros, carregava em apenas 2 segundos — usando o mesmo código.
Isso acendeu nosso alerta: não era o volume de dados retornado que impactava, e sim como o banco acessava esses dados.
Iniciamos a análise com alguns recursos como o do MySQL. O primeiro foi o SHOW FULL PROCESSLIST que nos mostrou as queries mais pesadas, as que lavaram mais tempo para ser finalizadas.
Então analisamos estas queries com o EXPLAIN do próprio MySQL e percebemos que, mesmo com índices individuais nas colunas usadas em WHERE, ORDER BY e JOIN, o MySQL ainda fazia leituras completas da tabela.
Como usando Laravel, adicionamos o recurso DB::listen() nativo do framework para ouvir todos os eventos de consulta SQL executados. Esse recurso é extremamente útil para debug ou para logar queries e seus tempos de execução.
Resultado: consumo absurdo de I/O e CPU, mesmo para contas pequenas.
O uso do pré-check com consulta limitada (limit 1) é uma prática inteligente de performance e tem nome sim dentro dos padrões de projeto e otimizações:
Podemos associá-la a Short-Circuit Evaluation, Guard Clauses e até a uma forma prática de Query Optimization via Early Exit.
Vamos aprofundar esse conceito dentro do seu contexto com o Pcontrol, focado em performance e design de código.
Essa função consulta rapidamente o banco de dados para verificar se existe ao menos um registro que atenda aos filtros solicitados. Se não houver nenhum, a aplicação já retorna um resultado vazio e evita que toda a query complexa, com joins e paginação, seja executada.
Por que isso melhora a performance? Imagine uma query com:
Agora imagine que o filtro inserido pelo usuário elimina 100% dos registros (ex: um source_id inexistente para aquela account_id). Rodar toda essa query seria desperdício de recursos.
Com o LIMIT 1, o banco só precisa localizar um único registro válido.
Se encontrar: executa a query pesada. Se não encontrar, retorna imediatamente. Isso é uma forma de “cortar caminho” computacional, economizando CPU e I/O de banco de dados.
Esse tipo de prática pode ser classificado ou relacionado a:
Um conceito de design de código, onde você interrompe o fluxo o mais cedo possível se uma condição já invalida o processo. No nosso caso: “Não tem lead? Então nem segue.”
Técnica de otimização lógica onde você avalia uma condição mínima necessária antes de executar algo mais custoso. Aplicada ao banco:
SELECT 1 FROM leads WHERE filtros LIMIT 1;
Resultado: se não tiver nem esse 1, pare tudo.
No mundo dos bancos de dados, isso é uma estratégia comum de otimização, especialmente quando combinada com índices:
Minimiza o escopo, evita joins e sorting e reduz carga no banco.
Filosofia de desenvolvimento onde um sistema deve falhar o mais rápido possível se não puder continuar. Usar o pré-check evita gasto de tempo e processamento em algo que já se sabe que não terá retorno útil.
O principio Fast fail é um dos meus favoritos, se você ainda não usa, comece a usar hoje.
Trabalhamos com milhões de registros e temos múltiplos filtros customizáveis, o lead pode ter diversas associações (campanhas, atendimentos, atividades e etc).
O método getLeadPreCheck() implementa um mecanismo de defesa que evita custo desnecessário, especialmente em contas com muitos dados.
Evita dezenas ou centenas de milissegundos (ou até segundos) desperdiçados com:
O Eloquent é excelente, mas para grandes volumes, sua abstração pode custar caro.
O que o Eloquent retorna não é um array puro, e sim uma instância de lluminate\Database\Eloquent\Collection que contém vários objetos com diversos métodos, atributos e etc. O objetivo do Eloquent é facilitar a vida do programador, mas a performance pode ser bem prejudicada com centenas de milhões de registros.
Trocando para para DB::table, passamos a ter controle total da query, eliminando a criação de objetivos desnecessários, usando apenas os campos e relações necessárias para melhorar nossa performance.
O Paginator do Laravel faz uso condicional do count(*). Ele o faz o count(*) de todos os registros em todas as páginas (em cada request) da paginação, isso é extremamente custoso para o banco de dados.
Solução: só fazemos o count quando estamos na página 1.
No Laravel, ao usar o Eloquent::paginate(), a mágica por trás da paginação inclui 2 queries principais:
Essa segunda query é feita em todas as requisições de página, inclusive quando você já está, por exemplo, na página 5 ou 10.
No nosso sistema de CRM de Vendas e geração de leads Pcontrol, temos cenários onde a tabela leads possui mais de 150 milhões de registros e ela não pára de crescer.
Mesmo que o filtro retorne apenas 500 registros, o Eloquent::paginate() faz o COUNT(*) em cima de toda a base filtrada, o que significa:
Tudo isso para exibir apenas “Página 5 de X”.
A solução adotada: Remoção do Paginate do Laravel, criação de uma classe que gera a paginação usando PHP puro.
Só executamos o count(*) se for a primeira página (página 1), guardamos o total em um sessão para que o valor seja exibido na página HTML para o usuário.
Ao aplicar uma lógica condicional para só executar count(*) na primeira página da paginação, conseguimos:
Mais uma pequena mudança com grande impacto no desempenho do Pcontrol.
Agora o MySQL faz:
Busca direta pelo account_id e filtra rapidamente por source_id, status e created_at. Já entrega um resultado enxuto para aplicar joins e ordenações.
Após essas mudanças, uma tela que levava 16 segundos para ser montada passou a carregar em 0.2 segundos — com dados reais em produção.
1 – Laravel é incrível, mas o Eloquent não é mágico. Quando trabalhamos com grandes volumes de dados, às vezes precisamos ir direto ao SQL.
2 – Paginação exige estratégia — evitar count(*) sempre que possível é essencial.
3 – Índices compostos mudam tudo: índices individuais não garantem grande performance em determinados contextos.
4 – EXPLAIN e SHOW FULL PROCESSLIST são seus melhores amigos: sempre analise o plano de execução das queries.
5 – Usar recursos nativos do banco de dados, do framework e da linguagem de programação para identificação, monitoramento e aprimoramento é uma estratégia simples e extremamente eficiente.
6 – O mais importante: pequenas decisões técnicas fazem uma enorme diferença no dia a dia do usuário final.
O Pcontrol é mais do que um CRM de vendas. Ele é um sistema completo de geração e qualificação de leads B2b , pensado para empresas que lidam com volume e performance. Se você quer escalar suas vendas sem abrir mão da velocidade e eficiência, venha conhecer o Pcontrol.
Se você também lida com centenas de milhões de registros no seu sistema Laravel e está enfrentando lentidão, espero que este relato te ajude.