Capa do Ebook gratuito SQL para Análise de Dados no Dia a Dia: Consultas, Relatórios e Insights com Dados Reais

SQL para Análise de Dados no Dia a Dia: Consultas, Relatórios e Insights com Dados Reais

Novo curso

26 páginas

Noções de performance: filtros, índices, planos e como evitar SELECT *

Capítulo 20

Tempo estimado de leitura: 0 minutos

+ Exercício

Performance em SQL, no contexto de análise de dados, é a capacidade de responder perguntas com rapidez e previsibilidade, consumindo o mínimo possível de recursos (CPU, memória, disco e tempo de execução). Mesmo quando você não administra o banco, suas consultas competem com outras rotinas (ETL, APIs, relatórios) e podem ficar lentas ou até causar bloqueios. Este capítulo foca em noções práticas: como filtros afetam custo, como índices ajudam (e quando não ajudam), como ler planos de execução e por que evitar SELECT *.

O que torna uma consulta lenta (visão prática)

Em geral, uma consulta fica lenta por um ou mais motivos:

  • Leitura excessiva de dados: varrer uma tabela grande inteira quando só precisa de uma fração.
  • Filtros pouco seletivos ou aplicados tarde: filtrar depois de juntar/agregar aumenta o volume intermediário.
  • Operações que impedem uso de índice: funções na coluna, conversões implícitas, padrões com % no início.
  • Ordenações e agregações grandes: ORDER BY, GROUP BY, DISTINCT podem exigir ordenar ou criar estruturas em memória/disco.
  • Junções caras: juntar tabelas grandes sem bons predicados ou com chaves não indexadas.
  • Retornar colunas demais: aumenta I/O e rede; também pode impedir “cobertura” por índice.

Uma regra mental útil: custo ≈ (quantidade de linhas lidas) × (custo por linha) + (custo de ordenar/agregar/juntar). A maior alavanca costuma ser reduzir linhas lidas e reduzir colunas retornadas.

Filtros: seletividade, sargabilidade e “filtrar cedo”

Seletividade: o quanto o filtro reduz o conjunto

Um filtro seletivo reduz muito o número de linhas. Exemplo: filtrar por order_id (único) costuma ser altamente seletivo; filtrar por status = 'PAID' pode ser pouco seletivo se a maioria está paga. Índices tendem a ajudar mais quando o filtro é seletivo.

Sargabilidade: filtros que permitem uso eficiente de índice

Um predicado “sargável” (search-argument-able) é aquele que o otimizador consegue transformar em busca eficiente no índice. Alguns padrões comuns que prejudicam:

Continue em nosso aplicativo

Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.

ou continue lendo abaixo...
Download App

Baixar o aplicativo

  • Função na coluna: WHERE DATE(created_at) = '2025-01-01' (tende a impedir uso de índice em created_at).
  • Conversão implícita: comparar coluna numérica com texto pode forçar conversão e varredura.
  • LIKE com coringa no início: LIKE '%gmail.com' geralmente não usa índice B-tree.

Prefira reescrever para intervalos e comparações diretas na coluna:

-- Evite (função na coluna pode impedir índice em created_at)  SELECT COUNT(*)  FROM orders  WHERE DATE(created_at) = DATE '2025-01-01';  -- Prefira (intervalo sargável)  SELECT COUNT(*)  FROM orders  WHERE created_at >= TIMESTAMP '2025-01-01 00:00:00'    AND created_at <  TIMESTAMP '2025-01-02 00:00:00';

Filtrar cedo: reduza o volume antes de operações caras

Em consultas analíticas, é comum juntar e só depois filtrar. Em muitos casos, filtrar antes reduz drasticamente o custo. A ideia é aplicar restrições o quanto antes, especialmente em tabelas grandes (fatos).

-- Exemplo: reduzir o conjunto de pedidos antes de juntar com itens  WITH filtered_orders AS (    SELECT order_id, customer_id, created_at    FROM orders    WHERE created_at >= TIMESTAMP '2025-01-01 00:00:00'      AND created_at <  TIMESTAMP '2025-02-01 00:00:00'      AND status = 'PAID'  )  SELECT fo.customer_id, COUNT(DISTINCT fo.order_id) AS paid_orders  FROM filtered_orders fo  JOIN order_items oi ON oi.order_id = fo.order_id  GROUP BY fo.customer_id;

Mesmo que o otimizador consiga “empurrar” filtros automaticamente, escrever de forma explícita ajuda a evitar ambiguidades e facilita a leitura do plano.

Índices: o que são e como impactam consultas

