Material de estudo

SQL na prática: pessoa e automóvel

Um guia de consulta rápida com tudo que apareceu nas aulas e exercícios — do CREATE TABLE às VIEWs, TRIGGERs e FUNCTIONs — organizado pra você revisar antes da próxima lista, com os erros reais que você encontrou e como resolver cada um.

MySQL / MySQL Workbench Base: pessoa · automovel · pessoa_automovel Aulas 01–05 · Exercícios 3–7 14 tópicos + referência de erros → Guia por aula (slides + dicas)
?

Busca rápida

Digite o que você quer fazer (ex.: ranking, sem carro, média, duplicados) e receba o comando pronto, com a explicação em uma linha. Os exemplos usam pessoa e automovel.

Ranking: os N maiores (top 3)

ORDER BY ... DESC põe o maior no topo e LIMIT N corta. Para os menores, troque por ASC.

SELECT modelo, preco
FROM automovel
ORDER BY preco DESC
LIMIT 3;

Ranking com posição (1º, 2º, 3º...)

MySQL 8+. RANK() repete a posição em empate e pula a seguinte (1,1,3). DENSE_RANK() não pula (1,1,2). ROW_NUMBER() numera sem empate.

SELECT modelo, preco,
       RANK() OVER (ORDER BY preco DESC) AS posicao
FROM automovel;

Ranking dentro de cada grupo

PARTITION BY reinicia o ranking a cada grupo. Aqui: o carro mais caro de cada pessoa é o de posicao = 1.

SELECT id_pessoa, modelo, preco,
       RANK() OVER (PARTITION BY id_pessoa ORDER BY preco DESC) AS posicao
FROM automovel;

Ranking de quem tem mais (contagem)

Conte por grupo e ordene pelo apelido da contagem.

SELECT p.nome, COUNT(a.id) AS qtd
FROM pessoa p
LEFT JOIN automovel a ON a.id_pessoa = p.id
GROUP BY p.id, p.nome
ORDER BY qtd DESC;

Contar linhas

COUNT(*) conta linhas; COUNT(coluna) ignora os NULL.

SELECT COUNT(*) AS total FROM automovel;

Contar por grupo

Toda coluna do SELECT que não é função precisa estar no GROUP BY.

SELECT carro_moto, COUNT(*) AS qtd
FROM automovel
GROUP BY carro_moto;

Filtrar o resultado de um grupo

WHERE filtra linhas antes de agrupar; HAVING filtra os grupos depois. Função no WHERE dá erro 1111.

SELECT id_pessoa, COUNT(*) AS qtd
FROM automovel
GROUP BY id_pessoa
HAVING COUNT(*) >= 2;

Soma, média, maior e menor

Funções de agregação: SUM, AVG, MAX, MIN.

SELECT SUM(preco) AS soma, AVG(preco) AS media,
       MAX(preco) AS maior, MIN(preco) AS menor
FROM automovel;

Linha inteira do maior (ou menor) valor

Duas formas: ordenar e pegar 1, ou filtrar pelo MAX numa subquery (esta traz todos os empatados).

SELECT * FROM automovel ORDER BY preco DESC LIMIT 1;

SELECT * FROM automovel
WHERE preco = (SELECT MAX(preco) FROM automovel);

Acima da média

Subquery no WHERE. Não dá para usar AVG() direto no WHERE.

SELECT modelo, preco
FROM automovel
WHERE preco > (SELECT AVG(preco) FROM automovel);

Quem NÃO tem relação (sem carro)

LEFT JOIN + IS NULL ou NOT EXISTS. Evite NOT IN se a lista puder ter NULL. Nunca use = NULL.

SELECT p.nome
FROM pessoa p
LEFT JOIN automovel a ON a.id_pessoa = p.id
WHERE a.id IS NULL;

SELECT nome FROM pessoa p
WHERE NOT EXISTS (SELECT 1 FROM automovel a WHERE a.id_pessoa = p.id);

Juntar duas tabelas (só os relacionados)

INNER JOIN traz só quem tem par nas duas tabelas. Sem ON vira produto cartesiano.

SELECT p.nome, a.modelo
FROM pessoa p
INNER JOIN automovel a ON a.id_pessoa = p.id;

Todos de um lado, com ou sem par

LEFT JOIN mantém todas as linhas da tabela da esquerda; sem par, as colunas da direita vêm NULL.

SELECT p.nome, a.modelo
FROM pessoa p
LEFT JOIN automovel a ON a.id_pessoa = p.id;

FULL JOIN (o MySQL não tem)

Simule com LEFT JOIN + UNION + RIGHT JOIN.

SELECT p.nome, a.modelo FROM pessoa p LEFT JOIN automovel a ON a.id_pessoa = p.id
UNION
SELECT p.nome, a.modelo FROM pessoa p RIGHT JOIN automovel a ON a.id_pessoa = p.id;

Se/senão numa coluna (CASE)

Funciona como if/else. Dê nome com AS. Pode ter vários WHEN e aninhar CASE.

SELECT nome,
  CASE WHEN cnh = 'S' THEN 'Sim' ELSE 'Não' END AS habilitado
FROM pessoa;

Buscar texto que contém uma palavra

% é o coringa: '%de%' contém, 'de%' começa, '%de' termina.

SELECT id, nome FROM disciplina WHERE nome LIKE '%de%';

Par ou ímpar / múltiplo

valor % 2 = 0 é par; % 3 = 0 é múltiplo de 3.

SELECT valor FROM numero WHERE valor % 2 = 0;

Achar duplicados

