Skip to content

SQL

SQL (Structured Query Language) é a linguagem padrão para conversar com um banco de dados relacional — criar/alterar tabelas, inserir, consultar, atualizar e remover dados. Os exemplos aqui usam MySQL, mas a sintaxe central é a mesma (com pequenas variações) em Oracle, PostgreSQL, SQL Server, ...

Definição: Convenção de maiúsculas em SQL

Não é uma regra do banco, mas uma convenção amplamente adotada: palavras-chave da linguagem em maiúsculas (SELECT, CREATE TABLE, WHERE, ...) e nomes que você escolheu (banco, tabela, coluna) em minúsculas. Isso deixa claro, ao ler um comando, o que é sintaxe da linguagem e o que é específico do seu modelo.

Criando e selecionando um banco

CREATE DATABASE controle_de_gastos;
USE controle_de_gastos;

CREATE DATABASE cria um banco novo (é comum ter um banco por projeto/aplicação); USE diz ao servidor qual banco usar nos comandos seguintes.

Criando uma tabela

CREATE TABLE compras (
    id INT AUTO_INCREMENT PRIMARY KEY,
    valor DECIMAL(18,2),
    data DATE,
    observacoes VARCHAR(255),
    recebida TINYINT
);

Toda tabela precisa de pelo menos uma coluna, e cada coluna declara um tipo:

Tipo Guarda
INT número inteiro
DECIMAL(precisao, escala) número decimal exato — precisao é o total de dígitos, escala é quantos ficam depois da vírgula
DATE uma data (yyyy-MM-dd)
VARCHAR(tamanho) texto de tamanho variável, até o máximo informado
TINYINT inteiro pequeno — usado como substituto de boolean (0/1), já que bancos relacionais tradicionalmente não têm um tipo booleano nativo

PRIMARY KEY marca a coluna como chave primária (ver Modelagem de Dados); AUTO_INCREMENT faz o banco gerar o próximo número sozinho a cada INSERT — você nunca informa o id manualmente.

Definição: Identificadores sem acento

Nomes de banco, tabela e coluna devem evitar acentos e caracteres especiais (observacoes, não observações) — problemas de encoding entre sistemas operacionais/ferramentas diferentes são uma fonte comum (e evitável) de bugs.

Para ver a estrutura de uma tabela já criada:

DESC compras;

E para apagar uma tabela inteira (dados e estrutura, sem confirmação):

DROP TABLE compras;

Para adicionar uma coluna a uma tabela que já existe (em vez de recriar do zero):

ALTER TABLE compras ADD COLUMN id INT AUTO_INCREMENT PRIMARY KEY;

Inserindo dados: INSERT INTO

INSERT INTO compras (valor, data, observacoes, recebida)
VALUES (20, '2016-01-05', 'Lanchonete', 1);

Dois detalhes que geram erro com frequência para quem está começando:

  • Formato de data: o padrão SQL é yyyy-MM-dd (ano-mês-dia) — não o formato brasileiro dd/MM/yyyy. É esse padrão (ISO 8601) que funciona de forma inequívoca entre bancos e localidades diferentes.
  • Separador decimal: ponto (915.50), não vírgula — 915,50 seria interpretado como dois valores separados.

A lista de colunas entre parênteses ((valor, data, observacoes, recebida)) é opcional, mas sem ela o INSERT assume a ordem exata em que as colunas foram declaradas na tabela — omiti-la é arriscado se você não tiver certeza dessa ordem, e obrigatório se quiser inserir os valores numa ordem diferente da declarada.

Consultando dados: SELECT

SELECT * FROM compras;

O * seleciona todas as colunas; FROM indica de qual tabela. O resultado de um SELECT é sempre uma nova tabela (temporária, só para leitura) com as linhas que combinam com a consulta.

Filtrando resultados: WHERE

SELECT * FROM compras WHERE valor < 500;

WHERE filtra quais linhas entram no resultado, usando os operadores de comparação usuais: = (igual), <, >, <=, >=. Texto é comparado entre aspas simples (observacoes = 'Lanchonete').

Para combinar mais de uma condição, AND (as duas precisam ser verdadeiras) e OR (pelo menos uma precisa ser verdadeira):

SELECT * FROM compras WHERE valor > 1500 AND recebida = 0;
SELECT * FROM compras WHERE valor < 500 OR valor > 1500;

Definição: Cuidado com AND em condições que se excluem

Um erro comum de quem está começando: tentar WHERE valor < 500 AND valor > 1500 esperando "valores fora da faixa 500-1500". Isso nunca retorna nada — nenhum valor pode ser, ao mesmo tempo, menor que 500 e maior que 1500. Quando as condições descrevem alternativas (uma coisa ou outra), o operador certo é OR, não AND. AND é para quando a mesma linha precisa satisfazer as duas condições simultaneamente.

Para buscar um pedaço de um texto (em vez de uma igualdade exata), LIKE com o caractere curinga % (substitui qualquer sequência de caracteres, inclusive vazia):

SELECT * FROM compras WHERE observacoes LIKE 'Parcela%'; -- começa com "Parcela"
SELECT * FROM compras WHERE observacoes LIKE '%de%';       -- contém "de" em qualquer posição

Para filtrar um intervalo de valores, BETWEEN a AND b é equivalente a >= a AND <= b, mas mais legível — funciona tanto para números quanto para datas:

SELECT valor, observacoes FROM compras WHERE valor BETWEEN 1000 AND 2000;

Para negar uma condição qualquer, existe o operador NOT:

SELECT * FROM compras WHERE NOT valor = 108;

Atualizando dados: UPDATE

UPDATE compras SET valor = 1500 WHERE id = 11;

UPDATE tabela SET coluna = valor WHERE condição — para atualizar mais de uma coluna de uma vez, separe por vírgula dentro do SET (não use AND — AND é sintaxe de WHERE, não de SET):

UPDATE compras SET valor = 1500, observacoes = 'Reforma de quartos' WHERE id = 11;

