54 pontos por GN⁺ 2025-10-19 | 5 comentários | Compartilhar no WhatsApp
  • Antipadrões de SQL tornam a manutenção de queries e pipelines de dados mais difícil e causam desempenho mais lento do que o esperado
  • Uso excessivo de CASE WHEN, aplicar funções em colunas indexadas, SELECT *, uso excessivo de DISTINCT, views e subqueries aninhadas, estruturas de dependência profundas são exemplos representativos
  • A maior parte dos problemas surge de soluções temporárias motivadas por pressão de velocidade e prazo e, no longo prazo, prejudica tanto a confiabilidade dos dados quanto a velocidade de desenvolvimento
  • Como soluções, são necessários joins bem definidos, uso de tabelas de dimensão, remoção de aninhamentos desnecessários e limpeza periódica de views
  • SQL deve ser tratado não como um simples script, mas como código de produção gerenciado em equipe, e um projeto inicial voltado à legibilidade reduz retrabalho

Introdução

  • Hoje vamos explicar alguns casos frequentes e de grande impacto entre os antipadrões de SQL
  • Esses problemas provocam um ciclo vicioso, com queda na confiabilidade dos dados e redução da velocidade de desenvolvimento de queries
  • A lista abaixo não cobre todos os casos; para um entendimento mais profundo, recomendamos o livro de Bill Karwin

CASE WHEN excessivamente complexo

  • Em sistemas de grande porte, o CASE WHEN é usado com frequência para converter códigos de status (ex.: 1=sem estoque) em algo legível por humanos
  • Adicionar essa lógica de CASE WHEN apenas a uma única view para acelerar o desenvolvimento de dashboards ou relatórios é, no longo prazo, um antipadrão
  • Isso gera cópia e cola de lógica duplicada, problemas de interpretação inconsistente e torna todo o ambiente de queries confuso
  • A solução é criar e reutilizar uma tabela de dimensão (dimension table) ou uma view compartilhada separada para converter os códigos de status

Uso de funções em colunas indexadas

  • Ao aplicar uma função em uma coluna indexada, como em “WHERE UPPER(name) = 'ABC'”, a eficiência do índice se perde
    • Em SQL Server e outros bancos, isso pode causar um full table scan desnecessário
  • A solução é indexar separadamente a coluna com a função aplicada ou transformar o valor de entrada para simplificar a condição da query

Uso de SELECT * em views

  • Ao desenvolver uma view, usar SELECT * pode parecer prático, mas se a estrutura (schema) mudar, a view pode quebrar facilmente
  • Como até colunas desnecessárias acabam incluídas, surgem dependências não intencionais e problemas de desempenho; por isso, é preciso selecionar as colunas explicitamente

Uso excessivo de DISTINCT para “resolver” duplicações

  • Quando resultados duplicados aparecem por causa de joins incorretos, usar SELECT DISTINCT como solução temporária esconde problemas de integridade dos dados
    • A causa raiz está em condições de join incompletas ou em erros na definição dos relacionamentos (1:1, 1:N etc.)
    • A solução correta é reforçar a lógica de join para deixar clara a definição dos relacionamentos e garantir a consistência relacional antes de agregações ou relatórios

Empilhamento de views (Excessive View Layer Stacking)

  • Quando várias equipes reutilizam views existentes e continuam criando novas views por cima delas, a cadeia de dependências se torna complexa e o desempenho piora drasticamente
    • O debugging fica difícil, e expandir a query passa a ser tão complicado quanto uma “escavação arqueológica”
  • É necessário, periodicamente, achatar (flatten) a lógica de transformação e, para operações complexas, materializar (materialize) em uma view base ou tabela claramente definida

Subqueries excessivamente profundas

  • Subqueries profundamente aninhadas, com mais de 3 ou 4 níveis, reduzem a legibilidade e dificultam o debugging
    • Existem até casos com subqueries de mais de 5.000 linhas
  • Com CTE (Common Table Expression), fica mais fácil separar as etapas lógicas e aumentar a clareza (readability) da query

Conclusão

  • SQL parece simples na superfície, mas à medida que o sistema cresce, sua complexidade também aumenta
  • A maioria dos antipadrões não nasce de má intenção, e sim do resultado de compromissos para entregar resultados rápidos (velocidade, prazo, soluções temporárias)
  • Se SQL for gerenciado como código (controle de versão, code review, design claro), é possível garantir produtividade e confiabilidade ao mesmo tempo no longo prazo
  • Investir alguns minutos no design inicial, pensando em clareza e consistência, reduz muito o retrabalho e a confusão no futuro