Agrupe pela coluna e filtre HAVING COUNT(*) > 1.

SELECT nome, COUNT(*) AS vezes
FROM pessoa
GROUP BY nome
HAVING COUNT(*) > 1;

Remover repetidos do resultado

DISTINCT tira linhas repetidas do resultado (não apaga nada da tabela).

SELECT DISTINCT profissao FROM pessoa;

Juntar, comuns ou diferença de duas consultas

Mesmo número de colunas. UNION sem repetidos, UNION ALL com repetidos, INTERSECT só os comuns, EXCEPT o que está só na 1ª.

SELECT nome FROM pai
UNION ALL
SELECT nome FROM filho
ORDER BY nome;

SELECT nome FROM pai INTERSECT SELECT nome FROM filho;
SELECT nome FROM pai EXCEPT SELECT nome FROM filho;

Reaproveitar um resultado (WITH)

O WITH cria uma tabela temporária com nome; filtre o apelido sem precisar de HAVING.

WITH info(id_pessoa, qtd) AS (
  SELECT id_pessoa, COUNT(*) FROM automovel GROUP BY id_pessoa)
SELECT * FROM info WHERE qtd >= 2;

Inserir a partir de um SELECT

Sem VALUES: o SELECT fornece as linhas.

INSERT INTO pessoa_automovel(nome, modelo, preco)
SELECT p.nome, a.modelo, a.preco
FROM pessoa p JOIN automovel a ON a.id_pessoa = p.id;

Atualizar/apagar sem acidente

Rode o WHERE num SELECT antes. Sem WHERE, todas as linhas mudam. O Workbench bloqueia com o erro 1175.

SELECT * FROM automovel WHERE id = 5;      -- confira antes
UPDATE automovel SET preco = 50000 WHERE id = 5;

Criar uma view

View não guarda dados, só a consulta. Boa para dar acesso a poucas colunas.

CREATE VIEW view_aluno AS
SELECT nome, ra FROM aluno ORDER BY nome;
SELECT * FROM view_aluno;

Criar índice e ver o ganho

Índice acelera buscas na coluna; EXPLAIN ANALYZE mostra Table scan x index scan e o tempo.

CREATE INDEX idx_modelo ON automovel(modelo);
EXPLAIN ANALYZE SELECT * FROM automovel WHERE modelo = 'Civic';

Trigger que valida (BEFORE INSERT)

NEW é a linha que vai entrar. SIGNAL cancela e mostra a mensagem (erro 1644).

DELIMITER $$
CREATE TRIGGER trg_before_insert_salario
BEFORE INSERT ON salario
FOR EACH ROW
BEGIN
  IF NEW.remuneracao < 0 THEN
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Remuneração deve ser positiva';
  END IF;
END $$
DELIMITER ;

Trigger que grava log (AFTER)

No UPDATE use OLD (antes) e NEW (depois). Não mexa na própria tabela do trigger (erro 1442).

DELIMITER $$
CREATE TRIGGER trg_after_update_salario
AFTER UPDATE ON salario
FOR EACH ROW
BEGIN
  INSERT INTO log_salario(id_salario, remuneracao_anterior, remuneracao_nova, datahora)
  VALUES (NEW.id, OLD.remuneracao, NEW.remuneracao, CURRENT_TIMESTAMP());
END $$
DELIMITER ;

Criar uma function

Devolve um valor com RETURN e é chamada dentro do SELECT.

DELIMITER $$
CREATE FUNCTION dobro(n INT)
RETURNS INT
DETERMINISTIC
BEGIN
  RETURN n * 2;
END$$
DELIMITER ;

SELECT dobro(remuneracao) FROM salario;

Criar e chamar uma procedure

CALL executa. OUT devolve por variável @x. Sempre use DELIMITER.

DELIMITER $$
CREATE PROCEDURE contar(OUT p_total INT)
BEGIN
  SELECT COUNT(*) INTO p_total FROM automovel;
END$$
DELIMITER ;

CALL contar(@t);
SELECT @t;

Lançar um erro próprio

Em trigger ou procedure. Aparece como erro 1644 com o texto escolhido.

SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Erro - Id não existe';

Paginação (pular linhas)

LIMIT tamanho OFFSET quantos_pular. Página 2 de 10 em 10 = OFFSET 10.

SELECT * FROM pessoa ORDER BY id LIMIT 10 OFFSET 10;

Total por pessoa, do maior para o menor

Junta, soma por grupo e ordena pelo apelido.

SELECT p.nome, SUM(a.preco) AS total
FROM pessoa p
JOIN automovel a ON a.id_pessoa = p.id
GROUP BY p.id, p.nome
ORDER BY total DESC;
01

Criar tabelas

Toda tabela precisa de uma chave primária, e toda FOREIGN KEY precisa apontar pra uma coluna que já existe na própria tabela — o SQL não cria a coluna sozinho.

Estrutura de referência
CREATE TABLE pessoa (
    id INT AUTO_INCREMENT,
    nome VARCHAR(100),
    profissao VARCHAR(45),
    cnh VARCHAR(1),
    PRIMARY KEY(id)
);

CREATE TABLE automovel (
    id INT AUTO_INCREMENT,
    carro_moto VARCHAR(10),
    modelo VARCHAR(45),
    preco DECIMAL(10,2),
    id_pessoa INT,               -- precisa existir antes do FOREIGN KEY
    PRIMARY KEY(id),
    FOREIGN KEY(id_pessoa) REFERENCES pessoa(id)
);
✕

