Mihael Costa/índice
← índice
PERFORMANCE

SQL Performance Lab

Rode a query lenta e a otimizada lado a lado, contra um banco de verdade, e leia os dois planos.

  • .NET 10
  • Dapper
  • EF Core
  • PostgreSQL
  • React

O que é

Cinco pares de queries. Cada par faz a mesma pergunta ao mesmo banco e devolve as mesmas linhas — só muda como a pergunta foi escrita. Você aperta executar, as duas rodam contra 2,65 milhões de linhas de pedidos, clientes e itens, e a página mostra dois tempos e dois planos de execução reais, capturados com EXPLAIN (ANALYZE, BUFFERS).

Nada aqui é gravado: texto dos cenários, contagens de linhas e parâmetros são lidos do banco, e os números chegam quando você clica. Se o serviço estiver dormindo, a página diz isso em vez de cair para um valor digitado antes.

Os cinco

Paginação profunda. OFFSET 500000 não pula linhas, ele as produz e joga fora. A versão com keyset carrega a última linha da página anterior como cursor. Mesmo índice, mesmas vinte linhas, e a varredura cai de 500.020 para 20.

Agregação sem índice de cobertura. date_trunc('month', placed_at) = … é verdade para as mesmas linhas que um predicado de intervalo, mas o planner não consegue inverter uma função sem índice. A correção é o intervalo mais INCLUDE; a contagem de páginas cai duas ordens de grandeza.

Join pela coluna sem índice. order_items carrega uma cópia desnormalizada de order_id sem índice, como tabelas reais ficam depois de uma importação. Join por ela força um hash join na tabela inteira; pela coluna indexada dá um nested loop sobre a chave primária.

Linha a agonizante linha. Um loop em plpgsql que calcula total corrente re-soma o trimestre a cada uma das noventa iterações — rápido statement a statement, por isso nunca aparece num slow query log. Uma window function faz isso numa passada só.

O N+1 do EF Core. Quarenta clientes, quarenta e uma idas ao banco; a correção é um Include filtrado. Mas peça os cinco pedidos mais recentes por cliente e o EF Core 8 traduz o Take num ROW_NUMBER() sobre a tabela inteira, ficando mais lento que o loop original. “Use Include” não é a lição; ler o SQL que seu ORM produziu é.

Sobre o engine

Meu trabalho de produção é SQL Server; isto roda em Postgres porque a demo tinha que custar zero e o plano gratuito da Neon não pede cartão.

Cada cenário carrega um bloco de SQL Server equivalente: como o operador se chama lá, o que muda na sintaxe, e onde o diagnóstico deixa de valer. INCLUDE, por exemplo, veio do SQL Server e o Postgres pegou emprestado na v11; já o SQL Server não tem construtor de valor de linha (um predicado de keyset vira uma disjunção), seu frame padrão é RANGE (que despeja em disco onde ROWS não despeja), e a causa cotidiana de uma varredura inesperada é CONVERT_IMPLICIT, não um índice faltando.

Conhecer os dois planners vale mais do que só rodar o que combina com o currículo.

O que custa deixar isso aberto

É um endpoint público que executa SQL: as queries são constantes de compilação no servidor e a requisição carrega só um id de cenário; a role do banco é somente leitura, restrita a um schema, com statement_timeout de cinco segundos; execuções têm rate limit por IP, e o serviço é limitado a quatro queries concorrentes porque o compute do plano gratuito é um quarto de vCPU.

O dataset foi dimensionado para essa restrição: 273MB de tabelas e índices, medidos, contra uma cota de 0,5GB.

Demo ao vivo

p95 ao vivo -uptime -