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;
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.
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.
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;
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;
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;
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;
COUNT(*) conta linhas; COUNT(coluna) ignora os NULL.
SELECT COUNT(*) AS total FROM automovel;
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;
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;
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;
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);
Subquery no WHERE. Não dá para usar AVG() direto no WHERE.
SELECT modelo, preco FROM automovel WHERE preco > (SELECT AVG(preco) FROM automovel);
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);
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;
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;
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;
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;
% é o coringa: '%de%' contém, 'de%' começa, '%de' termina.
SELECT id, nome FROM disciplina WHERE nome LIKE '%de%';
valor % 2 = 0 é par; % 3 = 0 é múltiplo de 3.
SELECT valor FROM numero WHERE valor % 2 = 0;
Agrupe pela coluna e filtre HAVING COUNT(*) > 1.
SELECT nome, COUNT(*) AS vezes FROM pessoa GROUP BY nome HAVING COUNT(*) > 1;
DISTINCT tira linhas repetidas do resultado (não apaga nada da tabela).
SELECT DISTINCT profissao FROM pessoa;
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;
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;
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;
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;
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;
Í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';
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 ;
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 ;
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;
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;
Em trigger ou procedure. Aparece como erro 1644 com o texto escolhido.
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Erro - Id não existe';
LIMIT tamanho OFFSET quantos_pular. Página 2 de 10 em 10 = OFFSET 10.
SELECT * FROM pessoa ORDER BY id LIMIT 10 OFFSET 10;
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;
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.
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.
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).
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.
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.
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.
SELECT nome AS nome_completo, CASE WHEN cnh = 'S' THEN 'Sim' ELSE 'Não' END AS possui_cnh FROM pessoa;
| Cláusula | Pra que serve |
|---|---|
| AS | Renomear coluna/tabela só na exibição do resultado |
| WHERE | Filtrar linhas antes de agrupar |
| ORDER BY ... DESC | Ordenar do maior pro menor |
| LIMIT n | Trazer só as n primeiras linhas do resultado |
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.
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.
| Comando | O que traz |
|---|---|
| UNION | Tudo dos dois SELECTs, sem repetir valores iguais |
| UNION ALL | Tudo dos dois SELECTs, pode repetir |
| INTERSECT | Só o que aparece nos dois resultados |
| EXCEPT | O que está no 1º SELECT e não está no 2º |
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.
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.
SELECT pessoa.nome, automovel.modelo FROM pessoa JOIN automovel ON automovel.id_pessoa = pessoa.id;
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
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.
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.
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;
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.
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.
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.
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;
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.
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.
| Onde | Exemplo | Cuidado |
|---|---|---|
| Como coluna | SELECT valor, (SELECT texto FROM t WHERE ...) | Só pode devolver 1 coluna — senão Error 1241 |
| No WHERE | WHERE valor IN (SELECT ...) | Pode devolver várias linhas |
| No FROM (como tabela) | FROM (SELECT ...) AS tabela | Precisa de AS pra dar nome à tabela derivada |
| No HAVING | HAVING valor IN (SELECT ...) | Filtra grupos já formados |
| No ORDER BY | ORDER 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.
| Comando | Uso | Exemplo |
|---|---|---|
| INSERT | Como fonte inteira dos dados | INSERT INTO t SELECT ... FROM outra |
| INSERT | Como parte de um valor | VALUES ((SELECT COUNT(*)+1 FROM t), '') |
| UPDATE | Como novo valor | SET texto = (SELECT ... WHERE id = t.id) |
| UPDATE | Como filtro | WHERE valor IN (SELECT ...) |
| DELETE | Como filtro | WHERE 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).
UPDATE sem WHERE muda a tabela inteira. Sempre confira com um SELECT usando a mesma condição antes de rodar o UPDATE de verdade.
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;
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;
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.
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.
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
| Situação | Como a view ajuda |
|---|---|
| Segurança por coluna | MySQL 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 recorrente | Um relatório com vários JOINs vira um SELECT * FROM view_relatorio simples |
| Combinar várias tabelas | O 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.
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;
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.
| Tabela | Colunas |
|---|---|
| salario | id, nome_funcionario, remuneracao |
| log_salario | id, id_salario, nome_funcionario, remuneracao_anterior, remuneracao_nova, datahora, usuario |
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.
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.
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.
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.
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.
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.
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.
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.
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.
DELIMITER $$ CREATE FUNCTION <nome função>(<parâmetros>) RETURNS <tipo retorno> <tipo de função> BEGIN <comandos> RETURN <retorno>; END$$ DELIMITER ;
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_funcionario | remuneracao | verificar_salario_2000(remuneracao) |
|---|---|---|
| Alberto | 1000.00 | Menor que 2000 |
| Bruno | 1500.00 | Menor que 2000 |
| Clóvis | 2000.00 | Maior ou igual a 2000 |
| Danilo | 5000.00 | Maior 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.
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.
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.
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.
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.
| Modo | Sentido | Uso |
|---|---|---|
| IN | entra (é o padrão) | A procedure só lê o valor recebido |
| OUT | sai | A procedure preenche; quem chamou lê depois em uma variável @x |
| INOUT | entra e sai | Recebe um valor, altera e devolve no mesmo parâmetro |
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);
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.
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.
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.
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.
Os erros reais que apareceram nos exercícios — guarde essa tabela, 90% dos erros de SQL de iniciante se repetem.
| Código | Mensagem | Causa | Soluçã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 |
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.