Erro clássico: declarar FOREIGN KEY(id_pessoa) sem antes criar a coluna id_pessoa INT na tabela. Dá Error 1072.

Escolhendo o tamanho da coluna

VARCHAR(1) só cabe 1 caractere. Se o valor real é uma palavra ('carro', 'moto'), a coluna precisa de espaço — VARCHAR(10), por exemplo. Já 'S'/'N' cabe em VARCHAR(1) numa boa.

DECIMAL sozinho (sem parênteses) vira DECIMAL(10,0) — ou seja, sem casas decimais. Pra preço, use DECIMAL(10,2).

02

Inserir dados

Sem lista de colunas, o MySQL espera um valor pra cada coluna da tabela, na ordem em que foi criada — incluindo o id. Por isso, sempre nomear as colunas evita erro de contagem e deixa o script à prova de mudanças futuras na tabela.

Forma segura
INSERT INTO pessoa (nome, profissao, cnh) VALUES
('paulo', 'maestro', 'S'),
('henrique', 'medico', 'N');

INSERT INTO automovel (carro_moto, modelo, preco, id_pessoa) VALUES
('moto', 'XT', 30000.00, 5),
('carro', 'civic', 45000.00, 5);
!

Ponto ≠ vírgula. Em SQL o ponto é o separador decimal, não de milhar. '30.000' vira 30,00 (trinta), não trinta mil. Pra R$ 30.000, escreva 30000.00.

03

Consultar dados

AS dá um apelido de exibição a uma coluna — não transforma nem reinterpreta o valor, só troca o rótulo no resultado. CASE WHEN troca um valor por outro conforme uma condição.

Alias e condicional
SELECT nome AS nome_completo,
       CASE WHEN cnh = 'S' THEN 'Sim' ELSE 'Não' END AS possui_cnh
FROM pessoa;
CláusulaPra que serve
ASRenomear coluna/tabela só na exibição do resultado
WHEREFiltrar linhas antes de agrupar
ORDER BY ... DESCOrdenar do maior pro menor
LIMIT nTrazer só as n primeiras linhas do resultado

CASE aninhado

Um CASE pode conter outro CASE dentro do ELSE — útil quando as condições têm prioridade (a primeira que bater "vence").

SELECT valor,
    CASE
        WHEN valor % 2 = 0 THEN 'Par'
        ELSE CASE
            WHEN valor % 5 = 0 THEN 'Múltiplo de 5'
            WHEN valor % 3 = 0 THEN 'Múltiplo de 3'
            ELSE 'Ímpar'
        END
    END AS analise
FROM numero;

Repare a ordem: só entra no CASE de dentro quem não é par — por isso um múltiplo de 6 (par e múltiplo de 3) sai como 'Par', não 'Múltiplo de 3'. A primeira condição que bate sempre ganha.

04

Combinar consultas

Diferente de JOIN (que junta colunas lado a lado), UNION/INTERSECT/EXCEPT combinam o resultado de dois SELECTs, um embaixo do outro. As duas consultas precisam ter o mesmo número de colunas.

ComandoO que traz
UNIONTudo dos dois SELECTs, sem repetir valores iguais
UNION ALLTudo dos dois SELECTs, pode repetir
INTERSECTSó o que aparece nos dois resultados
EXCEPTO que está no 1º SELECT e não está no 2º
Os 4 lado a lado
SELECT valor FROM numero
UNION
SELECT valor FROM numero_par;
-- 1,2,3,4,5,6,7,8,9,10 (sem duplicar o que já é comum)

SELECT valor FROM numero
INTERSECT
SELECT valor FROM numero_par;
-- só os valores que existem nas duas tabelas

SELECT valor FROM numero
EXCEPT
SELECT valor FROM numero_par;
-- os valores de "numero" que NÃO aparecem em "numero_par"

UNION é como simular um FULL JOIN (que o MySQL não tem de fábrica) — traz tudo de um lado e do outro, sem duplicar.

05

Juntar tabelas

JOIN só traz linhas que batem nas duas tabelas. LEFT JOIN traz tudo da tabela da esquerda, com NULL onde não houver correspondência.

Só quem tem automóvel
SELECT pessoa.nome, automovel.modelo
FROM pessoa
JOIN automovel ON automovel.id_pessoa = pessoa.id;
Todo mundo, mesmo sem automóvel
SELECT pessoa.nome, automovel.modelo
FROM pessoa
LEFT JOIN automovel ON automovel.id_pessoa = pessoa.id;
-- quem não tem carro aparece com modelo = NULL

Achar quem não tem correspondência

SELECT nome FROM pessoa p
WHERE NOT EXISTS (
    SELECT 1 FROM pessoa_automovel pa WHERE pa.nome_pessoa = p.nome
);
!

Armadilha do NOT IN com NULL: se a lista da subquery tiver algum NULL no meio, NOT IN para de funcionar e não retorna nenhuma linha — nem as óbvias. NOT EXISTS não tem esse problema; prefira ele.

Armadilha da linha fantasma: se uma tabela foi montada com LEFT JOIN, ela pode ter uma linha com o nome preenchido mas as outras colunas em NULL — isso "engana" o NOT IN/NOT EXISTS, porque tecnicamente aquele nome está na lista.

06

Agrupar e somar

Sempre que uma função de agregação (SUM, COUNT, AVG...) aparece junto de outras colunas no SELECT, essas outras colunas precisam estar no GROUP BY.

Valor total da garagem por pessoa
SELECT pessoa.nome,
       SUM(automovel.preco) AS total_garagem
