Trilha 12 · Bancos de dados

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.

① Intuição

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.

A ideia central é conjunto, não laço. Todo comando SQL opera sobre conjuntos de linhas de uma vez. O reflexo de quem vem de linguagens imperativas — buscar id por id num laço — é o pior erro de desempenho possível, porque paga a latência de ida e volta ao banco mil vezes em vez de uma. Se você está escrevendo um for em volta de uma consulta, quase sempre existe uma consulta só que faz o trabalho inteiro.
② Visualização interativa

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.

ou Ctrl+Enter
Ordem real de execução (que não é a ordem em que você escreve):
FROM8 linhas
carrega a tabela funcionarios
WHERE4 linhas
filtra linha a linha: salario > 10000
SELECT4 linhas
projeta as colunas nome, cargo, salario
nomecargosalario
Ana SouzaEngenheira12000
Carla DiasEngenheira14000
Elisa PradoEngenheira15500
Fabio NunesGerente18000
4 linha(s) retornada(s)
③ Explicação técnica

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
O detalhe do 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).
④ Projeto para programar

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.

⑤ Exercícios rápidos

Teste sua intuição

Qual é a ordem real de execução de uma consulta SQL?
Qual é a diferença entre WHERE e HAVING?
Qual é a diferença entre COUNT(*) e COUNT(email)?
O que funções de janela (OVER (PARTITION BY ...)) fazem de diferente do GROUP BY?
⑥ Aplicações no mundo real

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.

← Anterior: O modelo relacional Próxima: JOINs →