O valor atribuído pode usar o valor atual de qualquer coluna da mesma linha — útil para aplicar um cálculo a várias linhas de uma vez, em vez de calcular manualmente e atualizar uma por uma:

UPDATE compras SET valor = valor * 1.1 WHERE id >= 11 AND id <= 14; -- +10% em cada uma

Removendo dados: DELETE

DELETE FROM compras WHERE id = 11;

Definição: Cuidado — UPDATE/DELETE sem WHERE afeta a tabela inteira

A regra de ouro para essas duas instruções: escreva e confira o WHERE antes de rodar o UPDATE/DELETE. Sem WHERE, um UPDATE atualiza todas as linhas da tabela com o mesmo valor, e um DELETE apaga todas as linhas — sem aviso, sem confirmação, e (na maioria dos SGBDs) sem undo fácil depois. Um jeito seguro de trabalhar: escreva a condição do WHERE primeiro, teste-a com um SELECT, e só então acople o UPDATE/DELETE na frente dela.

NULL: ausência de valor

Por padrão, uma coluna aceita NULL — a ausência de qualquer valor. Um erro comum é confundir NULL com "texto vazio" ('') ou com 0: os três são coisas diferentes. NULL significa não se sabe/não se aplica, não "vazio" nem "zero".

Como NULL não é um valor de verdade, = não funciona para compará-lo — é preciso IS NULL (ou IS NOT NULL):

SELECT * FROM compras WHERE observacoes IS NULL;

Constraints: regras de integridade

Constraints são restrições que o banco garante sozinho, sem depender de código da aplicação lembrar de validar. A mais comum é impedir valores NULL numa coluna que não faz sentido ficar vazia:

-- numa tabela nova:
CREATE TABLE compras (
    valor DECIMAL(18,2) NOT NULL,
    data DATE NOT NULL,
    ...
);

-- numa tabela que já existe:
ALTER TABLE compras MODIFY COLUMN observacoes VARCHAR(255) NOT NULL;

ALTER TABLE ... MODIFY COLUMN altera a definição de uma coluna já existente (sem perder os dados já cadastrados) — preferível a apagar (DROP TABLE) e recriar a tabela do zero, que descartaria todos os registros.

Valores padrão: DEFAULT

Quando a maioria dos registros deveria começar com o mesmo valor numa coluna (em vez de deixá-la NULL até alguém preencher), DEFAULT define esse valor automaticamente quando o INSERT não o informa:

ALTER TABLE compras MODIFY COLUMN recebida TINYINT(1) DEFAULT 0;

INSERT INTO compras (valor, data, observacoes) VALUES (150, '2016-01-04', 'Compra de teste');
-- "recebida" vira 0 automaticamente, sem precisar informar

Funções de agregação e GROUP BY

Funções de agregação calculam um resultado a partir de várias linhas: SUM (soma), COUNT (quantidade), AVG (média), entre outras.

SELECT SUM(valor) FROM compras;              -- soma de todos os valores
SELECT COUNT(*) FROM compras WHERE recebida = 1; -- quantas compras recebidas

Definição: Por que uma função de agregação colapsa o resultado numa linha só

SELECT recebida, SUM(valor) FROM compras; não devolve "a soma de cada grupo de recebida" como se poderia esperar — devolve uma única linha, com a soma de tudo e um valor de recebida arbitrário (qualquer um dos existentes). É assim porque uma função de agregação, por definição, colapsa várias linhas em uma só — para agrupar por uma coluna específica, é preciso dizer isso explicitamente com GROUP BY:

SELECT recebida, SUM(valor) AS soma FROM compras GROUP BY recebida;
-- agora sim: uma linha por valor distinto de "recebida"

AS renomeia a coluna do resultado (um alias) — sem ele, o nome da coluna calculada seria o próprio texto da função (SUM(valor)), pouco legível.

Ao agrupar por mais de uma característica (ex.: mês e ano), todas as colunas do SELECT que não são agregação precisam estar no GROUP BY:

SELECT MONTH(data) AS mes, YEAR(data) AS ano, recebida, SUM(valor) AS soma
FROM compras
GROUP BY recebida, mes, ano;

YEAR(coluna) e MONTH(coluna) extraem o ano/mês de uma coluna DATE.

Definição: MAX/MIN, e como funções de agregação tratam NULL

Além de SUM/COUNT/AVG, MAX e MIN retornam o maior e o menor valor de um grupo. De modo geral, funções de agregação ignoram valores NULL — COUNT(coluna) conta só os valores não nulos daquela coluna, diferente de COUNT(*) (ver nota mais abaixo sobre JOIN + agregação). Ao agrupar, o banco pode usar um índice já existente sobre a coluna agrupada (evitando reordenar os dados) ou, na ausência de um, ordenar os dados primeiro para então agrupá-los.

Ordenando resultados: ORDER BY

SELECT MONTH(data) AS mes, YEAR(data) AS ano, recebida, SUM(valor) AS soma
FROM compras
GROUP BY recebida, mes, ano
ORDER BY ano, mes;

ORDER BY prioriza as colunas na ordem em que aparecem no comando — no exemplo acima, ordena por ano primeiro, e só usa mes para desempatar linhas do mesmo ano. Trocar a ordem dos argumentos muda o critério de prioridade da ordenação.

Relacionando tabelas: FOREIGN KEY e JOIN

Para o banco impor a regra de que uma chave estrangeira (ver Modelagem de Dados) só aponte para um registro que realmente existe, criamos uma constraint FOREIGN KEY:

ALTER TABLE compras
ADD CONSTRAINT fk_compradores FOREIGN KEY (id_compradores)
REFERENCES compradores (id);

Definição: Integridade referencial

Depois que essa constraint existe, o banco recusa qualquer INSERT/UPDATE em compras que tente usar um id_compradores que não exista de fato na tabela compradores — e, na direção contrária, normalmente também impede apagar um comprador que ainda tenha compras associadas a ele. Se já existirem dados inconsistentes na tabela antes de criar a constraint (um id_compradores "órfão", sem comprador correspondente), a própria criação da FOREIGN KEY falha até que esses dados sejam corrigidos.