FROM pessoa
JOIN automovel ON automovel.id_pessoa = pessoa.id
GROUP BY pessoa.id, pessoa.nome
ORDER BY total_garagem DESC;

Achar duplicatas

SELECT nome_pessoa, modelo, preco, COUNT(*) AS qtd
FROM pessoa_automovel
GROUP BY nome_pessoa, modelo, preco
HAVING COUNT(*) > 1;

HAVING é o "WHERE depois de agrupar" — filtra grupos, não linhas individuais. WHERE não pode usar o resultado de uma função de agregação; HAVING pode.

✕

Por que WHERE media > 6 não funciona (mesmo com media sendo o alias de AVG(nota)): o MySQL avalia o WHERE antes de calcular o GROUP BY/agregações — nesse momento o alias ainda não existe (Error 1054: Unknown column 'media'). E trocar pelo AVG(nota) > 6 direto no WHERE também quebra (Error 1111: Invalid use of group function), porque função de agregação não pode ir no WHERE.

Solução: usar HAVING media > 6 — ele roda depois do agrupamento, já com o alias calculado.

07

WITH (CTE)

WITH dá um nome a uma consulta pra você reaproveitar ela como se fosse uma tabela temporária, dentro do mesmo comando — deixa consultas grandes mais legíveis, e permite usar o resultado em SELECT, INSERT, UPDATE ou DELETE.

Nomeando o resultado do GROUP BY pra filtrar por ele
WITH info_temp(numero, quantidade, media) AS (
    SELECT numero, COUNT(numero) AS qtd, AVG(nota) AS media
    FROM exercicio
    GROUP BY numero
)
SELECT numero, quantidade, media
FROM info_temp
WHERE media > 6
ORDER BY quantidade DESC;

Aqui o WHERE media > 6 funciona numa boa — porque info_temp já é o resultado pronto do agrupamento, o alias media já existe como coluna de verdade.

WITH + INSERT / UPDATE / DELETE

Populando outra tabela com o resultado
INSERT INTO calculado(exercicio, quantidade, media)
WITH info_temp(numero, quantidade, media) AS (
    SELECT numero, COUNT(numero), AVG(nota)
    FROM exercicio GROUP BY numero
)
SELECT numero, quantidade, media FROM info_temp WHERE media > 6;
Atualizando outra tabela via JOIN com o CTE
WITH info_temp(numero, quantidade, media) AS (
    SELECT numero, COUNT(numero), AVG(nota)
    FROM exercicio GROUP BY numero
)
UPDATE calculado
INNER JOIN info_temp ON info_temp.numero = calculado.exercicio
SET calculado.quantidade = info_temp.quantidade,
    calculado.media = info_temp.media;

Repare que aqui o UPDATE usa um JOIN com o CTE, em vez de uma subquery escalar no SET — funciona porque atualiza várias linhas de uma vez, cada uma casando com sua linha correspondente em info_temp.

08

Subqueries

Uma subquery é uma consulta dentro de outra. INSERT INTO ... SELECT não conta — ali o SELECT não está aninhado em nada, é só a fonte de dados do INSERT.

5 jeitos de usar dentro de um SELECT

OndeExemploCuidado
Como colunaSELECT valor, (SELECT texto FROM t WHERE ...)Só pode devolver 1 coluna — senão Error 1241
No WHEREWHERE valor IN (SELECT ...)Pode devolver várias linhas
No FROM (como tabela)FROM (SELECT ...) AS tabelaPrecisa de AS pra dar nome à tabela derivada
No HAVINGHAVING valor IN (SELECT ...)Filtra grupos já formados
No ORDER BYORDER BY (SELECT texto FROM ...)Ordena por um valor buscado em outra tabela
✕

SELECT valor, (SELECT texto, texto_ingles FROM t WHERE ...) quebra com Error 1241 — Operand should contain 1 column(s). Uma subquery usada como coluna só pode trazer 1 coluna só; pra trazer duas, use JOIN em vez disso.

Em INSERT, UPDATE e DELETE

ComandoUsoExemplo
INSERTComo fonte inteira dos dadosINSERT INTO t SELECT ... FROM outra
INSERTComo parte de um valorVALUES ((SELECT COUNT(*)+1 FROM t), '')
UPDATEComo novo valorSET texto = (SELECT ... WHERE id = t.id)
UPDATEComo filtroWHERE valor IN (SELECT ...)
DELETEComo filtroWHERE valor IN (SELECT ...)
✕

Uma subquery usada como valor único (ex: SET preco = (SELECT modelo FROM automovel WHERE id_pessoa = 5)) quebra com "Subquery returns more than 1 row" se a pessoa tiver mais de um automóvel. LIMIT 1 "resolve" tecnicamente, mas descarta dado — normalmente sinal de que o problema pede um JOIN, não uma subquery escalar.

!

Regra importante: em INSERT, UPDATE e DELETE, a subquery não pode usar a mesma tabela que já está sendo alterada na query de fora (o MySQL bloqueia isso pra evitar referência circular).

09

Atualizar dados

UPDATE sem WHERE muda a tabela inteira. Sempre confira com um SELECT usando a mesma condição antes de rodar o UPDATE de verdade.

Atualizar usando valores de outra tabela
UPDATE pessoa_automovel
SET preco = (SELECT preco FROM automovel WHERE id = 1)
WHERE modelo = (SELECT modelo FROM automovel WHERE id = 1)
  AND nome_pessoa = (
      SELECT nome FROM pessoa WHERE id = (SELECT id_pessoa FROM automovel WHERE id = 1)
  );