5 comentários

 
aer0700 2025-10-20

Como agora não temos tempo, dizemos para apagar só o incêndio imediato e reescrever depois, mas isso vai se acumulando e acaba virando um terrível inferno de queries. Eu mesmo já criei várias. Mesmo sabendo que esse “depois” para reescrevê-las nunca chega.

 
firefoxsaiko123 2025-10-20

Cof cof...

 
ilikeall 2025-10-20

"A maioria dos problemas são soluções paliativas causadas por pressão de velocidade e prazos"
buá buá..

 
GN⁺ 2025-10-19
Comentários do Hacker News
  • Quando vejo DISTINCT sendo usado em uma query, suspeito que o autor não entendeu direito o modelo de dados, teoria dos conjuntos, ou talvez nenhum dos dois
    • Às vezes, DISTINCT também pode ser sinal de um esquema excessivamente normalizado. Por exemplo, não acho necessário criar uma tabela como addresses_cities só para evitar registrar nomes de cidades duplicados
    • Minha experiência é quase a mesma. Mas recentemente houve um caso em que, mesmo com todos os joins corretos, colocar DISTINCT dentro de uma CTE melhorou bastante o desempenho. Parece que o query planner consegue otimizar quando a unicidade dos registros é garantida
    • Também recebi um feedback parecido, de que não era bom eu adicionar LIMIT 1 em queries nas quais eu esperava no máximo um resultado. Mas em tabelas grandes, o banco tende a continuar varrendo a tabela inteira mesmo depois de encontrar o registro desejado (sqlite, mysql e postgresql fazem isso)
    • A pergunta é como saber se dá para omitir DISTINCT com segurança em uma query SELECT x FROM t. Mesmo que você veja no esquema de t que x tem restrição PRIMARY ou UNIQUE, alguém pode remover a restrição logo depois. Aí duplicatas começam a aparecer e você fica tentando entender o motivo. SQL não é uma linguagem de conjuntos (set), e sim de bags. Em runtime, ela só encontra a relação t e o atributo x e retorna isso. Pode haver duplicatas, ou até mudança de tipo. Se você quer um Set, precisa sempre declarar DISTINCT. O query planner então evita a deduplicação em runtime se houver UNIQUE ou PRIMARY
    • Em Cypher acontece o oposto. Ao lidar com dados complexos no neo4j, nós duplicados entram no resultado com muita facilidade, então DISTINCT é essencial. Principalmente ao usar relações de comprimento variável, sem DISTINCT a query fica lenta e cheia de duplicatas
  • Escrevi um tutorial em duas partes, com cerca de 9000 palavras, sobre como projetar a estrutura correta de queries sem DISTINCT
    https://kb.databasedesignbook.com/posts/systematic-design-of-join-queries/
    • Ótimo artigo. Salvei nos favoritos. E percebi que isso na verdade também é um livro
  • Uma coisa pouco mencionada é a query para encontrar “o que não existe”. Por exemplo, usar != ou NOT IN (...) quase sempre é ineficiente (embora possa ser aceitável quando outras condições já reduziram bastante o conjunto de resultados). E é importante entender como o banco trata null. Se null é igual a string vazia, se null == null, isso pode variar de banco para banco
    • Sobre tratamento de null e indexação: os bancos que usei não indexam valores null, então queries como WHERE col IS NULL funcionam de forma ineficiente mesmo quando existe índice em col. Se isso for realmente necessário, recomendo criar uma coluna char(1) ou bit para indicar se col é null, e indexar esse campo
    • Você disse que != ou NOT IN (...) quase sempre são ineficientes; queria entender por quê. Se os valores à direita forem constantes, isso deveria virar um lookup em tabela hash e normalmente ser eficiente. Existe alguma alternativa mais eficiente?
  • Não acho que todos os “antipadrões” apresentados sejam realmente antipadrões. O problema de condições de query não combinarem com índices, no fim das contas, vem da falta de entendimento sobre como índices funcionam. E muitos dos problemas citados aqui têm mais a ver com projeto de schema do que com SQL em si. Se DISTINCT é necessário, talvez o desenho da primary key esteja errado. Empilhar views demais costuma indicar que as tabelas-base foram mal projetadas. Uma boa modelagem de banco evita esses problemas antes que eles aconteçam
  • Esses “antipadrões” na verdade são só soluções de contorno simples para limitações de design da linguagem SQL, ou para coisas que nunca foram bem projetadas nela. Estou criando uma nova linguagem que roda sobre bancos SQL, e quero oferecer alternativas melhores para cada um desses problemas. Ainda está incompleta e mal documentada, mas adoraria receber feedback em https://lutra-lang.org
    • A expressão “banco SQL” é ambígua. SQL foi implementada não só em bancos relacionais, mas também em alguns não relacionais. Especialistas já reconhecem os problemas de SQL há muito tempo, e já existiram alternativas como Tutorial D, de Chris Date e Hugh Darwen. Mesmo assim, por causa de décadas de código e ferramentas acumuladas em SQL, substitutos não conseguiram se firmar. Graças ao SQL, eu tive décadas de estabilidade no emprego e renda constante; então, embora uma linguagem melhor seja necessária, também vejo esse cenário de forma positiva
    • O projeto parece interessante. Se ganhar mais maturidade, certamente vou acompanhar
  • O maior antipadrão é não tratar SQL como uma linguagem de programação de verdade, e sim só como uma linguagem de consulta. Recomendo manter um estilo de código consistente com indentação, agrupar partes logicamente relacionadas, transformar subqueries em CTEs e escrever bons comentários. Meu estilo está aqui: https://bentilly.blogspot.com/2011/02/sql-formatting-style.html
    • Acho que esse tipo de discussão sobre estilo de código quase não faz sentido sem uma ferramenta de lint adequada
  • O maior segredo para acelerar minhas queries e reduzir o uso de recursos do servidor foi torná-las mais sargable
    https://en.wikipedia.org/wiki/Sargable
    https://www.brentozar.com/blitzcache/non-sargable-predicates/
    • Fico curioso em que comunidades essa palavra “sargable” é realmente usada. Trabalho com SQL há mais de 20 anos, mas quase nunca vi isso em manuais, Stack Overflow ou HN. Queria saber se é mais comum em algum banco, empresa ou comunidade open source específica
    • Procurando a origem de sargable, esta resposta no Stack Overflow ajuda: https://dba.stackexchange.com/a/217983
      A palavra sargable é um portmanteau de “Search ARGument ABLE”
  • Muito do abuso de CASE WHEN pode ser resolvido consolidando a lógica em um UDF (User Defined Function)
    Usar funções sobre colunas indexadas é um sinal de que a query não é sargable
    Em vez de abusar de DISTINCT, para deduplicar fan-out derivado de joins de acordo com o grão da tabela, uma query como
    ROW_NUMBER() OVER (PARTITION BY <grain> ORDER BY <deterministic sort>) = 1
    
    pode ser útil. Alguns bancos oferecem o recurso QUALIFY, que deixa a query bem mais limpa
    explicação sobre sargable
    QUALIFY no Redshift
    • Problemas de falta de sargabilidade podem ser resolvidos facilmente com expression index. Pelo menos no sqlite, acho que sim
  • Há casos em que aninhar views é realmente necessário. No nosso software de POS, usamos muitas views aninhadas para criar uma view backbone que permita visualizar transações de forma limpa de uma só vez. Caso contrário, precisaríamos escrever where diferentes para cada tabela e tratar toda hora condições como void/devolução/cancelamento. Se houver mudanças, teríamos de corrigir dezenas de views/procedures. No nosso caso, usar views aninhadas é muito mais prático
  • O problema de usar função em coluna indexada precisa de uma explicação mais clara. Quando você aplica uma função numa coluna indexada, a eficiência do índice se perde e, na prática, acaba ocorrendo full scan, o que deixa tudo lento. Aprendi isso na pele
    • Há uma documentação bem conhecida sobre isso: https://use-the-index-luke.com/sql/where-clause/obfuscation
    • A solução proposta (por exemplo, adicionar um índice sobre a coluna UPPER(name)) não é a melhor pelo menos no MS SQL Server. Não sei sobre suporte em outros bancos, mas uma solução melhor é criar uma computed column com COLLATE sem diferenciação de maiúsculas e minúsculas
      ALTER TABLE example ADD name_ci AS name COLLATE SQL_Latin1_General_CI_AS;
      
      (ajuste conforme preferir)
    • Há um erro de digitação no blog citado. A primeira linha deveria estar em maiúsculas. Se o índice já foi criado sobre os dados com a função aplicada, então a query não fará full scan. Claro que, neste exemplo, usar uma collation case-insensitive desde o começo seria melhor, mas, no geral, a ideia principal continua válida
    • “Aprendi isso na pele” parece ser o lema dos desenvolvedores SQL. De todo modo, o SQL mudou pouco e com bastante estabilidade ao longo do tempo, então conhecer essas armadilhas de antemão continua sendo útil por muitos anos
 
ahwjdekf 2025-10-21

Faltou a coisa mais importante.

  • usar ORM