Para efetivamente combinar os dados das duas tabelas relacionadas numa única consulta, usamos JOIN ... ON:

SELECT * FROM compras JOIN compradores ON compras.id_compradores = compradores.id;

ON diz como as tabelas se relacionam — qual coluna de uma corresponde a qual coluna da outra.

Definição: Produto cartesiano — o erro de listar tabelas sem condição de junção

Uma forma antiga (e hoje desencorajada) de combinar tabelas é listá-las direto no FROM, separadas por vírgula, sem indicar como se relacionam:

SELECT * FROM compras, compradores; -- sem condição nenhuma
Isso gera o produto cartesiano: cada linha de uma tabela é combinada com todas as linhas da outra — 46 compras × 2 compradores dá 92 linhas, a maioria sem sentido nenhum (uma compra "pareada" com um comprador que não é o dela). O JOIN ... ON explícito é a forma correta e clara de dizer qual combinação faz sentido.

Restringindo valores de uma coluna: ENUM

Além de NOT NULL e FOREIGN KEY, o MySQL oferece ENUM — restringe uma coluna a um conjunto fixo de valores possíveis:

ALTER TABLE compras ADD COLUMN forma_pagto ENUM('BOLETO', 'CREDITO');

INSERT INTO compras (..., forma_pagto) VALUES (..., 'BOLETO'); -- ok
INSERT INTO compras (..., forma_pagto) VALUES (..., 'DINHEIRO'); -- valor fora do ENUM

Definição: ENUM não é padrão ANSI SQL

Diferente de FOREIGN KEY, NOT NULL ou JOIN (que fazem parte do padrão SQL, suportado por qualquer banco relacional), ENUM é uma extensão específica do MySQL — outros bancos resolvem essa mesma necessidade de forma diferente (ou nem oferecem um equivalente direto). Vale conferir a documentação do SGBD que você usa antes de depender de um recurso assim.

SQL mode: comportamento estrito do servidor

O MySQL tem configurações de modo que mudam como ele reage a valores inválidos. Por padrão, um INSERT com um valor fora do ENUM pode ser aceito silenciosamente (virando um valor vazio, com só um aviso) — para forçar o banco a recusar (erro, não aviso), habilita-se o modo estrito:

SET SESSION sql_mode = 'STRICT_ALL_TABLES'; -- só para esta sessão/conexão
SET GLOBAL sql_mode = 'STRICT_ALL_TABLES';  -- para todas as conexões futuras

SET SESSION vale só enquanto durar a conexão atual; ao reconectar, o servidor volta ao modo padrão configurado globalmente — por isso, para um comportamento consistente sempre, o ajuste deve ser feito com SET GLOBAL (ou na configuração do servidor).

Apelidando tabelas: alias

Em consultas com JOIN, repetir o nome inteiro da tabela em toda coluna (aluno.nome, aluno.id, ...) é cansativo. Um alias — um apelido curto, declarado logo após o nome da tabela — resolve isso:

SELECT a.nome FROM aluno a
JOIN matricula m ON m.aluno_id = a.id;

aluno a define a como apelido de aluno; a partir daí, a.coluna é equivalente a aluno.coluna. Alias também evitam ambiguidade quando duas tabelas na mesma consulta têm uma coluna de mesmo nome (ex.: id em ambas).

Combinando mais de duas tabelas

Um JOIN pode ser encadeado quantas vezes forem necessárias, cada um com seu próprio ON:

SELECT a.nome, c.nome FROM aluno a
JOIN matricula m ON m.aluno_id = a.id
JOIN curso c ON m.curso_id = c.id;

Isso combina aluno com curso, através da tabela associativa matricula no meio — o padrão típico para consultar uma relação many to many (ver Modelagem de Dados).

Verificando existência de relação: EXISTS/NOT EXISTS

Uma pergunta comum é "quais registros têm (ou não têm) um relacionado" — por exemplo, alunos sem nenhuma matrícula. EXISTS verifica se uma subquery (uma consulta dentro de outra) devolve pelo menos uma linha:

SELECT a.nome FROM aluno a
WHERE EXISTS (SELECT m.id FROM matricula m WHERE m.aluno_id = a.id);
-- alunos que TÊM ao menos uma matrícula

Combinado com NOT, inverte a pergunta — "quais não têm":

SELECT a.nome FROM aluno a
WHERE NOT EXISTS (SELECT m.id FROM matricula m WHERE m.aluno_id = a.id);
-- alunos SEM nenhuma matrícula

Definição: Subquery

Uma consulta SELECT usada dentro de outra — como argumento de EXISTS, ou em qualquer lugar onde um valor/lista de valores é esperado. A subquery roda para cada linha candidata da consulta externa (nos exemplos acima, uma vez para cada aluno), verificando a condição específica daquela linha.

O mesmo padrão de NOT EXISTS serve para qualquer relação — encontrar cursos sem matrícula, exercícios sem resposta, ou qualquer "órfão" numa relação um-para-muitos ou muitos-para-muitos.

Definição: Construa uma consulta complexa aos pedaços

Uma consulta que encadeia vários JOINs (ex.: curso → secao → exercicio → resposta → nota) fica muito mais fácil de escrever — e de depurar quando algo dá errado — um JOIN de cada vez: comece com SELECT coluna FROM tabela1, rode, confira o resultado, adicione o próximo JOIN, rode de novo, e repita até chegar na consulta final com GROUP BY/agregação. Tentar escrever tudo de uma vez, sem testar os passos intermediários, dificulta achar onde exatamente a consulta saiu do esperado.

COUNT(coluna) (em vez de COUNT(*)) conta só os valores não nulos daquela coluna especificamente — útil, por exemplo, ao contar alunos através de um JOIN (COUNT(a.id)), garantindo que a contagem seja sobre a coluna certa mesmo se o JOIN introduzir linhas repetidas por outros motivos.

Filtrando agregações: HAVING

WHERE filtra linhas individuais, antes de qualquer agrupamento — por isso não funciona para filtrar pelo resultado de uma função de agregação:

SELECT a.nome, AVG(n.nota) FROM nota n
JOIN resposta r ON r.id = n.resposta_id
JOIN aluno a ON a.id = r.aluno_id
WHERE AVG(n.nota) < 5
GROUP BY a.nome;
-- ERROR 1111 (HY000): Invalid use of group function

Para filtrar pelo resultado de uma agregação (ex.: "só alunos com média abaixo de 5"), existe uma cláusula própria, HAVING, que roda depois do GROUP BY:

SELECT a.nome, AVG(n.nota) FROM nota n
JOIN resposta r ON r.id = n.resposta_id
JOIN aluno a ON a.id = r.aluno_id
GROUP BY a.nome
HAVING AVG(n.nota) < 5;

Definição: WHERE x HAVING

WHERE filtra antes de agrupar (linha a linha, nenhuma função de agregação permitida ali). HAVING filtra depois de agrupar (só faz sentido usá-lo quando a condição envolve uma função de agregação, como AVG, SUM, COUNT). Sintaticamente, a ordem das cláusulas é sempre WHERE → GROUP BY → HAVING — tentar escrever HAVING antes do GROUP BY é erro de sintaxe.

Eliminando duplicatas: DISTINCT

SELECT DISTINCT tipo FROM matricula;

DISTINCT devolve só os valores únicos de uma coluna (ou combinação de colunas), eliminando repetições do resultado.

Múltiplos valores numa condição: IN

Encadear vários OR para comparar a mesma coluna com valores diferentes (tipo = 'PAGA_PJ' OR tipo = 'PAGA_PF' OR ...) funciona, mas cresce rápido e fica difícil de ler. IN resume isso numa lista:

SELECT c.nome, COUNT(m.id), m.tipo FROM matricula m
JOIN curso c ON m.curso_id = c.id
WHERE m.tipo IN ('PAGA_PJ', 'PAGA_PF', 'PAGA_CHEQUE', 'PAGA_BOLETO')
GROUP BY c.nome, m.tipo;

coluna IN (valor1, valor2, ...) é equivalente a coluna = valor1 OR coluna = valor2 OR ..., mas muito mais legível — e mais fácil de manter conforme a lista de valores cresce. Funciona com qualquer tipo de coluna, não só texto:

SELECT a.nome, c.nome FROM aluno a
JOIN matricula m ON m.aluno_id = a.id
JOIN curso c ON m.curso_id = c.id
WHERE a.id IN (1, 3, 4); -- os alunos de id 1, 3 ou 4

Subquery como coluna calculada

Além de usar uma subquery dentro de WHERE (como em EXISTS, visto acima), dá para usar uma subquery no lugar de uma coluna, no SELECT — útil quando o valor que falta para o relatório vem de uma consulta totalmente diferente da consulta principal.

Exemplo: um relatório que mostra a média de cada aluno em cada curso, e a diferença entre essa média e a média geral de todos os cursos. A consulta principal (aluno + curso + média do aluno) já é conhecida:

SELECT a.nome, c.nome, AVG(n.nota) as media_aluno FROM nota n
JOIN resposta r ON n.resposta_id = r.id
JOIN exercicio e ON r.exercicio_id = e.id
JOIN secao s ON e.secao_id = s.id
JOIN curso c ON s.curso_id = c.id
JOIN aluno a ON r.aluno_id = a.id
GROUP BY a.nome, c.nome;

Mas a média geral (SELECT AVG(n.nota) FROM nota n) é uma consulta separada, sem relação de JOIN com a principal. A saída: colocar essa consulta entre parênteses no lugar de uma coluna, e usá-la numa operação aritmética:

SELECT a.nome, c.nome, AVG(n.nota) as media_aluno,
AVG(n.nota) - (SELECT AVG(n.nota) FROM nota n) as diferenca
FROM nota n
JOIN resposta r ON n.resposta_id = r.id
JOIN exercicio e ON r.exercicio_id = e.id
JOIN secao s ON e.secao_id = s.id
JOIN curso c ON s.curso_id = c.id
JOIN aluno a ON r.aluno_id = a.id
GROUP BY a.nome, c.nome;

Definição: Subquery correlacionada

Uma subquery usada no lugar de uma coluna do SELECT, cujo resultado é combinado (aqui, subtraído) com uma coluna da consulta principal. Só funciona se a subquery devolver exatamente uma linha — se devolvesse mais de uma, o banco não saberia com qual delas fazer a conta, e a consulta falha. É chamada de "correlacionada" quando o valor que ela busca depende de uma coluna da linha atual da consulta externa (ex.: WHERE r.aluno_id = a.id, filtrando pelo a.id da linha de fora) — ao contrário da média geral do exemplo acima, que é a mesma para toda linha e por isso não referencia nada da consulta externa.

Esse mesmo padrão — uma subquery no SELECT filtrada pela linha da consulta externa — também serve para "contar quantos relacionados cada registro tem", sem precisar de JOIN + GROUP BY:

SELECT a.nome,
(SELECT COUNT(r.id) FROM resposta r WHERE r.aluno_id = a.id) AS quantidade_respostas,
(SELECT COUNT(m.id) FROM matricula m WHERE m.aluno_id = a.id) AS quantidade_matricula
FROM aluno a;

Repare no WHERE r.aluno_id = a.id dentro de cada subquery: sem esse filtro, a subquery contaria todas as respostas (ou matrículas) do banco, e todo aluno receberia o mesmo número — é o filtro que faz a subquery rodar "uma vez para cada aluno", trazendo o valor específico daquela linha.

Entendendo o LEFT JOIN

Um JOIN comum (chamado tecnicamente de inner join) só devolve uma linha quando existe correspondência dos dois lados da junção. Isso vira um problema quando o objetivo é justamente encontrar quem não tem correspondência — por exemplo, um relatório de participação que deveria incluir também os alunos sem nenhuma resposta:

SELECT a.nome, COUNT(r.id) AS respostas
FROM aluno a
JOIN resposta r ON r.aluno_id = a.id
GROUP BY a.nome;