Leia de dentro pra fora: cada subquery busca um valor (preço, modelo, id da pessoa, nome) — é o mesmo que escrever os valores fixos na mão, só que o SQL busca sozinho, então o script continua certo se o automóvel id = 1 mudar de dono ou de preço.

!

"Safe Update Mode" (Error 1175): isso é uma trava do MySQL Workbench, não do banco — ele bloqueia UPDATE/DELETE quando não reconhece uma coluna de chave direta no WHERE (mesmo que a condição esteja correta, como com subqueries). Resolve com:

SET SQL_SAFE_UPDATES = 0;
10

Índices

Um índice é como o índice remissivo de um livro: em vez de vasculhar linha por linha, o banco pula direto pro que interessa. Só compensa em tabelas grandes — em 5 linhas você não sente diferença, mas é boa prática indexar colunas usadas com frequência em WHERE, JOIN ou ORDER BY.

CREATE INDEX idx_modelo ON pessoa_automovel (modelo);

-- conferir que foi criado
SHOW INDEX FROM pessoa_automovel;

-- remover
DROP INDEX idx_modelo ON pessoa_automovel;

Medindo o ganho de verdade

EXPLAIN ANALYZE na frente de um SELECT mostra como o MySQL realmente executou a busca — se ele fez um table scan (leu linha por linha) ou um index scan (usou o índice), e quanto tempo levou cada etapa.

EXPLAIN ANALYZE SELECT valor FROM numero_par WHERE valor > 100;

Com poucas linhas (dezenas), o ganho é quase zero — chega a ser irrelevante. Com ~120 mil linhas, o mesmo índice derrubou o tempo de uma busca por faixa (valor > 100 AND valor < 1000) de ~16ms pra ~0,08ms — quase 200x mais rápido. A lição: índice é sobre escala, não sobre a tabela pequena que você está testando agora.

!

Não exagere: cada índice deixa INSERT/UPDATE/DELETE um pouco mais lentos, porque o banco precisa manter a estrutura do índice atualizada a cada mudança. Indexe só as colunas que você realmente filtra ou ordena com frequência.

11

Views

Uma VIEW é um SELECT salvo com um nome — funciona como uma "tabela" pra consultar, mas não guarda dado nenhum. Toda vez que você consulta a view, o MySQL roda o SELECT de novo por trás dos panos.

Criar e consultar
CREATE VIEW view_aluno AS
SELECT nome, ra
FROM aluno
ORDER BY nome;

SELECT * FROM view_aluno;
DESC view_aluno;   -- mostra só as colunas nome e ra

Pra que serve

SituaçãoComo a view ajuda
Segurança por colunaMySQL só dá permissão por tabela inteira; uma view expõe só as colunas que a pessoa pode ver (ex: nome e ra, escondendo cpf)
Consulta complicada e recorrenteUm relatório com vários JOINs vira um SELECT * FROM view_relatorio simples
Combinar várias tabelasO SELECT por trás pode ter JOIN, UNION, INTERSECT — não é limitado a 1 tabela
✓

A view é "ao vivo": ela não copia os dados da tabela original. Se alguém inserir um novo aluno na tabela aluno, ele já aparece em view_aluno na próxima consulta, sem precisar recriar a view — desde que ele se encaixe na condição do SELECT que a monta.

Dá pra INSERT/UPDATE/DELETE numa view?

Depende. Se a view for baseada em 1 tabela só, com um SELECT simples (sem UNION, sem agregação), sim — e a mudança acontece de verdade na tabela original.

UPDATE view_aluno SET ra = '999' WHERE nome = 'Danilo';
-- isso muda a linha na tabela "aluno" de verdade, não só na view
✕

Error 1288 — "target table is not updatable": acontece quando a view combina mais de uma fonte (ex: tem UNION no meio). Views assim são só para leitura.

Error 1423 — "underlying table doesn't have a default value": aparece como warning ao dar INSERT numa view que não inclui todas as colunas NOT NULL da tabela original — o insert até funciona, mas as colunas de fora ficam com valor "vazio" (string vazia, 0000-00-00...), o que geralmente não é o que você quer.

DROP VIEW view_aluno;
12

Trigger

Um TRIGGER (gatilho) dispara uma ação automaticamente quando um INSERT, UPDATE ou DELETE acontece numa tabela — antes (BEFORE) ou depois (AFTER) do evento. Serve tanto pra validar dados antes de salvar quanto pra registrar histórico automaticamente.

TabelaColunas
salarioid, nome_funcionario, remuneracao
log_salarioid, id_salario, nome_funcionario, remuneracao_anterior, remuneracao_nova, datahora, usuario
Sintaxe base
CREATE TRIGGER <nome trigger>
<BEFORE ou AFTER> <INSERT ou UPDATE ou DELETE>
ON <tabela>
FOR EACH ROW
BEGIN
    <comandos>
END;
!

DELIMITER: os comandos dentro do trigger já usam ; pra terminar cada linha — e ; também é o que marca o fim normal de uma query. Pra evitar que o MySQL corte o trigger no meio, muda-se o delimitador temporariamente (ex: $$) antes do CREATE TRIGGER, e volta pro padrão (DELIMITER ;) depois do END.

BEFORE INSERT — validar antes de salvar

DELIMITER $$
CREATE TRIGGER trigger_before_insert_salario
BEFORE INSERT ON salario
FOR EACH ROW
BEGIN
    IF NEW.remuneracao < 0 THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'Remuneração deve ser positiva';
    END IF;
END $$
DELIMITER ;

