Desenvolvimento Salesforce

Por que seu Apex passa no teste e quebra com volume em produção

Ilustração sobre governor limits do Apex no Salesforce

O código roda perfeito no sandbox. Você testa com três registros, tudo verde, sobe para produção. Duas semanas depois um import de pedidos entra com 250 linhas e o processo morre com Too many SOQL queries: 101. Ninguém mexeu no código. O que mudou foi o volume. Esse é o dia em que quase todo dev Salesforce conhece os governor limits, e quase sempre da pior forma.

Governor limits são tetos que o Salesforce impõe a cada transação Apex: quantas queries SOQL, quantos DML, quanto tempo de CPU e memória. Como a org divide servidor com outros clientes, impedem que um código derrube a instância inteira. O erro clássico é query dentro de loop: na 101ª iteração estoura. A solução é bulkificar, buscar tudo fora do loop.

Por que a plataforma limita seu código

O Salesforce é multitenant: a sua org, a do concorrente e mais milhares de outras rodam sobre a mesma infraestrutura física. Não existe um servidor só seu que você pode saturar. Se um Apex mal escrito pudesse abrir dez mil queries numa transação, ele degradaria a performance de todo mundo que dividisse aquele nó. Os governor limits são o mecanismo que impede isso: são um contrato de "até aqui você pode ir".

A consequência prática é dura para quem vem de outras plataformas. Num backend próprio, código ineficiente fica lento; no Salesforce, código ineficiente simplesmente para com uma exceção que você não consegue capturar para ignorar. Um LimitException não é um erro comum: ele encerra a transação e faz rollback de tudo. Não dá para dar try/catch e seguir em frente. Isso muda a forma de escrever: você projeta para o limite desde a primeira linha, não otimiza depois que ficou lento.

Quais são os limites que mais estouram

A lista oficial tem dezenas de limites, mas na prática cinco ou seis respondem por quase todos os incidentes. Estes são os valores por transação, e o ponto que quase ninguém nota de primeira: eles são por transação, não por registro. Se um único insert de 200 contas dispara uma trigger, os 200 registros compartilham o mesmo orçamento de 100 queries e 150 DML.

LimiteSíncronoAssíncrono
Queries SOQL100200
Registros por query50.00050.000
Statements DML150150
Registros por DML10.00010.000
Tempo de CPU10.000 ms60.000 ms
Heap (memória)6 MB12 MB
Callouts HTTP100100

Repare em dois detalhes. O limite de queries dobra no assíncrono (Batch, Queueable, @future), assim como o de CPU sobe de 10 para 60 segundos, e é por isso que processamento pesado migra para lá. E o limite de tempo de CPU não conta espera de banco ou de callout, só o processamento do seu código: um loop aninhado sobre listas grandes, ou muita manipulação de string, é o que costuma estourar os 10 segundos, não a query em si.

Por que o código quebra só em produção

Este é o ponto que pega o dev de surpresa. O limite é por transação e a transação síncrona típica lida com um lote de até 200 registros. No sandbox você testa com dois ou três, então o loop roda duas ou três vezes e nunca chega perto de 100 queries. Em produção, um import, uma integração ou uma atualização em massa manda 200 de uma vez, e o mesmo código executa o loop 200 vezes. O bug sempre esteve lá; o volume só o revelou.

Vou dar um caso concreto da Vetra Distribuidora, a empresa fictícia que uso como org de demonstração do portfólio. Havia uma trigger em OrderItem que, para cada linha do pedido, buscava o Product2 relacionado para copiar o nome e a família do produto. Com um pedido de cinco itens, cinco queries, tudo bem. Quando simulei um import de reposição de estoque com um pedido de 250 linhas, a transação morreu na 101ª linha com Too many SOQL queries: 101. O pedido inteiro sofreu rollback: nenhuma linha foi gravada, e o usuário viu uma tela de erro sem entender por quê. O código nunca tinha sido tocado. Só nunca tinha visto volume.

Como bulkificar: o padrão que resolve quase tudo

Bulkificação é a disciplina de escrever código que processa uma coleção de registros com um número constante de queries e DMLs, independente de o lote ter 1 ou 200 registros. A receita tem três passos que você repete em quase toda situação: colete os IDs primeiro, faça uma query só, depois trabalhe em memória com um Map.

Este é o padrão errado, o que estourou na Vetra. Uma query por iteração:

// NÃO faça: uma query dentro do loop
for (OrderItem item : itens) {
    Product2 p = [SELECT Name, Family FROM Product2 WHERE Id = :item.Product2Id];
    item.Description = p.Name;
    // 250 itens = 250 queries = erro na 101ª

E este é o mesmo trabalho bulkificado. Uma query só, resultado num Map indexado por ID, e o loop apenas lê de memória:

// 1. colete os IDs
Set<Id> prodIds = new Set<Id>();
for (OrderItem item : itens) prodIds.add(item.Product2Id);

// 2. uma query para todos
Map<Id, Product2> prods = new Map<Id, Product2>(
    [SELECT Id, Name, Family FROM Product2 WHERE Id IN :prodIds]);

// 3. trabalhe em memória, zero query no loop
for (OrderItem item : itens) {
    Product2 p = prods.get(item.Product2Id);
    item.Description = p.Name;
}

A diferença não é estilo. Com o padrão bulkificado, o pedido de 250 linhas gasta uma query em vez de 250, e o mesmo código atende com folga um pedido de 5.000 linhas. O construtor new Map<Id, Product2>(...) aceita direto o resultado da query e indexa por ID, o que torna o lookup dentro do loop instantâneo. Esse é o feijão com arroz da boa prática em Apex: nenhuma query ou DML nunca deve morar dentro de um loop.

DML dentro de loop: o outro erro clássico

Query no loop estoura o limite de 100. O irmão gêmeo do problema é o DML no loop, que estoura o de 150. O padrão perigoso atualiza registro por registro:

// NÃO faça: um update por iteração
for (Account a : contas) {
    a.Status__c = 'Ativo';
    update a;   // 200 contas = 200 DMLs = erro no 151º
}

A correção é acumular numa lista e fazer um único DML sobre ela, fora do loop:

List<Account> paraAtualizar = new List<Account>();
for (Account a : contas) {
    a.Status__c = 'Ativo';
    paraAtualizar.add(a);
}
update paraAtualizar;   // um DML para a lista inteira

Um update sobre uma lista de 200 registros conta como um statement DML, não 200. É a mesma lógica da query: a operação é feita em lote. Essa decisão de onde a lógica roda, em linguagem declarativa ou em código, é parte do que discuto em quando usar Flow ou Apex: o Flow bulkifica boa parte disso por você, e é uma das razões legítimas para não escrever Apex quando o Flow resolve.

Como enxergar o limite antes de estourar

Você não precisa adivinhar quão perto está do teto. A classe Limits reporta o consumo atual e o máximo de cada recurso em tempo de execução. Jogar isso num System.debug nos pontos críticos mostra exatamente onde o orçamento está sendo gasto:

System.debug('Queries: ' + Limits.getQueries() + '/' + Limits.getLimitQueries());
System.debug('DML: ' + Limits.getDmlStatements() + '/' + Limits.getLimitDmlStatements());
System.debug('CPU: ' + Limits.getCpuTime() + '/' + Limits.getLimitCpuTime());

Mas o debug depois do problema é remédio, não prevenção. A prevenção de verdade é o teste com volume. Um teste unitário que insere 2 registros nunca vai revelar um bug de bulkificação. O teste tem que criar um lote realista, de 200 registros, dentro de Test.startTest() e Test.stopTest(), que reinicia os contadores de limite e garante que a sua lógica roda dentro de um orçamento limpo:

@isTest
static void deveProcessar200SemEstourar() {
    List<Account> contas = new List<Account>();
    for (Integer i = 0; i < 200; i++) {
        contas.add(new Account(Name = 'Conta ' + i));
    }
    Test.startTest();
    insert contas;   // dispara a trigger com 200 de uma vez
    Test.stopTest();
    System.assertEquals(200, [SELECT COUNT() FROM Account WHERE Name LIKE 'Conta %']);
}

Se esse teste passa, a bulkificação está correta. Se ele estoura, você achou o bug no CI, não numa ligação de cliente às sexta à noite. Vale a mesma disciplina de sempre montar a query com segurança: quando o filtro é dinâmico, combine bulkificação com SOQL dinâmico à prova de injection, para não trocar um problema por outro.

Quando isso não é problema seu

Honestidade primeiro: se você é admin e nunca escreveu uma linha de Apex, provavelmente nunca vai bater de frente com esses limites. O Flow, os workflows padrão e a maioria das automações declarativas já operam de forma bulkificada por baixo dos panos. O risco real aparece quando existe código customizado, quando você chama Apex de dentro do Flow, ou quando várias automações disparam umas às outras na mesma transação e o orçamento compartilhado estoura sem que nenhuma delas, isoladamente, pareça pesada.

E não caia no extremo oposto de otimizar o que não precisa. Se a sua trigger nunca vai ver mais que dez registros por vez, escrever máquina de bulkificação elaborada é esforço desperdiçado. A regra é simples e barata: nunca ponha SOQL ou DML dentro de um loop, e teste com 200. Fazendo só isso, você elimina a esmagadora maioria dos incidentes de governor limits sem precisar de mais nada, nem de mim.

Perguntas frequentes sobre governor limits

O que são governor limits no Salesforce?

São tetos que a plataforma impõe a cada transação Apex: número de queries SOQL, statements DML, tempo de CPU, memória e callouts. Como a org é multitenant e divide servidor com outros clientes, esses limites impedem que um código individual consuma recurso demais e degrade a instância dos vizinhos.

Por que aparece o erro Too many SOQL queries: 101?

Porque a transação executou mais de 100 queries SOQL de uma vez. Quase sempre a causa é uma query dentro de um loop: com 101 iterações, a 101ª estoura. A correção é tirar a query do loop e buscar todos os registros num único SOQL antes de iterar.

Governor limits mudam entre síncrono e assíncrono?

Alguns mudam. Em contexto assíncrono (Batch, Queueable, @future) o limite de queries dobra para 200 e o de CPU sobe para 60 segundos. Não conte com isso como plano A: bulkifique primeiro e use o assíncrono para volume que realmente precisa dele.

Preciso me preocupar com isso se só uso Flow?

Menos, mas sim. O Flow tem os mesmos limites por trás e já bulkifica boa parte das operações por você. O risco aparece quando você chama Apex de dentro do Flow ou encadeia automações que disparam umas às outras na mesma transação e somam no orçamento compartilhado.

Seu Apex quebra com volume e você não sabe por quê?

Eu reviso triggers e classes Apex procurando query e DML em loop, refatoro para o padrão bulkificado e deixo o teste de 200 registros no lugar para o problema não voltar. Comece com um diagnóstico gratuito de 45 minutos.

Falar no WhatsApp Ver serviços