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.
p95 ao vivo -uptime -