INSERT INTO salario(nome_funcionario, remuneracao) VALUES('Alberto', -1000.00);
-- Error Code: 1644. Remuneração deve ser positiva

NEW representa a linha que está sendo inserida — só existe em triggers de INSERT/UPDATE. SIGNAL SQLSTATE '45000' é como o trigger "lança" um erro personalizado (uma exceção definida pelo usuário), interrompendo o comando e devolvendo a mensagem em MESSAGE_TEXT.

AFTER INSERT — registrar automaticamente

DELIMITER $$
CREATE TRIGGER trigger_after_insert_salario
AFTER INSERT ON salario
FOR EACH ROW
BEGIN
    INSERT INTO log_salario(id_salario, nome_funcionario, remuneracao_anterior,
                            remuneracao_nova, datahora, usuario)
    VALUES (NEW.id, NEW.nome_funcionario, 0.0, NEW.remuneracao,
            CURRENT_TIMESTAMP(), USER());
END $$
DELIMITER ;

Todo INSERT em salario agora grava sozinho uma linha em log_salario — sem precisar lembrar de fazer isso manualmente em nenhum lugar do sistema.

BEFORE UPDATE — comparar valor antigo com novo

DELIMITER $$
CREATE TRIGGER trigger_before_update_salario
BEFORE UPDATE ON salario
FOR EACH ROW
BEGIN
    IF OLD.remuneracao > NEW.remuneracao THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'Não pode diminuir o salário do funcionário';
    END IF;
END $$
DELIMITER ;

UPDATE salario SET remuneracao = 500 WHERE id = 1;
-- Error Code: 1644. Não pode diminuir o salário do funcionário

Em triggers de UPDATE existem os dois: OLD (a linha antes da mudança) e NEW (a linha depois) — dá pra comparar os dois valores na mesma condição.

AFTER UPDATE — buscando o valor anterior com subquery

DELIMITER $$
CREATE TRIGGER trigger_after_update_salario
AFTER UPDATE ON salario
FOR EACH ROW
BEGIN
    DECLARE salario_antigo DECIMAL(9,2);
    SET salario_antigo = (SELECT remuneracao_nova
                          FROM log_salario
                          WHERE nome_funcionario = NEW.nome_funcionario
                          ORDER BY id
                          LIMIT 1);
    INSERT INTO log_salario(id_salario, nome_funcionario, remuneracao_anterior, remuneracao_nova, datahora, usuario)
    VALUES (NEW.id, NEW.nome_funcionario, salario_antigo, NEW.remuneracao, CURRENT_TIMESTAMP(), USER());
END $$
DELIMITER ;

DECLARE cria uma variável local dentro do trigger — precisa vir logo no começo do BEGIN. Aqui ela guarda o último salário já registrado no log, pra usar como "remuneração anterior" da nova linha.

BEFORE DELETE — regra de negócio antes de apagar

DELIMITER $$
CREATE TRIGGER trigger_before_delete_salario
BEFORE DELETE ON salario
FOR EACH ROW
BEGIN
    DECLARE media_salario DECIMAL(9,2);
    DECLARE maximo_salario DECIMAL(9,2);
    SET media_salario = (SELECT AVG(remuneracao) FROM salario);
    SET maximo_salario = (SELECT MAX(remuneracao) FROM salario);
    IF OLD.remuneracao < (media_salario/2) OR OLD.remuneracao = maximo_salario THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'Não pode demitir quem recebe menos que metade da média, nem o dono (maior salário)';
    END IF;
END $$
DELIMITER ;
✕

Trigger e FOREIGN KEY são coisas diferentes que podem bloquear o mesmo DELETE. No exemplo, tentar apagar quem tem o maior salário é barrado pelo trigger (Error 1644). Já tentar apagar alguém que já tem linha em log_salario é barrado pela FOREIGN KEY (Error 1451 — "Cannot delete or update a parent row"), não pelo trigger: o trigger até deixaria, mas a integridade referencial não deixa deletar um pai que ainda tem filho apontando pra ele.

AFTER DELETE

DELIMITER $$
CREATE TRIGGER trigger_after_delete_salario
AFTER DELETE ON salario
FOR EACH ROW
BEGIN
    <comandos>
END $$
DELIMITER ;

Se o DELETE foi permitido, o AFTER DELETE roda depois — útil pra "limpar atrás", tipo apagar registros relacionados em outra tabela.

Consultar triggers já criados

SHOW TRIGGERS;
SHOW CREATE TRIGGER trigger_before_insert_salario;
!

Trigger não pode alterar a própria tabela que o disparou: um trigger de INSERT na tabela salario não pode fazer INSERT de volta em salario — o MySQL bloqueia (Error 1442) pra evitar loop infinito.

Loop entre tabelas ainda é possível: se um trigger da tabela 1 escreve na tabela 2, e um trigger da tabela 2 escreve de volta na tabela 1, isso não é bloqueado automaticamente — cuidado ao desenhar triggers encadeados entre tabelas diferentes.

13

Function

Uma FUNCTION definida pelo usuário funciona igual as prontas do MySQL (AVG, COUNT, MAX...) — recebe parâmetros, calcula algo e devolve um único valor com RETURN. Dá pra chamar dentro de um SELECT, coluna a coluna.

Sintaxe base
DELIMITER $$
CREATE FUNCTION <nome função>(<parâmetros>)
RETURNS <tipo retorno>
<tipo de função>
BEGIN
    <comandos>
    RETURN <retorno>;
END$$
DELIMITER ;

