- 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
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.
Cof cof...
"A maioria dos problemas são soluções paliativas causadas por pressão de velocidade e prazos"
buá buá..
Comentários do Hacker News
DISTINCTsendo usado em uma query, suspeito que o autor não entendeu direito o modelo de dados, teoria dos conjuntos, ou talvez nenhum dos doisDISTINCTtambém pode ser sinal de um esquema excessivamente normalizado. Por exemplo, não acho necessário criar uma tabela comoaddresses_citiessó para evitar registrar nomes de cidades duplicadosDISTINCTdentro de uma CTE melhorou bastante o desempenho. Parece que o query planner consegue otimizar quando a unicidade dos registros é garantidaLIMIT 1em 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,mysqlepostgresqlfazem isso)DISTINCTcom segurança em uma querySELECT x FROM t. Mesmo que você veja no esquema detquextem restriçãoPRIMARYouUNIQUE, 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çãote o atributoxe retorna isso. Pode haver duplicatas, ou até mudança de tipo. Se você quer umSet, precisa sempre declararDISTINCT. O query planner então evita a deduplicação em runtime se houverUNIQUEouPRIMARYneo4j, nós duplicados entram no resultado com muita facilidade, entãoDISTINCTé essencial. Principalmente ao usar relações de comprimento variável, semDISTINCTa query fica lenta e cheia de duplicatasDISTINCThttps://kb.databasedesignbook.com/posts/systematic-design-of-join-queries/
!=ouNOT 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 tratanull. Senullé igual a string vazia, senull == null, isso pode variar de banco para banconulle indexação: os bancos que usei não indexam valoresnull, então queries comoWHERE col IS NULLfuncionam de forma ineficiente mesmo quando existe índice emcol. Se isso for realmente necessário, recomendo criar uma colunachar(1)oubitpara indicar secolénull, e indexar esse campo!=ouNOT 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?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çamhttps://en.wikipedia.org/wiki/Sargable
https://www.brentozar.com/blitzcache/non-sargable-predicates/
sargable, esta resposta no Stack Overflow ajuda: https://dba.stackexchange.com/a/217983A palavra
sargableé um portmanteau de “Search ARGument ABLE”CASE WHENpode 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 pode ser útil. Alguns bancos oferecem o recursoQUALIFY, que deixa a query bem mais limpaexplicação sobre sargable
QUALIFY no Redshift
sqlite, acho que simwherediferentes 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áticoUPPER(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 comCOLLATEsem diferenciação de maiúsculas e minúsculas (ajuste conforme preferir)Faltou a coisa mais importante.