Dicas e truques de SQL
(github.com/ben-n93)- Uma lista de hábitos de escrita em SQL e padrões de consulta usados com frequência em trabalho de análise de dados, com a ressalva de que nem tudo pode se aplicar igualmente a todos os RDBMS
- Em termos de legibilidade, recomenda-se usar vírgulas no início da linha,
WHERE 1=1, indentação, CTE, comentários eUSINGpara tornar as consultas mais fáceis de ler e modificar - No processamento de dados, são mostrados exemplos práticos de sintaxes como anti-join,
QUALIFY,GROUP BY ROLLUPeEXCEPT, usadas para filtrar resultados, gerar totais e verificar diferenças entre tabelas - Em desempenho e precisão,
NOT INcomNULL, conversão implícita de tipo e conflitos de alias em campos calculados podem afetar o resultado ou a velocidade da consulta - Em consultas complexas, hábitos básicos como entender a ordem de execução, consultar a documentação, indicar a origem das colunas e usar nomes descritivos para consultas salvas são importantes para melhorar depuração e reutilização
Hábitos para melhorar a legibilidade ao escrever SQL
- Este repositório é uma coletânea de dicas e truques de SQL aprendidos ao longo de vários anos, focada no que é útil no dia a dia de analistas de dados e no que teria sido bom saber ao começar a usar SQL
- Algumas dicas podem não servir para todos os RDBMS
-
Vírgulas no início da linha e
ANDno início da linha- Recomenda-se usar vírgulas no início da linha em vez de vírgulas no fim para separar campos na cláusula
SELECT - Fica mais claro distinguir se é uma nova coluna ou apenas uma quebra de linha no código
- Mesmo com linhas de tamanhos diferentes, fica mais fácil encontrar vírgulas ausentes
- Pelo mesmo motivo, também é possível colocar
ANDno início das condições da cláusulaWHERE
- Recomenda-se usar vírgulas no início da linha em vez de vírgulas no fim para separar campos na cláusula
-
Facilitar testes de condições com
WHERE 1=1- Colocar a condição fictícia
1=1na cláusulaWHEREevita que a consulta quebre durante testes ao comentar condições - Mesmo se todas as condições forem comentadas, o
1=1continua ali e a consulta ainda pode ser executada
- Colocar a condição fictícia
-
Indentação e formatadores
-
Considere CTE em consultas complexas
- Quando uma inline view é aninhada em 2 ou 3 níveis ou mais, a consulta tende a ficar difícil de entender quando você a revisita semanas depois
- CTE é apresentada como uma forma de deixar consultas longas mais organizadas e de ajudar na reutilização e depuração
-
Comentários devem explicar o “porquê”
- Depois de algum tempo, pode ser difícil lembrar por que determinado processamento foi feito
- Em geral, comentários funcionam melhor explicando por que algo foi feito, e não apenas como o código funciona
- O exemplo mostra um comentário em uma condição que exclui conteúdo arquivado porque o novo CMS não consegue processar o formato de vídeos arquivados
-
Use
USINGpara joins com colunas de mesmo nome- Ao fazer join entre duas tabelas por colunas com o mesmo nome, usar
USINGpode deixar a expressão mais simples do queON USINGtambém remove duplicidade da coluna em comum no resultado, retornando apenas uma- Ao usar
ON, se você não indicar explicitamente a coluna em comum, pode ocorrer erro deambiguous column name
- Ao fazer join entre duas tabelas por colunas com o mesmo nome, usar
Sintaxes úteis para processamento de dados
-
Encontrar linhas ausentes em outra tabela com anti-join
- Anti-join é usado para retornar linhas que existem em uma tabela, mas não têm correspondência em outra
- O exemplo trata do caso de buscar apenas
video_idde conteúdos que não foram arquivados - Há várias formas de implementar isso
- Fazer
LEFT JOINe depois filtrar apenas as linhas cuja chave da tabela correspondente sejaNULL - Usar
NOT INcom subconsulta - Usar
NOT EXISTScom subconsulta correlacionada - Não se recomenda
NOT IN, pois ele pode não funcionar como esperado por causa de valoresNULL
-
Filtrar resultados de funções de janela com
QUALIFYQUALIFYpermite filtrar o resultado da consulta com base no resultado de funções de janela- Isso reduz a quantidade de linhas de código, já que o filtro pode ser feito sem inline view
- O exemplo seleciona os 10 principais mercados por produto com
DENSE_RANK()e depois aplica o filtro comQUALIFY - Há a limitação de que
QUALIFYaparentemente só está disponível em grandes data warehouses como Snowflake, Amazon Redshift e Google BigQuery
-
GROUP BYeORDER BYpor posição de coluna- Em vez do nome da coluna, dá para usar a posição da coluna como em
GROUP BY 1eORDER BY 2 - Isso pode ser útil em consultas temporárias ou pontuais
- Para código de produção, recomenda-se sempre referenciar diretamente os nomes das colunas
- Em vez do nome da coluna, dá para usar a posição da coluna como em
-
Gerar totais com
GROUP BY ROLLUPGROUP BY ROLLUPpode ser usado para gerar subtotais e totais gerais- O exemplo calcula a soma dos salários por departamento e cria também uma linha com o total geral
- A documentação do Transact-SQL explica que
ROLLUPcria grupos para combinações de expressões de coluna e reduz o número de grupos da direita para a esquerda para gerar subtotais e total geral - Aplicando
COALESCE, a linha de total pode ser exibida comoTotal - É preciso prestar atenção na coluna de ordenação para que a linha de total fique no fim do resultado
-
Encontrar diferenças entre dois conjuntos de resultados com
EXCEPTEXCEPTretorna linhas que estão no resultado da primeira consulta, mas não no da segunda- Usar
EXCEPTjunto comUNION ALLpermite validar se duas tabelas contêm os mesmos dados - Se nenhuma linha for retornada, as duas tabelas são idênticas
- Se houver linhas retornadas, elas são a causa da diferença
Padrões que prejudicam desempenho e precisão
-
Em colunas que podem ter
NULL,NOT EXISTSé melhor queNOT IN- Se a coluna comparada permite
NULL,NOT INem geral pode ser mais lento queNOT EXISTS - Isso ocorreu em Snowflake, e o Don’t Do This da PostgreSQL Wiki diz que
NOT IN (SELECT ...)não costuma ser bem otimizado NOT INtambém não funciona como esperado quando háNULLnos valores comparados- O fato de a coluna permitir
NULLnão significa que existam valoresNULLde fato, mas ao lidar com tabelas que você não pode alterar,NOT EXISTSpode ajudar a melhorar a performance
- Se a coluna comparada permite
-
Conversão implícita de tipo pode deixar a consulta lenta ou causar falhas
- Se você colocar na condição um valor com tipo diferente do da coluna, o banco pode tentar fazer uma conversão implícita de tipo
- O exemplo trata do caso de comparar o inteiro
200050com a colunavideo_id, que é do tipo string - Confiar em conversão implícita de tipo pode causar problemas
- Se houver valores que não possam ser convertidos, pode ocorrer erro
- A consulta pode ficar mais lenta por causa do trabalho extra de converter cada valor para o tipo especificado
- Você pode usar o mesmo tipo de dado da coluna ou, para evitar erros, funções como
TRY_TO_NUMBERdo Snowflake - O impacto no desempenho varia conforme o tamanho do conjunto de dados processado
Erros comuns
-
NOT INeNULLNOT INnão funciona quando háNULLnos valores comparados- Como
NULLrepresenta Unknown, o mecanismo SQL não consegue verificar que o valor inspecionado não está na lista - Nesse caso, usar
NOT EXISTSé uma alternativa
-
Conflito de alias em campo calculado
- Se você der a um campo calculado o mesmo nome de uma coluna existente, pode surgir um comportamento inesperado
- A documentação de
GROUP BYdo Snowflake diz que, se o nome na cláusulaGROUP BYcorresponder tanto ao nome da coluna quanto ao alias, o nome da coluna será usado - No exemplo, ao criar o alias
LEFT(product, 1) AS producte usarGROUP BY product, o agrupamento acontece pela colunaproductoriginal, e não pela primeira letra, retornando 3 linhas - Há duas soluções
- Usar um alias único, como
product_letter - Especificar a expressão, como em
GROUP BY LEFT(product, 1) - Problemas com alias também podem ocorrer em funções de janela
- No exemplo, um
CASEmuda a revenue deRobotpara 0, mas a alteração é aplicada depois da execução da função de janela, então o ranking sai diferente do esperado - Sempre que possível, use alias únicos ou coloque diretamente a expressão calculada no
ORDER BYda função de janela
-
Indique a qual tabela a coluna pertence
- Em consultas complexas com vários joins, você precisa conseguir rastrear problemas de valor até a tabela de origem
- Quando duas tabelas compartilham o mesmo nome de coluna, não indicar a origem pode fazer o RDBMS gerar erro
- O exemplo usa alias de tabela como
vc.video_idemetadata.seasonpara deixar clara a origem das colunas
Ordem de execução, documentação e nomes salvos
-
Entenda a ordem de execução do SQL
- Uma das orientações mais importantes para quem está aprendendo SQL é entender a ordem de execução das cláusulas
- Saber essa ordem pode mudar bastante a forma como você escreve consultas
- Como material de referência, é sugerido A beginner’s guide to the true order of SQL operations
-
Leia a documentação até o fim
- Houve um caso em Snowflake em que se usou
GREATEST()para retornar a data mais recente entre várias colunas de data GREATEST()retornaNULLse qualquer um dos argumentos forNULL- Se a documentação tivesse sido lida com mais atenção, seria possível usar
GREATEST_IGNORE_NULLS()em vez deCOALESCE(GREATEST(...), ...) - Em muitos casos, levar menos de um minuto para revisar a documentação evita o trabalho de descobrir por que algo está se comportando de forma diferente do esperado
- Houve um caso em Snowflake em que se usou
-
Use nomes descritivos para consultas salvas
- Para evitar a situação de não conseguir encontrar uma consulta que precisa ser executada novamente ou usada como referência, vale a pena salvá-la com um nome descritivo
- O nome salvo normalmente inclui o tema da consulta, o mês de execução e o nome de quem fez o pedido
- O exemplo segue o formato
Lapsed users analysis - 2023-09-01 - Olivia Roberts
2 comentários
As vírgulas iniciais foram todas escritas como vírgulas finais neste post. No original, elas aparecem no início.
Comentários no Hacker News
Minha dica adicional é esta: aprenda bem o servidor de banco de dados e verifique os planos de execução com frequência. Podem aparecer resultados inesperados, então é bom ajustar e verificar de novo
Em geral,
EXISTSé mais rápido queIN, eNOT EXISTSse comporta de forma diferente deEXCEPTno tratamento deNULL. Às vezes é muito mais rápido usar colunas de subconsulta na lista doSELECTdo que fazer join de tabelas e depois filtrar linhas com algo comoDISTINCT. Isso vale mesmo ao buscar mais de 10 valores da mesma tabela, e pode continuar valendo mesmo que o servidor de banco de dados suporte lateral join. Só que a subconsulta deve retornar no máximo uma linhaConsultas que não são pontuais não devem fazer varredura completa da tabela. A varredura de tabela de hoje pode virar a indisponibilidade de amanhã, então é preciso adicionar índices. Lembre também que a cláusula
GROUP BYnormalmente determina o uso de índicesSe você precisa filtrar por uma expressão, por exemplo verificar se uma substring é igual a determinado valor, pode adicionar uma coluna calculada e criar um índice nela. Alguns bancos de dados dão suporte direto a índices de expressão. Usar
UNION ALLem vez deORmuitas vezes pode deixar consultas complexas ou com várias condiçõesORmuito mais rápidasQuando o banco de dados não consegue escolher uma boa ordem de filtragem, também é útil fazer
JOINcom uma subconsulta para forçar a ordemUma coisa interessante no Postgres — e talvez outros bancos também sejam assim — é que uma operação
INSERT (SELECT ...)pode ficar quase linearmente mais rápida se você fizer sharding manual de acordo com o número de núcleos de CPU. Isso funcionava mesmo com cerca de 10 joins. Primeiro, veja oEXPLAINe encontre o join mais interno ou mais externo; depois, execute consultas paralelas separadas para cada intervalo de linhas (id >= start AND id < end). Usei muito essa abordagem num trabalho de 6 anos atrás por motivos estranhos. O Postgres 10+ adicionou paralelismo, mas, até onde sei, ainda não é tão avançado assimSELECTPor exemplo, se eu executar
SELECT column1, (SELECT column2, column3, ... FROM table_b WHERE table_a.id = table_b.a_id) FROM table_a, recebo, como esperado, “subquery must return only one column”. A ideia é retornar várias colunas como um registro/tipo composto?Não ficou imediatamente claro para mim por que a cláusula
GROUP BYnormalmente determina o uso de índices, mas, para quem tiver curiosidade, este artigo explica passo a passo: https://www.brentozar.com/archive/2015/06/indexing-for-group...EXPLAINe aprender a interpretá-lo com a ferramenta de sua preferência. Também é preciso monitorar as consultasNa minha startup anterior, instalamos o PgHero, e ele ajudou muito na otimização de desempenho e na definição de prioridades
Há consultas em que a varredura completa da tabela é a estratégia de acesso mais eficiente. Normalmente são consultas analíticas/de agregação que leem a tabela inteira; às vezes, até quando se busca apenas 50% de todas as linhas, a varredura de tabela é melhor.
Também não vejo bem como uma varredura de tabela somente leitura levaria a uma indisponibilidade, já que ela não bloqueia acessos simultâneos. A única desvantagem é a carga maior de I/O; se o servidor não aguenta isso, eu diria que ele já está seriamente subdimensionado
Os 3 exemplos da seção “legibilidade” são estranhos. Os 2 primeiros literalmente sacrificam a legibilidade para facilitar a escrita, e o último é um monstro difícil de ler que quase nem a indentação consegue salvar
Acho que desenvolvedores olham o histórico de commits tanto quanto olham o código-fonte em si
Já vi isso com frequência suficiente para não me incomodar muito mais
Não vejo problema
E não parece haver nada errado
SELECTem uma por linha, mas deixa linhas de 150 caracteres intactas? Isso é uma definição quebrada de legibilidade. E eu nem vou começar a falar das vírgulasEm revisão de código, ninguém enxerga direito linhas longas. Esse era o maior problema do AngularJS. Merges eram resolvidos errado e tudo quebrava, porque por volta da coluna 90 a visão fica turva. Já passei por mais de meia dúzia de equipes com revisão de código, e era sempre igual. Mesmo estando muito consciente desse problema e tentando evitá-lo, eu ainda erro mais ou menos metade das vezes que os outros
Vamos quebrar um pouco essas linhas. Especialmente se for para mostrar exemplos a outras pessoas
Dicas para lidar com stored procedures complexas são estas
WHERE. Se tentar fazerJOINou operações complexas, há grande chance de ocorrer timeoutSe você fizer o item 5 sem o item 6, é bem provável que não perceba que está fazendo algo que não é ótimo. Meu conselho é evitar otimização prematura, escrever primeiro da forma mais intuitiva e só otimizar quando necessário. O mais importante é não escrever SQL de forma procedural. Você descreve os dados que quer, não ordena ao engine como buscá-los
Por outro lado, a Microsoft continua colocando avisos para não tentar ajustá-lo, como se o query planner soubesse de tudo melhor do que ninguém
A versão do DB também pode influenciar
Não gosto de desenvolvimento feito “por via das dúvidas”. Isso vale para interfaces e também para placeholders como
where 1=1Faça quando for necessário. Não faça só porque talvez um dia no futuro venha a ser necessário. Código de produção não é lugar para deixar auxiliares de desenvolvimento. Durante o desenvolvimento, faça como quiser, mas em código de produção legibilidade e intenção clara são muito mais importantes
Mais uma coisa sobre “anti join”. Se você só quer verificar se existe uma linha que satisfaça a condição em outra tabela grande ou em uma subquery, é melhor usar EXISTS em vez de
INouLEFT JOINEXISTSretorna verdadeiro assim que encontra uma correspondência. No caso deLEFT JOINeIN, o engine reúne todos os resultados antes de avaliar(NOT) EXISTSproduziu um plano de execução melhor que(LEFT) JOINou(NOT) IN, ou o mesmo planoAlém disso, a intenção fica mais clara
Sobre “comente seu código”, pelo menos no MSSQL é comum recomendarem usar
/**/em vez de--nos comentários. Isso porque recursos como o Query Store muitas vezes armazenam queries sem quebras de linha; quando você pega a query de lá, precisa corrigir tudo manualmente em vez de simplesmente usar o formatador da IDEOBJECT_DEFINITIONselect name,cast((select OBJECT_DEFINITION(object_id) for xml path('')) as xml) from sys.proceduresComo as quebras de linha são preservadas, pode ser mais fácil organizar. Porém outros caracteres XML se quebram, como
>virando>. Outra opção é usarVARBINARYe algo que depois desfaça issoTodo mundo está surtando com a sugestão das vírgulas, mas acham que
1=1na cláusulaWHEREé uma boa ideia? Quando vejo isso em code review, não sei o que pensar do autorWHEREnão afetam outras linhas, então o code review fica mais fácilMas, se o motivo for adicionar condições dinâmicas como neste caso, no lugar onde trabalho a pessoa certamente seria demitida
Alguém poderia compartilhar uma orientação geral sobre onde traçar a linha entre acelerar as coisas via configuração do DB — uma abordagem quase de “comprar” — e “construir”, que na prática é implementar manualmente? Pela minha experiência limitada, DBAs competentes recebem salários muito mais altos e trabalham em outros lugares, então esse trabalho muitas vezes acaba caindo para o desenvolvedor da aplicação. Como foi dito acima, é importante conhecer o DB
Um exemplo típico são dados que se acumulam em grande volume com o tempo e em que os dados mais recentes são acessados com mais frequência. Um DBA pode manter o acesso rápido com particionamento ou índices parciais, mas um desenvolvedor de aplicação também pode mover registros para uma tabela de arquivo separada em segundo plano, mantendo suporte a recursos como busca final no conjunto completo de dados. Também fica a impressão de que ferramentas poderiam automatizar razoavelmente o trabalho inicial de dividir uma tabela em várias no momento adequado, como quando se está limitado pela falta de recursos de um DB em nuvem
Outra opção de gerenciamento é armazenar todos os blobs/arquivos grandes em um banco de dados separado, ou no sistema de arquivos, para usar uma configuração de armazenamento diferente. Isso também pode ser absorvido pelo DB ou tratado manualmente
No extremo, parece que dá até para chegar a implementar índices por conta própria. Seria ter uma tabela enorme com uma chave primária autoincremental e muitas colunas, e criar uma tabela separada com esse ID e algumas colunas pesquisáveis. Poderia até chegar a busca full-text ou vetores
Uma dica útil ao implementar manualmente o padrão de view materializada no MSSQL 2016+ é usá-lo junto com troca de partições. Isso é bem explicado e implementado em https://github.com/cajuncoding/SqlBulkHelpers?tab=readme-ov-.... Foi uma pequena biblioteca que encontrei por acaso, a mais útil comercialmente para mim, mas com ranking de busca baixo e poucas estrelas, focada em inserções em massa no MSSQL com .NET. Acho que é um bom exemplo de traçar bem a linha entre comprar/construir por meio da automação da troca de partições
O que faltou: é preciso parar de usar
SELECT *. Quase certamente você não precisa da largura inteira da tabela e, ao fazer isso, aumenta os dados a filtrar e transmitir, além de impedir um recurso bacana: semi-joinsSe você é desenvolvedor, sim.
SELECT *tem armadilhas, e quase sempre você deve explicitar as colunas ou usar um query builder que faça isso por vocêMas, se você é analista, a vida é curta, e às vezes você pode não querer digitar todas as colunas.
SELECT *também é aceitávelPode ser um pouco fora do tema, mas é aceitável um mantenedor simplesmente fechar pull requests sem qualquer comentário ou discussão?
Pergunto como alguém que às vezes contribuiu, ou tentou contribuir, com o repositório
Exemplo: https://github.com/ben-n93/SQL-tips-and-tricks/pulls?q=is%3A...