Condição simples com IF/ELSE

DELIMITER $$
CREATE FUNCTION verificar_salario_2000(salario DECIMAL(9,2))
RETURNS VARCHAR(100)
DETERMINISTIC
BEGIN
    DECLARE resultado VARCHAR(100);
    IF salario >= 2000 THEN SET resultado = 'Maior ou igual a 2000';
    ELSE SET resultado = 'Menor que 2000';
    END IF;
    RETURN resultado;
END$$
DELIMITER ;

SELECT *, verificar_salario_2000(remuneracao) FROM salario;
nome_funcionarioremuneracaoverificar_salario_2000(remuneracao)
Alberto1000.00Menor que 2000
Bruno1500.00Menor que 2000
Clóvis2000.00Maior ou igual a 2000
Danilo5000.00Maior ou igual a 2000

DETERMINISTIC avisa o MySQL que a função sempre devolve o mesmo resultado pros mesmos parâmetros de entrada — obrigatório declarar (ou NOT DETERMINISTIC) dependendo da configuração do servidor.

Calculando algo com base em outra tabela

DELIMITER $$
CREATE FUNCTION verificar_salario_media(salario DECIMAL(9,2))
RETURNS VARCHAR(100)
DETERMINISTIC
BEGIN
    DECLARE media DECIMAL(9,2);
    DECLARE resultado VARCHAR(100);
    SET media = (SELECT AVG(remuneracao) FROM salario);
    IF salario > media THEN SET resultado = 'Salário maior do que a média';
    ELSE SET resultado = 'Salário menor ou igual do que a media';
    END IF;
    RETURN resultado;
END$$
DELIMITER ;

A função roda a subquery SELECT AVG(remuneracao) FROM salario toda vez que é chamada — pra cada linha do SELECT externo. Funciona bem numa tabela pequena; numa tabela grande, vale pensar no custo de recalcular a média linha a linha.

Mais de um parâmetro

DELIMITER $$
CREATE FUNCTION calcular_total_salario(salario DECIMAL(9,2), meses INT)
RETURNS DECIMAL(9,2)
DETERMINISTIC
BEGIN
    DECLARE resultado DECIMAL(9,2);
    SET resultado = salario * meses;
    RETURN resultado;
END$$
DELIMITER ;

SELECT nome_funcionario, calcular_total_salario(remuneracao, 8) FROM salario;
-- Alberto: 8000.00 · Bruno: 12000.00 · Clóvis: 16000.00 · Danilo: 40000.00
DROP FUNCTION verificar_salario_2000;
✓

Trigger vs Function, em uma frase: trigger reage sozinho a um INSERT/UPDATE/DELETE (você nunca "chama" ele); function você chama explicitamente, geralmente dentro de um SELECT, e ela sempre devolve um valor.

14

Stored Procedure

Uma PROCEDURE é um bloco de comandos SQL guardado no banco e executado com CALL. Diferente da function, ela não precisa devolver valor: pode fazer INSERT/UPDATE, mostrar um SELECT ou devolver resultados por parâmetros OUT.

Sintaxe base
DELIMITER $$
CREATE PROCEDURE <nome>(<IN | OUT | INOUT> <parâmetro> <tipo>, ...)
BEGIN
    <comandos>
END$$
DELIMITER ;

CALL <nome>(<argumentos>);
DROP PROCEDURE <nome>;

O DELIMITER $$ troca o fim de comando de ; para $$, senão o Workbench encerra a criação no primeiro ; de dentro do corpo.

Os três tipos de parâmetro

ModoSentidoUso
INentra (é o padrão)A procedure só lê o valor recebido
OUTsaiA procedure preenche; quem chamou lê depois em uma variável @x
INOUTentra e saiRecebe um valor, altera e devolve no mesmo parâmetro

Só entrada (IN)

DELIMITER $$
CREATE PROCEDURE selecionar_1_disciplina(IN p_id INT)
BEGIN
    SELECT * FROM disciplina WHERE id = p_id;
END$$
DELIMITER ;

CALL selecionar_1_disciplina(3);

Devolvendo valor (OUT)

DELIMITER $$
CREATE PROCEDURE contar_automoveis(OUT p_total INT)
BEGIN
    SELECT COUNT(*) INTO p_total FROM automovel;
END$$
DELIMITER ;

CALL contar_automoveis(@total);
SELECT @total;

SELECT ... INTO guarda o resultado dentro do parâmetro. Se a consulta não devolver nenhuma linha, o MySQL dá o aviso 1329 No data; se devolver mais de uma, o erro 1172.

Entrada e saída juntas (INOUT)

DELIMITER $$
CREATE PROCEDURE dobrar(INOUT p_numero INT)
BEGIN
    SET p_numero = p_numero * 2;
END$$
DELIMITER ;

SET @n = 7;
CALL dobrar(@n);
SELECT @n;   -- 14

No INOUT o argumento precisa ser uma variável (@n) já com valor; passar um número literal dá erro, porque não há onde devolver o resultado.

Laços: WHILE e REPEAT

DELIMITER $$
CREATE PROCEDURE contagem_regressiva(IN p_inicio INT)
BEGIN
    WHILE p_inicio > 0 DO
        SELECT p_inicio;
        SET p_inicio = p_inicio - 1;
    END WHILE;
END$$

CREATE PROCEDURE contagem_regressiva_repeat(IN p_inicio INT)
BEGIN
    REPEAT
        SELECT p_inicio;
        SET p_inicio = p_inicio - 1;
    UNTIL p_inicio <= 0
    END REPEAT;