Um índice é uma estrutura auxiliar (geralmente B-tree) que permite localizar linhas sem varrer a tabela inteira. Pense nele como um “mapa” ordenado por uma ou mais colunas. Em análise do dia a dia, você não precisa decorar detalhes internos, mas precisa entender três efeitos práticos:

  • Acelera filtros em colunas indexadas (especialmente seletivos).
  • Acelera junções quando as colunas de junção estão indexadas (principalmente do lado “procurado”).
  • Pode acelerar ordenações se a ordem do índice coincide com o ORDER BY.

Quando um índice pode não ajudar

  • Filtro pouco seletivo: se 90% das linhas atendem ao filtro, pode ser mais barato varrer a tabela.
  • Funções/conversões na coluna: como visto, podem impedir o uso.
  • Retornar muitas colunas: mesmo encontrando as linhas via índice, o banco pode precisar buscar a linha inteira na tabela (lookup), o que pode ser caro.
  • Consultas que agregam quase tudo: se você precisa ler a maior parte da tabela para somar/contar, o índice pode ter pouco efeito.

Índices compostos: ordem das colunas importa

Índices com múltiplas colunas funcionam melhor quando o filtro usa um “prefixo” da ordem do índice. Exemplo: índice em (created_at, status) ajuda bem em filtros por created_at e por created_at + status, mas pode não ajudar tanto em status sozinho (depende do banco e do plano).

Como analista, você pode usar essa noção para conversar com o time de dados/DBA: “minhas consultas mais frequentes filtram por período e status; faz sentido um índice composto começando por data?”.

Índices e colunas de junção

Em junções, é comum que o banco escolha entre estratégias como nested loop, hash join ou merge join. Um índice na coluna de junção pode tornar nested loop viável (buscar rapidamente correspondências). Sem índice, o banco pode preferir hash join (construindo uma tabela hash em memória) ou até varrer mais dados.

Se você percebe que uma junção entre uma tabela grande e outra média está lenta, uma pergunta prática é: “a coluna usada no ON está indexada em pelo menos um lado, especialmente no lado onde o banco precisa procurar?”

Planos de execução: como ler o essencial

O plano de execução é a explicação de como o banco pretende executar sua consulta (estimado) ou como executou (real). A forma de obter varia por banco, mas os conceitos são parecidos. Procure por comandos como EXPLAIN ou EXPLAIN ANALYZE (quando disponível) para ver:

  • Tipo de leitura: varredura completa (table/seq scan) vs. busca por índice (index seek/scan).
  • Ordem das operações: onde filtra, onde junta, onde agrega.
  • Estimativa vs. realidade (no plano real): linhas estimadas e linhas retornadas.
  • Pontos caros: operações com alto custo, tempo ou muitas linhas processadas.

Passo a passo para diagnosticar performance com plano

Use este roteiro sempre que uma query “parece simples” mas está lenta:

1) Confirme o objetivo e reduza o escopo

Antes de otimizar, garanta que a consulta está respondendo a pergunta certa. Em seguida, rode uma versão mínima: limite o período, reduza colunas, remova ordenações temporariamente. Isso ajuda a identificar o componente caro.

-- Versão mínima para medir leitura e filtros  SELECT COUNT(*)  FROM orders  WHERE created_at >= TIMESTAMP '2025-01-01 00:00:00'    AND created_at <  TIMESTAMP '2025-02-01 00:00:00';

2) Rode o plano e identifique o “maior consumidor”

Procure no plano o operador que mais lê linhas ou mais tempo consome. Frequentemente será uma varredura grande, uma ordenação (sort) ou uma agregação pesada.

3) Verifique se o filtro está sargável

Se o plano mostra varredura completa apesar de existir índice, revise o WHERE em busca de funções na coluna, conversões e padrões de LIKE.

4) Cheque cardinalidade estimada vs. real

Quando o plano real mostra grande diferença entre linhas estimadas e reais, o otimizador pode escolher uma estratégia ruim (por exemplo, nested loop quando deveria ser hash join). Isso pode indicar estatísticas desatualizadas ou filtros complexos. Como analista, você pode:

  • Reescrever filtros para ficarem mais diretos.
  • Evitar expressões muito complexas no predicado.
  • Solicitar atualização de estatísticas ao time responsável (quando aplicável).

5) Avalie junções e ordenações

Se a parte cara é uma junção, verifique:

  • Se o predicado de junção está correto e restritivo.
  • Se há filtros aplicados antes da junção para reduzir linhas.
  • Se as colunas de junção têm índices adequados.

Se a parte cara é um SORT, pergunte:

  • Você precisa mesmo ordenar tudo ou só um top N?
  • O ORDER BY poderia ser feito depois de reduzir o conjunto?
  • Existe índice que já entrega na ordem desejada?