Se existem 16 alunos no banco mas a consulta acima devolve só 4, o motivo é o JOIN: um aluno sem nenhuma resposta correspondente simplesmente desaparece do resultado, porque não há linha de resposta para casar com ele.

Definição: LEFT JOIN

Variação do JOIN que preserva todas as linhas da tabela à esquerda (a que vem logo depois do FROM), mesmo quando não existe correspondência na tabela à direita — nesse caso, as colunas da tabela direita vêm como NULL. É a ferramenta certa sempre que a pergunta é "todos os X, incluindo os que não têm Y relacionado".

SELECT a.nome, COUNT(r.id) AS respostas
FROM aluno a
LEFT JOIN resposta r ON r.aluno_id = a.id
GROUP BY a.nome;
-- agora os 16 alunos aparecem; quem não tem resposta mostra 0 (COUNT ignora os NULLs)

RIGHT JOIN é o espelho do LEFT JOIN — preserva todas as linhas da tabela à direita. Na prática, quase não é usado: basta inverter a ordem das tabelas e trocar por LEFT JOIN, então a maioria dos times padroniza em usar sempre LEFT JOIN por consistência.

Definição: JOIN (inner) x LEFT JOIN — quando usar qual

Use JOIN comum quando a pergunta só faz sentido para quem tem o relacionamento (ex.: "nota de cada resposta" — resposta sem nota não interessa). Use LEFT JOIN quando a ausência de relacionamento também é uma informação relevante para a resposta (ex.: "quais alunos não estão participando" — isso só aparece se o aluno sem resposta continuar na lista).

JOIN sem qualificador é, tecnicamente, sempre um INNER JOIN — os dois termos significam a mesma coisa; alguns times escrevem INNER JOIN explicitamente só para deixar claro (por contraste com LEFT/RIGHT) que aquele JOIN exige associação dos dois lados.

JOIN ou subquery?

Muita consulta que usa subquery (como as de contagem por aluno, vistas acima) também pode ser escrita com JOIN + GROUP BY, com o mesmo resultado final. Qual preferir?

Definição: prefira JOIN a subquery quando o resultado é equivalente

SGBDs otimizam melhor consultas com JOIN do que consultas equivalentes com subquery — o desempenho tende a ser melhor com JOIN. Quando as duas abordagens resolvem o mesmo problema, prefira JOIN.

Uma armadilha ao combinar múltiplos LEFT JOINs independentes na mesma consulta: juntar aluno com resposta e, na mesma consulta, com matricula (duas relações "um-para-muitos" que não têm relação entre si) multiplica as linhas — cada resposta do aluno é combinada com cada matrícula dele, gerando respostas × matrículas linhas por aluno, em vez de contar cada uma separadamente:

SELECT a.nome, COUNT(r.id) AS qtd_respostas, COUNT(m.id) AS qtd_matriculas
FROM aluno a
LEFT JOIN resposta r ON r.aluno_id = a.id
LEFT JOIN matricula m ON m.aluno_id = a.id
GROUP BY a.nome;
-- ERRADO: um aluno com 7 respostas e 2 matrículas aparece com 14 em ambas as colunas

A saída é usar COUNT(DISTINCT ...), contando só os ids únicos de cada lado antes de multiplicarem entre si:

SELECT a.nome, COUNT(DISTINCT r.id) AS qtd_respostas, COUNT(DISTINCT m.id) AS qtd_matriculas
FROM aluno a
LEFT JOIN resposta r ON r.aluno_id = a.id
LEFT JOIN matricula m ON m.aluno_id = a.id
GROUP BY a.nome;

Definição: por que JOINs independentes multiplicam linhas

Quando uma consulta faz JOIN de uma tabela com duas outras tabelas diferentes, sem relação entre elas (aqui, resposta e matricula só se relacionam via aluno, não uma com a outra), o banco combina cada linha de um lado com cada linha do outro para o mesmo aluno — o mesmo efeito de um produto cartesiano, só que restrito a cada aluno. COUNT(DISTINCT coluna) resolve porque conta valores únicos daquela coluna, ignorando quantas vezes ela se repetiu por causa da multiplicação. Nesse cenário específico (contar quantidades de duas relações independentes de uma vez), a versão com subqueries correlacionadas (ver acima) fica mais simples de acertar de primeira do que o JOIN duplo com DISTINCT.

Paginação: LIMIT

Devolver todas as linhas de uma tabela de uma vez só não escala — imagine listar todos os alunos de uma escola com milhões de registros, ou todas as mensagens antigas de uma conversa. O padrão é devolver os dados aos poucos (paginação), como uma rede social carrega só as postagens mais recentes e busca mais conforme você rola a tela.

SELECT a.nome FROM aluno a ORDER BY a.nome LIMIT 5;

LIMIT restringe a consulta a um número máximo de linhas — aqui, os 5 primeiros alunos em ordem alfabética. Para pegar a próxima página, é preciso pular os já vistos: a forma completa é LIMIT deslocamento, quantidade:

SELECT a.nome FROM aluno a ORDER BY a.nome LIMIT 5, 5;
-- pula os 5 primeiros (linhas 0-4) e traz os 5 seguintes

Definição: LIMIT deslocamento, quantidade

LIMIT sozinho (LIMIT 5) equivale a LIMIT 0, 5 — começa da primeira linha (linha 0) e traz até 5. Com dois números, o primeiro é quantas linhas pular a partir do início, o segundo é quantas trazer depois disso — é assim que se implementa paginação (página 1 = LIMIT 0, 10, página 2 = LIMIT 10, 10, página 3 = LIMIT 20, 10, ...). Usar LIMIT sem ORDER BY funciona, mas não garante qual subconjunto de linhas volta a cada execução — por isso paginação de verdade sempre combina os dois.

Explorando um banco desconhecido pelo dicionário de dados

