SQL: consultando dados
SQL é uma linguagem declarativa: você descreve o que quer, nunca como obter. Não há laços, não há índices no seu código, não há ordem de leitura — tudo isso é decisão do otimizador. É a linguagem de programação mais duradoura em uso: nascida em 1974, e ainda o jeito padrão de conversar com dados meio século depois.
Descreva o resultado, não o caminho
Numa linguagem comum, "os três maiores salários" é um algoritmo: percorra a lista, guarde os
maiores, ordene, corte. Em SQL é uma descrição:
ORDER BY salario DESC LIMIT 3. Você não diz se deve usar um índice, ordenar tudo ou
manter um heap de tamanho 3 — quem decide é o otimizador, que conhece o tamanho da tabela e os
índices existentes, e pode até mudar de estratégia amanhã quando os dados crescerem.
Essa é a força e a armadilha do SQL. A força: consultas escritas em 1990 ficaram mais rápidas
sozinhas, à medida que os otimizadores melhoraram. A armadilha: quando uma consulta está lenta, o
código não diz o porquê — é preciso perguntar ao banco o que ele decidiu fazer, com
EXPLAIN. Esse é o assunto da lição 12.4.
for em volta de uma consulta, quase sempre existe uma
consulta só que faz o trabalho inteiro.
Um motor SQL rodando no seu navegador
Escreva consultas de verdade sobre a tabela funcionarios. Além do resultado, o widget
mostra a ordem em que cada cláusula é executada e quantas linhas sobram em cada
etapa — que é justamente o que falta na cabeça de quem está aprendendo.
Um motor SQL de verdade, pequeno, rodando no seu navegador sobre a tabela funcionarios (8 linhas). Escreva a consulta e veja não só o resultado, mas a ordem em que o banco executa cada cláusula.
| nome | cargo | salario |
|---|---|---|
| Ana Souza | Engenheira | 12000 |
| Carla Dias | Engenheira | 14000 |
| Elisa Prado | Engenheira | 15500 |
| Fabio Nunes | Gerente | 18000 |
A ordem de execução (que não é a ordem de escrita)
-- A ordem em que você ESCREVE: SELECT depto, COUNT(*) FROM funcionarios WHERE ano >= 2018 GROUP BY depto HAVING COUNT(*) > 2 ORDER BY COUNT(*) DESC LIMIT 5; -- A ordem em que o banco EXECUTA: -- 1. FROM → pega as linhas da tabela -- 2. WHERE → filtra LINHA a linha -- 3. GROUP BY → junta linhas em grupos -- 4. HAVING → filtra GRUPOS -- 5. SELECT → calcula as colunas de saída -- 6. ORDER BY → ordena o resultado -- 7. LIMIT → corta -- Isso explica dois erros que todo mundo comete: -- -- ✗ WHERE COUNT(*) > 2 -- O WHERE roda ANTES do agrupamento; nesse momento COUNT(*) -- ainda não existe. Filtro de grupo é HAVING. -- -- ✗ SELECT salario * 12 AS anual FROM f WHERE anual > 100000 -- O apelido "anual" nasce no SELECT, que roda DEPOIS do WHERE. -- No ORDER BY ele já existe e funciona.
O básico: SELECT, WHERE, DISTINCT
-- SELECT é uma operação sobre CONJUNTOS, não um laço. -- Você descreve o resultado que quer; o banco decide como obtê-lo. SELECT nome, salario * 12 AS anual -- projeção: quais colunas FROM funcionarios -- origem WHERE depto = 'Produto' -- seleção: quais linhas AND ano >= 2020; -- Operadores úteis no WHERE WHERE salario BETWEEN 8000 AND 12000 -- intervalo (inclusivo) WHERE depto IN ('Dados', 'Plataforma') -- pertence à lista WHERE nome LIKE 'A%' -- % = qualquer coisa WHERE nome ILIKE '%souza%' -- ILIKE ignora maiúsculas (Postgres) WHERE email IS NULL -- nunca "= NULL" -- DISTINCT elimina linhas repetidas do RESULTADO SELECT DISTINCT depto FROM funcionarios;
Agregações, GROUP BY e HAVING
-- Agregações reduzem MUITAS linhas a UMA. SELECT COUNT(*) FROM funcionarios; -- quantas linhas SELECT COUNT(email) FROM funcionarios; -- quantas NÃO-NULAS ⚠ SELECT AVG(salario) FROM funcionarios; SELECT SUM(salario) FROM funcionarios; SELECT MIN(ano), MAX(ano) FROM funcionarios; -- GROUP BY faz isso UMA VEZ POR GRUPO SELECT depto, COUNT(*) AS pessoas, ROUND(AVG(salario)) AS media FROM funcionarios GROUP BY depto; -- REGRA: no SELECT com GROUP BY, toda coluna precisa estar -- no GROUP BY ou dentro de uma agregação. Faz sentido: -- se o grupo "Produto" tem 4 pessoas, qual "nome" o banco -- deveria devolver numa linha só? A pergunta não tem resposta. -- (O MySQL historicamente aceitava e devolvia um nome ARBITRÁRIO. -- Um erro seria muito melhor que uma resposta inventada.) -- WHERE filtra linhas; HAVING filtra grupos. SELECT depto, COUNT(*) FROM funcionarios WHERE ano >= 2018 -- antes: descarta LINHAS GROUP BY depto HAVING COUNT(*) > 2; -- depois: descarta GRUPOS
COUNT que já derrubou muito relatório: COUNT(*) conta linhas; COUNT(coluna) conta valores não nulos
daquela coluna. Se metade dos e-mails for NULL, COUNT(email) devolve metade do total —
e não avisa. O mesmo vale para AVG e SUM: NULLs são ignorados, então a
média de [10, NULL, 20] é 15, não 10. Muitas vezes é o que você quer; o problema é quando não é, e
ninguém percebe.
Subconsultas, CTEs e funções de janela
-- SUBCONSULTA: uma consulta dentro da outra SELECT nome, salario FROM funcionarios WHERE salario > (SELECT AVG(salario) FROM funcionarios); -- CTE (WITH): a mesma coisa, com nome e legível de cima para baixo. -- Prefira CTEs a subconsultas aninhadas — o custo é o mesmo e a -- consulta passa a ser lida como uma sequência de passos. WITH medias AS ( SELECT depto, AVG(salario) AS media FROM funcionarios GROUP BY depto ) SELECT f.nome, f.salario, m.media FROM funcionarios f JOIN medias m ON m.depto = f.depto WHERE f.salario > m.media; -- FUNÇÕES DE JANELA: agregam SEM colapsar as linhas. -- Repare: GROUP BY devolveria 3 linhas; isto devolve as 8, -- cada uma acompanhada da média do seu departamento. SELECT nome, depto, salario, AVG(salario) OVER (PARTITION BY depto) AS media_depto, RANK() OVER (PARTITION BY depto ORDER BY salario DESC) AS posicao FROM funcionarios; -- Funções de janela resolvem em uma consulta o que normalmente -- viraria um laço na aplicação: rankings, totais acumulados, -- médias móveis, comparação com a linha anterior (LAG/LEAD).
Consultando de verdade
Mini projeto: baixe um dataset público em CSV (Kaggle, dados.gov.br, ou o clássico
de voos) e carregue no SQLite com .import ou com pandas. Depois responda em SQL,
sem nenhum laço em Python: total de registros por categoria, top 10 por algum valor, média por mês, e
quantos valores nulos existem em cada coluna. Compare o tempo com a versão equivalente em Python
puro.
Projeto principal: escreva um pequeno relatório de negócio sobre esse dataset usando
CTEs — nada de subconsulta aninhada. Cada CTE deve ser um passo nomeado do raciocínio
(WITH vendas_mes AS (...), media_movel AS (...)). Inclua pelo menos uma função de
janela: um ranking dentro de cada categoria ou um total acumulado ao longo do tempo. O objetivo é que
outra pessoa consiga ler a consulta de cima para baixo e entender o raciocínio.
Desafio extra: implemente um mini interpretador de SQL, como o desta página.
Comece com SELECT colunas FROM tabela WHERE condicao sobre uma lista de dicionários em
Python. Depois acrescente ORDER BY, LIMIT e, por último,
GROUP BY com agregações. Escrever isso ensina a ordem de execução de um jeito que
nenhuma explicação alcança: você precisa aplicar o WHERE antes de agrupar, senão o código não
funciona.
Teste sua intuição
COUNT(*) e COUNT(email)?
OVER (PARTITION BY ...)) fazem de diferente do GROUP BY?
Onde você encontra isso
Todo dashboard que você já viu
Metabase, Looker, Power BI, Tableau: por baixo da interface de arrastar e soltar, todos geram SQL. Quando um gráfico demora, o problema quase sempre é a consulta gerada — e saber ler SQL é o que separa "o dashboard está lento" de "este GROUP BY está varrendo 200 milhões de linhas sem filtro de data".
SQL injection
Montar SQL concatenando string com entrada do usuário permite que a entrada vire código. É a vulnerabilidade web mais antiga que ainda aparece no top 10 da OWASP. A defesa não é filtrar caracteres: é usar consultas parametrizadas, em que os valores viajam separados do comando e o banco jamais os interpreta como sintaxe.
SQL fora dos bancos de dados
A ideia de "descreva o resultado, deixe o motor decidir o caminho" se espalhou: Spark SQL processa petabytes em clusters, Flink e ksqlDB rodam SQL sobre fluxos contínuos de eventos, e o BigQuery varre terabytes em segundos — todos com a mesma sintaxe. Aprender SQL uma vez rende em contextos que não existiam quando a linguagem foi criada.