Como evitar SELECT * (e por que isso melhora performance)

SELECT * parece conveniente, mas tem custos reais:

  • Mais I/O e rede: você lê e transfere colunas que não usa.
  • Mais CPU: o banco precisa materializar mais dados.
  • Menos chance de usar índice “cobrindo”: se você seleciona só colunas que estão no índice, o banco pode evitar buscar a linha completa na tabela (dependendo do banco).
  • Fragilidade: se alguém adiciona uma coluna grande (ex.: JSON, texto longo), sua query passa a ficar mais lenta sem você mudar nada.
  • Ambiguidade em joins: em consultas com múltiplas tabelas, * pode trazer colunas duplicadas e confusas.

Prática recomendada: selecione apenas o necessário

-- Evite  SELECT *  FROM customers  WHERE customer_id = 123;  -- Prefira  SELECT customer_id, full_name, email, created_at  FROM customers  WHERE customer_id = 123;

Passo a passo para substituir SELECT * com segurança

1) Liste as colunas realmente usadas

Verifique quais colunas aparecem em:

  • Filtros (WHERE)
  • Junções (ON)
  • Agrupamentos (GROUP BY)
  • Ordenações (ORDER BY)
  • Cálculos e expressões no SELECT

2) Selecione apenas colunas de saída (e, se necessário, chaves técnicas)

Se você precisa de uma coluna apenas para juntar e não para exibir, avalie se ela precisa estar no SELECT. Muitas vezes não precisa.

-- Você pode usar a chave para JOIN sem retorná-la  SELECT c.customer_id, c.full_name, SUM(oi.quantity * oi.unit_price) AS revenue  FROM customers c  JOIN orders o ON o.customer_id = c.customer_id  JOIN order_items oi ON oi.order_id = o.order_id  WHERE o.created_at >= TIMESTAMP '2025-01-01 00:00:00'    AND o.created_at <  TIMESTAMP '2025-02-01 00:00:00'  GROUP BY c.customer_id, c.full_name;

3) Atenção a colunas grandes

Campos como descrições longas, blobs, JSON e logs podem ser muito pesados. Se você precisa deles, traga apenas quando necessário e, se possível, em uma consulta separada para poucos registros (por exemplo, após identificar IDs relevantes).

-- Primeiro: encontre os IDs relevantes (leve)  SELECT o.order_id  FROM orders o  WHERE o.created_at >= TIMESTAMP '2025-01-01 00:00:00'    AND o.created_at <  TIMESTAMP '2025-02-01 00:00:00'    AND o.status = 'CHARGEBACK'  ORDER BY o.created_at DESC  LIMIT 50;  -- Depois: busque detalhes pesados só para esses IDs (quando necessário)  SELECT order_id, chargeback_reason, raw_payload_json  FROM orders  WHERE order_id IN (/* lista dos 50 IDs */);

Ordenação, DISTINCT e paginação: custos escondidos

ORDER BY: ordenar tudo é caro

ORDER BY pode exigir ordenar um grande volume de linhas. Se você precisa apenas dos “primeiros N”, combine com LIMIT (ou equivalente) e garanta que o filtro reduza o conjunto antes da ordenação.

-- Top N com filtro antes  SELECT order_id, customer_id, total_amount, created_at  FROM orders  WHERE created_at >= TIMESTAMP '2025-01-01 00:00:00'    AND created_at <  TIMESTAMP '2025-02-01 00:00:00'  ORDER BY total_amount DESC  LIMIT 100;

Se houver índice que suporte a ordenação (por exemplo, em total_amount ou em combinação com o filtro), o banco pode evitar um sort completo. Caso contrário, ele pode ordenar em memória e, se não couber, “derramar” para disco (muito mais lento).

DISTINCT: remédio que pode sair caro

DISTINCT força o banco a eliminar duplicidades, frequentemente com sort ou hash. Se você está usando DISTINCT para “consertar” duplicidade causada por join, é melhor atacar a causa (chave correta, granularidade correta) do que pagar o custo toda vez. Use DISTINCT quando a pergunta realmente exige unicidade e você entende a origem das duplicidades.

Paginação: OFFSET grande tende a degradar

Paginar com OFFSET alto (ex.: página 1000) pode ser caro porque o banco precisa percorrer e descartar muitas linhas. Para extrações e relatórios, prefira paginação por chave (keyset pagination) quando aplicável, usando um marcador (por exemplo, created_at + order_id) para buscar o “próximo bloco”.