Quando não existe um Modelo de Entidade-Relacionamento (MER, ver Diagramas e UML 2) à mão para consultar, um banco Oracle (e a maioria dos bancos relacionais, com views equivalentes) permite descobrir a estrutura de um schema desconhecido consultando seu próprio dicionário de dados — útil tanto para montar uma consulta a partir de um enunciado em texto quanto para entender uma base legada sem documentação.

Definição: Views do dicionário de dados (Oracle)

  • user_tables / all_tables — listam as tabelas existentes (do próprio usuário, ou de todos que ele tem acesso).
  • user_tab_comments / all_tab_comments — mostram comentários/descrição cadastrados sobre cada tabela, quando existirem.
  • desc nome_tabela (comando do SQL*Plus, não uma view) — lista as colunas de uma tabela, seus tipos e se aceitam null.
  • user_constraints / user_cons_columns — permitem descobrir os relacionamentos (chaves estrangeiras) entre tabelas sem precisar de um MER: cruzando essas duas views é possível identificar quais colunas de uma tabela referenciam outra.
SELECT cons.table_name || '.' || cons_col.column_name ||
       ' faz ligação com ' ||
       cons_depend.table_name || '.' || cons_col_depend.column_name ||
       ' através da chave ' || cons.constraint_name AS dependencias
FROM user_constraints cons, user_cons_columns cons_col,
     user_constraints cons_depend, user_cons_columns cons_col_depend
WHERE cons.constraint_name = cons_col.constraint_name
AND cons.table_name = cons_col.table_name
AND cons.table_name = 'EMPLOYEES'
AND cons.constraint_type = 'R'  -- 'R' = Foreign Key
AND cons_depend.constraint_name = cons_col_depend.constraint_name
AND cons_depend.table_name = cons_col_depend.table_name
AND cons_depend.constraint_name = cons.r_constraint_name
ORDER BY 1;

Definição: Roteiro para montar um SELECT a partir de um enunciado

Metodologia prática, análoga à usada para blocos PL/SQL (ver Uma metodologia para começar a escrever um bloco PL/SQL):

  1. Identifique as tabelas envolvidas — pelo nome explícito no enunciado, ou inferindo pelo assunto (ex.: "departamentos" sugere uma tabela de departamentos). Na dúvida, consulte user_tables/all_tables.
  2. Identifique as colunas de cada tabela com desc tabela, conferindo se os nomes batem com o que o enunciado pede.
  3. Escreva o select/from com as colunas e tabelas identificadas, sempre usando aliases para tabelas e colunas — evita ambiguidade entre colunas de mesmo nome em tabelas diferentes, e deixa o comando mais legível.
  4. Faça as ligações entre as tabelas na cláusula where (ou join), localizando as chaves estrangeiras via MER ou, na ausência dele, consultando user_constraints/user_cons_columns.
  5. Acrescente as demais restrições pedidas pelo enunciado (filtros adicionais, order by).

O mesmo raciocínio (identificar tabelas → colunas → ligações → restrições) vale igualmente para montar um insert, update ou delete.

Views: salvando uma consulta com nome

Definição: View

Tabela virtual baseada em uma consulta. Não guarda os dados: executa a consulta sempre que é usada, então reflete os dados atuais das tabelas de origem.

CREATE VIEW clientes_ativos AS
SELECT id, nome, email
FROM clientes
WHERE status = 'ATIVO';

SELECT * FROM clientes_ativos;
  • Vantagem: simplifica consultas longas e repetidas e reutiliza a lógica.
  • Segurança: pode expor só as colunas permitidas (ver permissões).
  • Atenção: a view usa os dados atuais das tabelas de origem — se a estrutura delas mudar, a view pode quebrar.

Índices: acelerando buscas

Definição: Índice

Estrutura auxiliar que permite localizar dados mais rápido, sem varrer a tabela inteira (como o índice remissivo de um livro).

CREATE INDEX idx_clientes_email ON clientes (email);
Quando usar Colunas muito usadas em WHERE, JOIN e ORDER BY
Vantagem Reduz o tempo de leitura em tabelas grandes
Custo INSERT, UPDATE e DELETE ficam um pouco mais lentos (o índice também precisa ser atualizado) e ocupa espaço
Boa prática Crie índices com base em consultas reais; remova os não usados ou duplicados

Plano de execução (EXPLAIN)

O comando EXPLAIN mostra o caminho que o banco planejou para executar uma consulta: tabelas, índices, filtros e operações usadas, com uma estimativa de custo. Serve para analisar consultas lentas antes de criar índices.

EXPLAIN SELECT * FROM clientes WHERE email = 'ana@exemplo.com';

Sinal de alerta: leituras completas de tabelas grandes (full scan) podem indicar uma consulta ou um índice ruins. Otimize com dados reais e teste as mudanças.

Transações e propriedades ACID

Definição: Transação

Conjunto de operações tratado como uma única unidade: ou todas são efetivadas ou nenhuma. Evita dados incompletos ou inconsistentes (ex.: transferência — debitar uma conta e creditar outra).

BEGIN;                                   -- inicia (em alguns bancos: START TRANSACTION)
UPDATE contas SET saldo = saldo - 100 WHERE id = 1;
UPDATE contas SET saldo = saldo + 100 WHERE id = 2;
COMMIT;                                  -- confirma tudo
-- ou ROLLBACK;                          -- desfaz tudo se ocorrer erro
Propriedade ACID Significado Exemplo
Atomicidade Tudo acontece ou nada acontece Compra só termina se pagamento e pedido forem registrados
Consistência Os dados respeitam as regras do banco Chaves e restrições continuam válidas
Isolamento Transações simultâneas não se atrapalham Dois clientes comprando o último item
Durabilidade Após o COMMIT, a mudança permanece salva Mesmo se o servidor reiniciar

(Transações em Oracle/PL/SQL em PL/SQL; em sistemas distribuídos, o modelo alternativo de consistência eventual está em NoSQL.)

SQL injection e prepared statements

Definição: SQL injection

Falha em que texto digitado pelo usuário é concatenado diretamente ao comando SQL, permitindo que uma entrada maliciosa altere a consulta e exponha, altere ou exclua dados.