END$$
DELIMITER ;

WHILE testa a condição antes (pode nem rodar); REPEAT testa depois (roda ao menos uma vez) e termina quando a condição do UNTIL fica verdadeira — a lógica é invertida em relação ao WHILE.

Decisão com IF e erro com SIGNAL

DELIMITER $$
CREATE PROCEDURE gerenciar_automovel(IN p_acao VARCHAR(10), IN p_id INT)
BEGIN
    IF p_acao = 'EXCLUIR' THEN
        IF NOT EXISTS (SELECT 1 FROM automovel WHERE id = p_id) THEN
            SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Erro - Id não existe';
        END IF;
        DELETE FROM automovel WHERE id = p_id;
    ELSE
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Erro - Ação desconhecida';
    END IF;
END$$
DELIMITER ;

CALL gerenciar_automovel('EXCLUIR', 99);
-- Error Code: 1644. Erro - Id não existe

É o mesmo SIGNAL do trigger: interrompe a procedure com o erro 1644 e a mensagem que você escolheu. O SQLSTATE '45000' significa "erro definido pelo usuário".

✓

Procedure vs Function: a procedure é chamada com CALL, pode ter OUT/INOUT e pode alterar dados à vontade. A function é usada dentro de um SELECT e devolve exatamente um valor com RETURN.

15

Referência rápida de erros

Os erros reais que apareceram nos exercícios — guarde essa tabela, 90% dos erros de SQL de iniciante se repetem.

CódigoMensagemCausaSolução
1072 Key column 'x' doesn't exist in table FK aponta pra uma coluna que não foi criada Criar a coluna antes do FOREIGN KEY
1050 Table 'x' already exists Tentou criar uma tabela que já existe DROP TABLE IF EXISTS antes, se quiser recriar
1136 Column count doesn't match value count INSERT sem lista de colunas, com número de valores errado Sempre listar as colunas: INSERT INTO t (a,b) VALUES (...)
1075 Incorrect table definition — autoincrement needs a key Coluna AUTO_INCREMENT sem ser chave/índice Declarar PRIMARY KEY nessa coluna
1175 You are using safe update mode... Workbench bloqueando UPDATE/DELETE sem chave "reconhecível" no WHERE SET SQL_SAFE_UPDATES = 0; ou desativar em Preferences
— Subquery returns more than 1 row Subquery escalar (num SET/SELECT) devolveu várias linhas Usar JOIN/GROUP BY em vez de subquery escalar, ou repensar o filtro
— NOT IN não retorna nada A lista da subquery tem um NULL no meio Trocar por NOT EXISTS
1054 Unknown column 'x' in 'where clause' Usar o alias de uma função de agregação (ex: media) dentro do WHERE Trocar WHERE por HAVING
1111 Invalid use of group function Usar AVG()/COUNT() etc. direto no WHERE Trocar WHERE por HAVING
1241 Operand should contain 1 column(s) Subquery usada como coluna do SELECT devolvendo mais de 1 coluna Selecionar só 1 coluna na subquery, ou usar JOIN
1288 The target table of the UPDATE/DELETE is not updatable UPDATE/DELETE/INSERT numa view que combina mais de 1 fonte (ex: tem UNION) Alterar a tabela original diretamente — view assim é só leitura
1423 Field of view '...' underlying table doesn't have a default value INSERT numa view que não cobre todas as colunas NOT NULL da tabela original Incluir as colunas obrigatórias na view, ou inserir direto na tabela
1644 <mensagem definida por você> Um TRIGGER disparou SIGNAL SQLSTATE '45000' — exceção personalizada bloqueando a operação Esperado: ajustar o dado pra respeitar a regra de negócio do trigger
1442 Can't update table 'x' in stored function/trigger because it is already used by statement which invoked this stored function/trigger Um TRIGGER tentou fazer INSERT/UPDATE na mesma tabela que disparou ele (evita loop infinito) Mover essa lógica pra uma tabela diferente, ou tratar fora do trigger
1305 PROCEDURE banco.x does not exist CALL de uma procedure que não foi criada (ou nome/banco errado) Rodar o CREATE PROCEDURE antes e conferir o nome e o banco em uso
1318 Incorrect number of arguments for PROCEDURE CALL com mais ou menos argumentos do que a procedure declara Passar um argumento para cada parâmetro, na ordem (inclusive os OUT)
1064 You have an error in your SQL syntax (ao criar procedure) Faltou o DELIMITER $$: o Workbench cortou o corpo no primeiro ; Envolver com DELIMITER $$ ... END$$ DELIMITER ;
1451 Cannot delete or update a parent row: a foreign key constraint fails Tentou apagar uma linha que ainda tem outra tabela apontando pra ela via FOREIGN KEY Apagar/atualizar primeiro os registros filhos, ou configurar ON DELETE CASCADE
→

De olho no próximo exercício

✓

Exercício 8 (tabelas automovel e tabuada) usa STORED PROCEDURE: uma procedure sem parâmetros que preenche a tabuada de 1 a 10 com dois WHILE aninhados, outra com n1/n2 de entrada e o id de saída (OUT), uma com INOUT que testa se o número é primo (senão multiplica pelas ocorrências na tabuada) e uma de 7 parâmetros que faz INSERIR/ATUALIZAR/EXCLUIR lançando erro com SIGNAL ("Erro - Placa já cadastrada", "Erro - Id não existe", "Erro - Ação desconhecida"). Entrega em um único arquivo .sql.

Prazo: Turma A — 27/09 · Turma B — 20/09.