O que é um plano de execução (e o que ele está tentando responder)
Um plano de execução é a “receita” que o otimizador escolheu para produzir o resultado de uma consulta. Ele descreve uma árvore de operadores (passos) que recebem linhas como entrada e produzem linhas como saída, até chegar ao resultado final do SELECT. A leitura conceitual do plano responde principalmente: (1) de onde os dados serão lidos (tabela/índice), (2) como serão filtrados, combinados e ordenados, (3) quanto trabalho cada etapa deve fazer (custo estimado), e (4) onde estão os gargalos (etapas que processam muitas linhas, fazem ordenação, usam estruturas de hash grandes, ou repetem trabalho em loops).
Como enxergar um plano como uma árvore de operadores
Mesmo que cada banco mostre o plano com nomes e layout diferentes, a estrutura é quase sempre uma árvore: folhas (leituras) alimentam operadores intermediários (filtros, joins, agregações, sorts) e a raiz entrega o resultado. Em muitos planos, a execução “acontece” de baixo para cima: primeiro as leituras, depois os joins, depois sort/aggregate, e por fim o retorno das linhas.
- Folhas: operadores de leitura (scan/seek) em tabelas/índices.
- Meio: filtros, joins, sorts, agregações.
- Topo: projeção (seleção de colunas), limite/paginação, retorno.
Operadores típicos e como interpretá-los
Scan (tabela ou índice)
Scan significa “varrer” uma estrutura inteira ou uma grande parte dela. Conceitualmente, é leitura sequencial de muitas páginas/entradas.
- Table scan: varre a tabela (ou partição) para encontrar linhas relevantes. Sinal de alto volume lido quando a tabela é grande.
- Index scan: varre um índice (muitas entradas). Pode ser melhor que table scan se o índice for menor, mas ainda indica leitura ampla.
Como identificar gargalo: scans costumam ser caros quando o número de linhas lidas é muito maior que o número de linhas retornadas, ou quando alimentam joins que multiplicam trabalho.
Seek (busca pontual ou por faixa)
Seek significa “ir direto” a um ponto do índice (ou a uma faixa) e ler apenas o necessário. Em geral, é sinal de boa seletividade do predicado e menor volume lido.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
- Seek pontual: igualdade (ex.:
id = ?). - Seek por faixa: intervalo (ex.:
data >= ... AND data < ...).
Como identificar gargalo: um seek pode virar gargalo se estiver dentro de um loop que executa milhares de vezes (ex.: nested loops), ou se a faixa ainda for grande.
Filtro (Filter/Predicate)
Filtro aplica uma condição e reduz linhas. Pode aparecer como operador separado ou embutido em outros operadores.
- Filtro cedo (perto das folhas) costuma ser bom: reduz volume antes de joins/sorts.
- Filtro tarde (depois de join/sort) pode indicar que o plano precisou carregar muitas linhas antes de descartar.
Como identificar gargalo: se o filtro remove muitas linhas (entrada muito maior que saída), pergunte por que ele não foi aplicado antes (ou por que não houve uma leitura mais seletiva).
Sort (ordenação)
Sort ordena linhas para atender ORDER BY, para preparar um MERGE JOIN, ou para viabilizar agregações específicas. Sort é um dos gargalos mais comuns porque exige comparar muitas linhas e, dependendo do volume, pode usar memória e “derramar” para disco.
- Sort para ORDER BY: aparece perto do topo, antes do retorno.
- Sort para join: aparece antes de um merge join.
- Sort para agregação: aparece antes de um aggregate que funciona por agrupamento ordenado.
Como identificar gargalo: procure sorts que recebem muitas linhas. Se o sort está acima de um join que explode cardinalidade, ele pode ficar muito caro.
Joins: Nested Loops, Hash Join, Merge Join
Joins combinam linhas de duas entradas (lado esquerdo e direito). O tipo escolhido depende do volume, da disponibilidade de ordenação/índices e das estimativas.
Nested Loops Join
Nested loops funciona como: para cada linha do lado externo, procurar correspondências no lado interno. É ótimo quando o lado externo é pequeno e o lado interno pode ser acessado de forma eficiente (por exemplo, via seek).
- Sinal de risco: lado externo grande → muitas iterações → custo explode.
- Observe: quantas linhas saem do lado externo e quantas vezes o lado interno é acessado.
Hash Join
Hash join constrói uma tabela hash de um lado (geralmente o menor) e depois sonda com o outro lado. Bom para grandes volumes sem ordenação útil, mas pode consumir muita memória.
- Sinal de risco: entrada grande no lado “build” (construção) ou falta de memória (hash grande).
- Observe: qual lado está sendo “build” e quantas linhas entram nele.
Merge Join
Merge join percorre duas entradas já ordenadas pela chave do join. É eficiente quando as entradas já vêm ordenadas (ou quando ordenar é barato), e tende a ser estável para volumes grandes.
- Sinal de risco: necessidade de sort grande antes do merge join.
- Observe: se há sorts imediatamente abaixo do merge join.
Agregação (Aggregate / Group)
Agregação reduz muitas linhas em menos linhas (por exemplo, GROUP BY, COUNT, SUM). Pode aparecer em duas formas conceituais:
- Hash aggregate: usa hash para agrupar; bom sem ordenação, pode consumir memória.
- Sort/group aggregate: ordena por chave de agrupamento e agrega em sequência; pode exigir sort.
Como identificar gargalo: agregações que recebem muitas linhas (especialmente após joins) e que exigem sort/hash grande.
Custo estimado: o que significa (e o que não significa)
O custo estimado é uma medida relativa de trabalho que o otimizador usa para comparar alternativas. Ele não é tempo real em milissegundos, e não é comparável entre bancos diferentes. Ainda assim, é útil para localizar “onde o otimizador acha que está o trabalho”.
- Custo local: custo do operador em si (por exemplo, ordenar N linhas).
- Custo acumulado: custo do operador + custo de tudo abaixo dele (subárvore).
Como usar: encontre o operador (ou subárvore) com maior custo acumulado e verifique se ele também tem alto volume de linhas. Se o plano mostra percentuais por operador, use-os como pista, não como verdade absoluta.
Linhas estimadas vs. linhas retornadas: o termômetro de “plano saudável”
Muitos planos exibem linhas estimadas (o que o otimizador achou que passaria) e linhas reais/retornadas (o que realmente passou na execução). A comparação é uma das formas mais práticas de entender gargalos.
| Sintoma | O que costuma indicar | Onde olhar no plano |
|---|---|---|
| Estimou 100, retornou 100.000 | Subestimação: operador acima pode ficar muito mais caro (sort, hash, join) | Operador onde a divergência começa e seus pais (acima) |
| Estimou 100.000, retornou 100 | Superestimação: pode levar a escolher join/estratégia “pesada” sem necessidade | Escolha do tipo de join e presença de sort/hash desnecessários |
| Entrada enorme, saída pequena | Filtro tardio ou leitura pouco seletiva | Onde o filtro aparece e qual leitura o alimenta |
| Explosão de linhas após join | Join com baixa seletividade ou condição incompleta | Operador de join e cardinalidade de saída |
Passo prático: percorra o plano e marque o primeiro operador em que “linhas reais” divergem muito das “estimadas”. A partir dali, os custos e escolhas podem ter sido baseados em premissas erradas.
Como encontrar o passo mais caro (gargalo) sem se perder
Checklist rápido de gargalos comuns
- Sort grande (muitas linhas entrando) → custo alto e possível uso de disco.
- Hash join/aggregate grande → consumo de memória e custo elevado.
- Scan grande (tabela/índice) → muito I/O e CPU para filtrar depois.
- Nested loops com lado externo grande → repetição de trabalho no lado interno.
- Join que aumenta muito as linhas → multiplicação de trabalho para tudo acima.
Roteiro de leitura (modelo repetível)
Comece pelo objetivo do SELECT: o que a consulta quer entregar? Há
ORDER BY?GROUP BY?LIMIT/TOP? Isso antecipa a presença de sort e aggregate no topo.Localize a raiz do plano: o operador final (retorno/projeção). Em seguida, desça pela árvore para entender de onde vêm as linhas.
Siga a árvore de baixo para cima: para cada operador, anote: (a) tipo (scan/seek/filter/join/sort/aggregate), (b) linhas de entrada e saída, (c) custo (local e/ou acumulado).
Identifique onde está o maior volume de linhas: geralmente o gargalo está onde muitas linhas são processadas (mesmo que poucas sejam retornadas no final).
Procure por “pontos de amplificação”: joins que aumentam linhas, nested loops que repetem acessos, e operadores que exigem materialização (sort/hash).
Marque operadores “caros por natureza”: sort, hash join, hash aggregate. Pergunte: “quantas linhas entram aqui?” e “isso era inevitável?”.
Compare estimado vs real: se houver grande divergência, trate como pista de que o plano pode estar inadequado para os dados reais.
Escolha 1 gargalo principal: o operador com maior custo acumulado e/ou maior volume e que, se melhorado, reduz trabalho para toda a parte de cima do plano.
Exemplos conceituais (planos fictícios em texto)
Exemplo 1: Sort caro no topo
SELECT ... FROM Pedidos WHERE status = 'ABERTO' ORDER BY data_criacao DESC LIMIT 50 Plano (de baixo para cima): 1) Index Scan em idx_status (linhas: 2.000.000 lidas, 200.000 saem) 2) Filter (status='ABERTO') (linhas: 200.000 entram, 200.000 saem) 3) Sort (por data_criacao DESC) (linhas: 200.000 entram, 200.000 saem) [custo alto] 4) Limit 50 (linhas: 50 saem)Como ler: apesar de retornar 50 linhas, o plano ordena 200.000. O gargalo provável é o Sort, alimentado por um volume grande vindo do scan.
Exemplo 2: Nested loops com lado externo grande
SELECT ... FROM Clientes c JOIN Pedidos p ON p.cliente_id = c.id WHERE c.pais = 'BR' Plano: 1) Table Scan em Clientes (linhas: 5.000.000 lidas, 800.000 saem) 2) Nested Loops Join 2a) Lado externo: saída de Clientes (800.000 linhas) 2b) Lado interno: Index Seek em Pedidos por cliente_id (executa 800.000 vezes) 3) OutputComo ler: o seek em Pedidos parece eficiente, mas executar 800.000 vezes pode ser o gargalo. O ponto crítico é o volume do lado externo no nested loops.
Exemplo 3: Hash join grande e explosão de linhas
SELECT ... FROM ItensPedido i JOIN Produtos pr ON pr.id = i.produto_id WHERE i.data >= '2025-01-01' Plano: 1) Index/Range Scan em ItensPedido por data (linhas: 30.000.000 lidas, 10.000.000 saem) 2) Table Scan em Produtos (linhas: 2.000.000 lidas, 2.000.000 saem) 3) Hash Join (build: Produtos 2.000.000; probe: ItensPedido 10.000.000) [custo muito alto] 4) Output (linhas: 10.000.000 saem)Como ler: o hash join é caro porque constrói hash de 2 milhões de produtos e processa 10 milhões de itens. O gargalo é o Hash Join e o volume que chega nele.
Exemplo 4: Merge join exigindo sorts
SELECT ... FROM A JOIN B ON A.chave = B.chave Plano: 1) Scan em A (linhas: 5.000.000) 2) Sort em A por chave (linhas: 5.000.000) 3) Scan em B (linhas: 5.000.000) 4) Sort em B por chave (linhas: 5.000.000) 5) Merge Join (linhas: 5.000.000)Como ler: o merge join em si pode ser eficiente, mas os dois sorts podem dominar o custo. O gargalo pode estar nos sorts, não no join.
Exercícios guiados: encontre o gargalo no plano
Exercício 1: “Retorna pouco, trabalha muito”
Consulta: SELECT nome FROM Usuarios WHERE email LIKE '%@empresa.com' ORDER BY criado_em DESC LIMIT 20 Plano: 1) Table Scan em Usuarios (linhas: 12.000.000 lidas, 12.000.000 saem) 2) Filter (email LIKE '%@empresa.com') (linhas: 12.000.000 entram, 300.000 saem) 3) Sort (criado_em DESC) (linhas: 300.000 entram, 300.000 saem) 4) Limit 20- Qual operador é o gargalo mais provável: scan, filter ou sort? Por quê?
- Em qual ponto o volume de linhas cai de forma significativa?
- O
LIMIT 20ajuda em alguma coisa antes do sort? Explique com base no plano.
Exercício 2: “Nested loops suspeito”
Consulta: SELECT ... FROM Contas c JOIN Transacoes t ON t.conta_id = c.id WHERE c.status = 'ATIVA' Plano: 1) Index Scan em Contas por status (linhas estimadas: 50.000; reais: 900.000) 2) Nested Loops 2a) Externo: Contas (reais: 900.000) 2b) Interno: Index Seek em Transacoes por conta_id (reais: 900.000 execuções) 3) Output- Qual é o sinal mais forte de que o plano pode estar inadequado para os dados reais?
- O gargalo está mais associado ao tipo de join ou à estimativa? Justifique.
- Se você tivesse que investigar um único número primeiro, qual seria: linhas reais do externo, custo do seek interno, ou custo total do join?
Exercício 3: “Hash grande + agregação”
Consulta: SELECT cliente_id, COUNT(*) FROM Eventos WHERE data >= '2025-01-01' GROUP BY cliente_id Plano: 1) Index/Range Scan em Eventos por data (estimadas: 2.000.000; reais: 40.000.000) 2) Hash Aggregate (group by cliente_id) (entra: 40.000.000; sai: 3.000.000) [custo alto] 3) Output- Qual operador você marcaria como gargalo principal? Por quê?
- O problema parece ser mais “leitura” ou “processamento em memória”? Use as contagens de linhas para apoiar.
- Em que ponto do plano a divergência entre estimado e real começa?
Exercício 4: “Merge join com sorts escondidos”
Consulta: SELECT ... FROM Vendas v JOIN Lojas l ON v.loja_id = l.id WHERE v.data BETWEEN '2025-01-01' AND '2025-01-31' Plano: 1) Range Scan em Vendas por data (entra: 8.000.000; sai: 8.000.000) 2) Sort em Vendas por loja_id (entra: 8.000.000) [custo alto] 3) Scan em Lojas (entra: 50.000) 4) Sort em Lojas por id (entra: 50.000) 5) Merge Join (sai: 8.000.000)- O merge join é o vilão ou os sorts? Aponte o(s) operador(es) mais caro(s) conceitualmente.
- Qual sort é mais preocupante e por quê?
- Se você só pudesse otimizar um ponto, qual escolheria para reduzir custo global?