-- MAL: monta o SQL juntando o que o usuário digitou
"SELECT * FROM usuarios WHERE login = '" + login + "' AND senha = '" + senha + "'"
-- entrada maliciosa:  login = ' OR '1'='1

Definição: Prepared statement (consulta parametrizada)

Consulta preparada com marcadores de posição (?) cujos valores são enviados separadamente da estrutura do comando. A entrada nunca é interpretada como SQL.

SELECT * FROM usuarios WHERE email = ?
  • Prevenção: use prepared statements e parâmetros; nunca monte SQL juntando texto recebido do usuário.
  • Validação: valide formato, tamanho e tipo das entradas.
  • Defesa extra: a conta que a aplicação usa deve ter poucos privilégios (ver Administração e Operação de Banco).
  • No Java, o equivalente é o PreparedStatement do JDBC; em JPA/Spring Data, os parâmetros nomeados (@Param) — ver Spring.

SQL no Oracle: dialeto, objetos e usuários

Os exemplos acima usam MySQL. O Oracle Database segue o padrão SQL, mas tem um dialeto próprio e um ecossistema de objetos e de segurança bastante rico, muito presente em grandes empresas, bancos e governo. A linguagem procedural do Oracle está em PL/SQL; desempenho de consultas, em Performance e Tuning de SQL, Plano de execução e Índices. Ferramentas de linha de comando: SQL*Plus (vem com o banco) e interfaces gráficas como o SQL Developer.

MySQL x Oracle: principais diferenças

Assunto MySQL (acima) Oracle
Texto / números VARCHAR, INT, DECIMAL VARCHAR2(n), NUMBER(p,s), CHAR; objetos grandes CLOB/BLOB
Data DATE, DATETIME DATE já guarda data e hora; TIMESTAMP para frações de segundo
Limitar linhas LIMIT 10 FETCH FIRST 10 ROWS ONLY (12c+) ou WHERE ROWNUM <= 10 (clássico)
Chave automática AUTO_INCREMENT Sequence + trigger, ou coluna GENERATED ... AS IDENTITY (12c+)
SELECT sem tabela SELECT 1 + 1; SELECT 1 + 1 FROM DUAL; (DUAL é uma tabela de uma linha)
Concatenar CONCAT(a, b) a \|\| b
Diferença de conjuntos EXCEPT MINUS
Valores permitidos ENUM Não existe: use CHECK (col IN ('A','B')) ou tabela de domínio
String vazia '' e NULL são diferentes '' é tratado como NULL
Banco x usuário CREATE DATABASE O "espaço" de objetos é o schema do usuário (CREATE USER)
Substituir nulo IFNULL NVL, COALESCE
-- Oracle: 5 maiores salários, de forma moderna e clássica
SELECT nome, salario FROM empregados ORDER BY salario DESC FETCH FIRST 5 ROWS ONLY;

SELECT * FROM (SELECT nome, salario FROM empregados ORDER BY salario DESC) WHERE ROWNUM <= 5;

Como o Oracle executa um comando SQL

Todo comando passa por três etapas:

  1. PARSE (análise): valida a sintaxe e procura, na memória compartilhada (shared pool), um plano já preparado para exatamente o mesmo texto; se achar, é um soft parse (barato). Se não, verifica no dicionário de dados se tabelas, colunas e permissões existem e o otimizador baseado em custo escolhe o plano de execução: é o hard parse (caro).
  2. EXECUTE: lê os dados (da memória, ou do disco se não estiverem em memória; leitura lógica x física). Em atualizações, trava (lock) as linhas e aloca o espaço de undo/rollback para poder desfazer.
  3. FETCH: devolve as linhas ao cliente (só em consultas).

Por isso o uso de variáveis bind (:valor) em vez de concatenar valores no texto do comando reduz hard parses e protege contra SQL injection (Prepared statements).

Schemas, sessões e dicionário de dados

  • Ao conectar com um usuário, o banco abre uma sessão. Cada usuário é dono de um schema (conjunto de seus objetos: tabelas, views, packages, triggers). Objetos de outros usuários se referenciam como schema.objeto (e só funcionam se o dono concedeu acesso).
  • O dicionário de dados são tabelas internas mantidas pelo próprio Oracle, expostas por views com prefixo USER_ (meus objetos), ALL_ (a que tenho acesso) e DBA_ (todos): USER_TABLES, USER_TAB_COLUMNS, USER_CONSTRAINTS, USER_INDEXES, USER_OBJECTS. Nunca altere essas tabelas diretamente; os comandos DDL as atualizam. Técnica geral em Explorando um banco desconhecido.

Transações no Oracle

Uma transação começa na primeira instrução DML e termina com COMMIT ou ROLLBACK (ou uma instrução DDL, que confirma implicitamente o que veio antes; QUIT normal também confirma). SAVEPOINT marca pontos intermediários para um ROLLBACK TO. SET TRANSACTION inicia explicitamente uma transação com regras definidas (por exemplo, somente leitura). Propriedades gerais em ACID e detalhes de uso em PL/SQL: transações.

Criando tabelas no Oracle: detalhes úteis

-- Copiar estrutura E dados (CTAS)
CREATE TABLE emp_backup AS SELECT * FROM emp;

-- Tabela temporária global: os dados somem no fim da transação ou da sessão
CREATE GLOBAL TEMPORARY TABLE emp_tmp (
  empno NUMBER(4), ename VARCHAR2(30)
) ON COMMIT DELETE ROWS;        -- ou ON COMMIT PRESERVE ROWS (dura a sessão)

ALTER TABLE emp ADD (email VARCHAR2(100));
ALTER TABLE emp MODIFY (ename VARCHAR2(60) NOT NULL);
ALTER TABLE emp READ ONLY;      -- somente leitura (volta com READ WRITE)
TRUNCATE TABLE emp_tmp;         -- esvazia rápido; DDL: sem rollback, não dispara triggers
  • Um DEFAULT só vale quando a coluna não é informada; se o INSERT passa NULL explicitamente, o NULL prevalece.
  • Ao desabilitar uma chave primária referenciada por outras tabelas, é preciso desabilitar também as chaves estrangeiras dependentes (ou usar CASCADE).