-- Exemplo conceitual de paginação por chave  SELECT order_id, created_at, total_amount  FROM orders  WHERE (created_at, order_id) < (TIMESTAMP '2025-01-31 10:00:00', 987654)  ORDER BY created_at DESC, order_id DESC  LIMIT 100;

Checklist prático de performance para consultas do dia a dia

  • Evite SELECT *: traga só colunas necessárias, especialmente em tabelas largas.
  • Filtre cedo: reduza linhas antes de joins, agregações e ordenações.
  • Escreva filtros sargáveis: evite função na coluna; prefira intervalos e comparações diretas.
  • Cuidado com LIKE: padrões com % no início tendem a não usar índice B-tree.
  • Desconfie de ORDER BY sem LIMIT em tabelas grandes.
  • Use EXPLAIN/EXPLAIN ANALYZE: identifique varreduras grandes, sorts e discrepâncias de cardinalidade.
  • Junções: confirme colunas de junção, filtros antes do join e possibilidade de índices.
  • Teste incrementalmente: comece com contagens e amostras pequenas, depois adicione complexidade.

Mini laboratório: otimizando uma consulta típica (passo a passo)

Imagine a necessidade de listar pedidos pagos do último mês com informações do cliente e valor total, para exportar a um relatório. Uma versão ingênua pode ser lenta por trazer colunas demais e filtrar de forma pouco eficiente.

Passo 1: versão ingênua (problemas comuns)

SELECT *  FROM orders o  JOIN customers c ON c.customer_id = o.customer_id  WHERE DATE(o.created_at) >= DATE '2025-01-01'    AND DATE(o.created_at) <= DATE '2025-01-31'    AND o.status = 'PAID'  ORDER BY o.created_at DESC;

Problemas: SELECT * (colunas demais), função DATE() na coluna (pode impedir índice), intervalo com <= no fim do dia (pode ser confuso), ordenação possivelmente grande.

Passo 2: tornar o filtro sargável e correto por intervalo

SELECT *  FROM orders o  JOIN customers c ON c.customer_id = o.customer_id  WHERE o.created_at >= TIMESTAMP '2025-01-01 00:00:00'    AND o.created_at <  TIMESTAMP '2025-02-01 00:00:00'    AND o.status = 'PAID'  ORDER BY o.created_at DESC;

Melhora: o filtro por data vira um intervalo direto na coluna.

Passo 3: remover SELECT * e trazer apenas o necessário

SELECT    o.order_id,    o.created_at,    o.total_amount,    o.status,    c.customer_id,    c.full_name,    c.email  FROM orders o  JOIN customers c ON c.customer_id = o.customer_id  WHERE o.created_at >= TIMESTAMP '2025-01-01 00:00:00'    AND o.created_at <  TIMESTAMP '2025-02-01 00:00:00'    AND o.status = 'PAID'  ORDER BY o.created_at DESC;

Melhora: menos dados trafegando e menor custo de materialização.

Passo 4: reduzir antes de juntar (quando fizer sentido)

Se orders é muito grande e customers também, filtrar pedidos primeiro pode diminuir o custo da junção. Em muitos bancos isso é otimizado automaticamente, mas estruturar ajuda a leitura e pode evitar surpresas.

WITH paid_orders AS (    SELECT order_id, customer_id, created_at, total_amount, status    FROM orders    WHERE created_at >= TIMESTAMP '2025-01-01 00:00:00'      AND created_at <  TIMESTAMP '2025-02-01 00:00:00'      AND status = 'PAID'  )  SELECT    po.order_id, po.created_at, po.total_amount, po.status,    c.customer_id, c.full_name, c.email  FROM paid_orders po  JOIN customers c ON c.customer_id = po.customer_id  ORDER BY po.created_at DESC;

Passo 5: validar no plano

Agora rode o plano e verifique:

  • O filtro por created_at e status está sendo aplicado antes da junção?
  • Há varredura completa em orders ou uso de índice?
  • O custo maior está no sort do ORDER BY? Se sim, você precisa mesmo dessa ordenação para exportação, ou pode ordenar no consumidor (BI/planilha) após extrair um conjunto menor?

Agora responda o exercício sobre o conteúdo:

Ao tentar melhorar a performance de uma consulta analítica que está lenta, qual mudança tende a aumentar a chance de o banco usar um índice em uma coluna de data?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Filtros sargaveis permitem busca eficiente no indice. Ao usar um intervalo direto na coluna (sem funcao ou conversao), o otimizador tende a conseguir aplicar o indice, reduzindo leituras e custo.

Próximo capitúlo

Notas de compatibilidade entre MySQL, PostgreSQL e SQL Server nas funções mais usadas

Arrow Right Icon
Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.