Constraints e mensagens de erro

Constraint Garante Erro comum quando violada
NOT NULL Coluna sempre preenchida ORA-01400 (cannot insert NULL)
PRIMARY KEY Unicidade e não nulo; cria índice ORA-00001 (unique constraint violated)
UNIQUE Valores não repetidos (aceita nulo) ORA-00001
FOREIGN KEY Só referencia chave existente ORA-02291 (parent key not found), ORA-02292 (child record found, ao apagar o pai)
CHECK Regra sobre a linha (sal + comm > 10000) ORA-02290 (check constraint violated)

ON DELETE CASCADE apaga os filhos junto com o pai; ON DELETE SET NULL zera a chave nos filhos. Dê nomes às constraints (CONSTRAINT emp_dept_fk ...): sem nome, o Oracle gera um (SYS_C00123) que dificulta ler os erros. Veja-as em USER_CONSTRAINTS e USER_CONS_COLUMNS.

Views, índices, sinônimos e sequences

  • View: CREATE OR REPLACE VIEW ... AS SELECT ...; WITH READ ONLY impede alterações por ela e WITH CHECK OPTION impede inserir/atualizar linhas que a própria view não enxergaria. Materialized view guarda o resultado (com regra de atualização); a view comum guarda só a consulta. Conceito: Views.
  • Índices: o Oracle usa um índice quando as colunas do WHERE aparecem sem função em volta (WHERE UPPER(nome) = ... ignora o índice de nome, a menos que exista um índice por função). Tipos: B-tree (padrão), único, composto e bitmap (para colunas de baixa cardinalidade, como sexo ou status, em tabelas de consulta; evite em tabelas com muita escrita concorrente). Não dá para criar dois índices sobre a mesma lista de colunas.
  • Sinônimo: apelido para um objeto (tabela, view, sequence), muito usado para apontar para objetos de outro schema sem escrever o prefixo. Pode ser privado (só do usuário) ou público (CREATE PUBLIC SYNONYM). O sinônimo não dá acesso; o GRANT dá.
  • Sequence: gerador de números únicos, independente de tabela.
CREATE SEQUENCE seq_pedido START WITH 1000 INCREMENT BY 1 NOCACHE NOCYCLE;

INSERT INTO pedidos (id, cliente) VALUES (seq_pedido.NEXTVAL, 'ACME');
SELECT seq_pedido.CURRVAL FROM DUAL;   -- só depois de um NEXTVAL na sessão

-- Oracle 12c+: coluna identity (sem trigger)
CREATE TABLE produtos (id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, nome VARCHAR2(80));

Parâmetros: START WITH, INCREMENT BY, MINVALUE/MAXVALUE, CYCLE e CACHE (pré-aloca números na memória). Podem ocorrer lacunas (um ROLLBACK ou a perda do cache não devolve números), então não use a sequence como contador sem falhas.

Usuários, privilégios e roles

CREATE USER app_vendas IDENTIFIED BY "senha-forte"
  DEFAULT TABLESPACE users QUOTA 100M ON users;

GRANT CREATE SESSION TO app_vendas;              -- privilégio de SISTEMA: sem ele, ORA-01045 ao conectar
GRANT CREATE TABLE, CREATE VIEW TO app_vendas;   -- mais privilégios de sistema
GRANT SELECT, UPDATE (preco) ON loja.produtos TO app_vendas;   -- de OBJETO (UPDATE só na coluna preco)
GRANT SELECT ON loja.produtos TO analista WITH GRANT OPTION;  -- pode repassar o acesso
REVOKE UPDATE ON loja.produtos FROM app_vendas;  -- (revoga na tabela inteira, não por coluna)
DROP USER app_vendas CASCADE;                    -- CASCADE se o usuário for dono de objetos
Tipo Exemplos Quem repassa
Privilégio de sistema CREATE SESSION, CREATE TABLE, CREATE SYNONYM, CREATE SEQUENCE WITH ADMIN OPTION
Privilégio de objeto SELECT, INSERT, UPDATE, DELETE, EXECUTE, INDEX, REFERENCES WITH GRANT OPTION

Role é um agrupamento de privilégios (de sistema e de objeto) concedido como se fosse um só; não tem dono e pode ser ativada ou desativada por sessão. Boas práticas e limites:

CREATE ROLE controla_empregado;
GRANT SELECT, INSERT, UPDATE ON loja.emp TO controla_empregado;
GRANT controla_empregado TO app_vendas;
ALTER USER app_vendas DEFAULT ROLE ALL EXCEPT excluir_empregado;
  • Privilégios recebidos por role não valem em stored procedures nem para criar views ou chaves estrangeiras sobre objetos de outro schema: nesses casos é preciso um GRANT direto ao usuário.
  • Roles predefinidas como CONNECT, RESOURCE e DBA existem por compatibilidade e dão muito poder. Em produção prefira roles próprias com o mínimo de privilégios (Administração de banco).
  • O usuário que acabou de ser criado não consegue sequer conectar até receber CREATE SESSION.

Comandos úteis do SQL*Plus

Comando Para que serve
L[IST], A[PPEND], C[HANGE] /velho/novo/, DEL, I[NPUT] Editar o comando no buffer do SQL*Plus
/ Executa o conteúdo do buffer sem listá-lo
GET, SAVE, EDIT, START ou @arquivo Ler, gravar, editar e executar scripts (@@ busca no diretório do script em execução)
SPOOL arquivo / SPOOL OFF Grava a saída em arquivo (.lst por padrão)
SET LINESIZE, SET PAGESIZE, SET HEADING, SET ECHO Variáveis de ambiente que controlam a exibição
DEFINE, ACCEPT, &variavel, &&variavel Variáveis de substituição (veja PL/SQL: variáveis bind e de substituição)
login.sql Script lido ao iniciar o SQL*Plus, para configurar o ambiente automaticamente