flowchart TD
A[Aplicacao envia UPDATE] --> B{Existe trigger BEFORE?}
B -- Sim --> C[Executa funcao do gatilho]
C --> D[Pode alterar NEW ou abortar]
B -- Nao --> E[Aplica modificacao na tabela]
D --> E
E --> F{Existe trigger AFTER?}
F -- Sim --> G[Executa pos-processamento]
F -- Nao --> H[Confirma dentro da transacao]
G --> H
H --> I[Controle volta a aplicacao]
Gatilhos, funções e procedimentos armazenados
Chegamos a um dos módulos mais saborosos do nosso percurso, e eu te garanto: este tema costuma render questões muito boas em prova. Até aqui, você e eu tratamos o banco de dados quase como um repositório obediente — ele guarda o que mandamos guardar e devolve o que pedimos. Agora vou te mostrar que um SGBDR moderno é bem mais ativo do que isso. Ele pode reagir sozinho a eventos, pode encapsular lógica reutilizável e pode executar rotinas inteiras de negócio sem que a aplicação cliente precise saber dos detalhes. Os três protagonistas dessa virada são os gatilhos, as funções e os procedimentos armazenados. Vou usar o PostgreSQL como nosso laboratório, porque a banca da USP gosta de cobrar PostgreSQL e porque a linguagem PL/pgSQL é didática e expressiva. Repare comigo, ao longo do texto, em uma distinção que separa os três: gatilhos são reativos (disparam sozinhos), enquanto funções e procedimentos são invocados explicitamente por quem os chama.
O que é um gatilho e por que ele é reativo
Um gatilho — ou trigger — é um objeto de banco de dados que o próprio SGBDR executa automaticamente quando um determinado evento acontece em uma tabela. Esse evento, na esmagadora maioria dos casos, é uma operação de manipulação de dados: um INSERT, um UPDATE ou um DELETE. Alguns sistemas também permitem gatilhos sobre eventos de definição de dados (DDL), como a criação de uma tabela, mas o coração do assunto está nos eventos DML. A característica que eu quero que você grave é justamente esta natureza reativa: ninguém “chama” um gatilho. Você não escreve EXECUTE meu_gatilho. Você simplesmente faz um UPDATE numa tabela, e se houver um gatilho associado àquela tabela para aquele evento, o SGBDR o dispara implicitamente, dentro da mesma transação. É como um sensor de presença: você não aperta um botão, você apenas passa, e a luz acende.
Essa diferença em relação a procedimentos e funções é o ponto que distingue conceitualmente o gatilho. Procedimentos e funções são passivos no sentido de que esperam ser chamados; o gatilho é vigilante. Por isso ele é a ferramenta natural para três grandes propósitos que vão reaparecer em toda a sua prova: garantir integridade de dados que as restrições declarativas comuns não conseguem expressar, fazer auditoria automática de alterações e automatizar cálculos e propagações derivadas.
Definição. Um gatilho é um objeto associado a uma tabela (ou visão) que o SGBDR executa automaticamente em resposta a um evento DML (INSERT, UPDATE, DELETE), seja antes (BEFORE), depois (AFTER) ou no lugar (INSTEAD OF) da operação, podendo atuar uma vez por linha afetada ou uma vez por comando.
Classificando os gatilhos: momento, nível e evento
Quando estudamos gatilhos, três eixos de classificação aparecem juntos, e vale a pena montar isso na cabeça de forma organizada. O primeiro eixo é o momento de disparo. Um gatilho BEFORE roda antes de a operação tocar efetivamente os dados; ele é o lugar certo para validar ou ajustar valores, porque ainda dá tempo de modificar a linha que está prestes a ser gravada. Um gatilho AFTER roda depois que a operação já se concretizou com sucesso; é onde colocamos pós-processamento, como gravar um log de auditoria ou atualizar uma tabela relacionada. Já o gatilho INSTEAD OF substitui a operação original — ele não acontece nem antes nem depois, ele acontece no lugar dela. No PostgreSQL, o INSTEAD OF existe exclusivamente sobre visões, e é o truque que torna atualizável uma visão que, por sua complexidade, não seria atualizável de forma automática.
O segundo eixo é o nível de atuação. Um gatilho de linha (FOR EACH ROW) é executado uma vez para cada linha afetada pela operação. Se um único UPDATE atinge mil linhas, um gatilho de linha dispara mil vezes, e em cada disparo ele tem acesso às pseudovariáveis OLD e NEW, que representam, respectivamente, a versão antiga e a nova da linha. Um gatilho de comando (FOR EACH STATEMENT) é executado uma única vez por comando SQL, independentemente de quantas linhas foram tocadas — inclusive se foram zero. Nesse nível não há OLD nem NEW individuais, porque o foco é a operação como um todo, não cada registro.
O terceiro eixo é o evento, e aqui já conversamos: INSERT, UPDATE, DELETE e, em alguns SGBDR, eventos DDL. Vou organizar isso numa tabela para você revisar de relance.
| Eixo | Opções | Quando usar |
|---|---|---|
| Momento | BEFORE |
Validar ou ajustar dados antes de gravar |
| Momento | AFTER |
Auditoria, propagação, notificação pós-gravação |
| Momento | INSTEAD OF |
Tornar visões atualizáveis (só em visões) |
| Nível | FOR EACH ROW |
Lógica por linha, com acesso a OLD/NEW |
| Nível | FOR EACH STATEMENT |
Lógica única por comando |
| Evento | INSERT / UPDATE / DELETE |
Conforme a operação monitorada |
Há um detalhe de implementação do PostgreSQL que cai em prova e que muita gente erra: no PostgreSQL você não escreve o corpo do gatilho dentro do CREATE TRIGGER. Você primeiro escreve uma função de gatilho — uma função especial que retorna o tipo trigger — e depois associa essa função à tabela com CREATE TRIGGER. Isso difere de SGBDR como MySQL, onde o corpo vai direto no comando do gatilho. Guarde essa separação: função de gatilho de um lado, comando de associação do outro.
Como um gatilho se encaixa no ciclo de uma operação DML
Quero que você visualize o caminho que um comando percorre quando há gatilhos no meio. Imagine um UPDATE chegando ao servidor. O SGBDR primeiro identifica que existe um gatilho BEFORE de linha; para cada linha, ele monta as pseudovariáveis OLD (valores atuais) e NEW (valores propostos) e executa a função de gatilho. Se essa função alterar NEW, é a versão alterada que será efetivamente gravada. Só então a modificação física acontece. Em seguida, se houver um gatilho AFTER, ele roda já com os dados consolidados. E tudo isso vive dentro da mesma transação — se qualquer etapa lançar um erro, a transação inteira é revertida.
Repare numa consequência importante desse diagrama: o gatilho BEFORE tem poder de interferir no que será gravado, ao passo que o AFTER apenas observa o que já foi gravado. Essa é a regra prática para escolher o momento. Se você precisa mudar o dado, é BEFORE. Se você precisa reagir ao dado já gravado, é AFTER.
stateDiagram-v2
[*] --> ComandoRecebido
ComandoRecebido --> AvaliaBefore: dispara BEFORE
AvaliaBefore --> Gravacao: NEW validado
AvaliaBefore --> Rollback: erro lancado
Gravacao --> AvaliaAfter: dispara AFTER
AvaliaAfter --> Commit: tudo ok
AvaliaAfter --> Rollback: erro lancado
Commit --> [*]
Rollback --> [*]
Gatilhos na prática: auditoria, validação e automação
Vamos sair da teoria. Suponha que você mantenha uma tabela de produtos e que a área de conformidade exija um registro de toda alteração de preço. Esse é o caso clássico de auditoria, e ele pede um gatilho AFTER UPDATE de linha. Primeiro eu crio a tabela de log e a função de gatilho.
CREATE TABLE produtos (
id SERIAL PRIMARY KEY,
nome VARCHAR(120) NOT NULL,
preco NUMERIC(10,2) NOT NULL,
estoque INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE produtos_auditoria (
id SERIAL PRIMARY KEY,
produto_id INTEGER NOT NULL,
campo VARCHAR(40) NOT NULL,
valor_antigo TEXT,
valor_novo TEXT,
alterado_em TIMESTAMP NOT NULL DEFAULT now(),
alterado_por TEXT NOT NULL DEFAULT current_user
);
CREATE OR REPLACE FUNCTION fn_auditar_produto()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.preco IS DISTINCT FROM OLD.preco THEN
INSERT INTO produtos_auditoria (produto_id, campo, valor_antigo, valor_novo)
VALUES (OLD.id, 'preco', OLD.preco::TEXT, NEW.preco::TEXT);
END IF;
IF NEW.estoque IS DISTINCT FROM OLD.estoque THEN
INSERT INTO produtos_auditoria (produto_id, campo, valor_antigo, valor_novo)
VALUES (OLD.id, 'estoque', OLD.estoque::TEXT, NEW.estoque::TEXT);
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_auditar_produto
AFTER UPDATE ON produtos
FOR EACH ROW
EXECUTE FUNCTION fn_auditar_produto();Observe três coisas que eu fiz de propósito. Usei IS DISTINCT FROM em vez de <> porque esse operador trata NULL corretamente — ele considera diferentes uma coluna que era NULL e passou a ter valor, algo que o <> simples não detecta. Registrei current_user como autor da mudança, o que dá rastreabilidade. E como é um gatilho AFTER, o RETURN NEW aqui é apenas convenção; o valor de retorno de um gatilho AFTER FOR EACH ROW é ignorado pelo PostgreSQL. Esse último ponto contrasta com o BEFORE, onde o retorno é determinante, como você verá a seguir.
Atenção ao valor de retorno. Em um gatilho BEFORE ... FOR EACH ROW, retornar NEW deixa a operação prosseguir com a linha (eventualmente modificada); retornar NULL cancela silenciosamente a operação para aquela linha. Em um gatilho AFTER ... FOR EACH ROW, o retorno é ignorado, mas é boa prática retornar NULL ou NEW por clareza. Confundir esses comportamentos é um erro recorrente.
Agora um exemplo de validação que só um gatilho resolve bem. Quero impedir que o salário de um funcionário seja reduzido — uma regra de negócio que nenhuma CHECK simples expressa, pois ela compara o valor novo com o valor anterior. Esse é o território do BEFORE UPDATE.
CREATE OR REPLACE FUNCTION fn_impede_reducao_salario()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.salario < OLD.salario THEN
RAISE EXCEPTION 'Reducao de salario nao permitida: % para %',
OLD.salario, NEW.salario;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_impede_reducao_salario
BEFORE UPDATE ON funcionarios
FOR EACH ROW
EXECUTE FUNCTION fn_impede_reducao_salario();O RAISE EXCEPTION aborta a operação e, por estar dentro da transação, desfaz tudo. É o jeito PL/pgSQL de fazer o que outros SGBDR fazem com SIGNAL ou RAISERROR. Repare como o BEFORE é o momento natural: estamos vetando a gravação antes que ela ocorra.
Para automação, considere o cálculo do total de um pedido sempre que um item muda. Aqui há uma sutileza que eu quero que você perceba: em um DELETE, a pseudovariável NEW não existe, e em um INSERT a OLD não existe. Por isso, ao escrever uma função que serve a vários eventos, usamos a variável especial TG_OP para descobrir qual operação disparou o gatilho e escolher de onde tirar o pedido_id.
CREATE OR REPLACE FUNCTION fn_recalcula_total_pedido()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
v_pedido_id INTEGER;
BEGIN
IF TG_OP = 'DELETE' THEN
v_pedido_id := OLD.pedido_id;
ELSE
v_pedido_id := NEW.pedido_id;
END IF;
UPDATE pedidos
SET total = COALESCE((
SELECT SUM(quantidade * preco_unitario)
FROM itens_pedido
WHERE pedido_id = v_pedido_id
), 0)
WHERE id = v_pedido_id;
RETURN NULL;
END;
$$;
CREATE TRIGGER trg_recalcula_total
AFTER INSERT OR UPDATE OR DELETE ON itens_pedido
FOR EACH ROW
EXECUTE FUNCTION fn_recalcula_total_pedido();Um único CREATE TRIGGER cobrindo INSERT OR UPDATE OR DELETE é elegante e evita duplicação. O COALESCE garante que, ao remover o último item, o total caia para zero em vez de virar NULL.
Cuidado com cascatas e recursão. Um gatilho pode disparar outro gatilho, e até disparar a si mesmo se modificar a própria tabela que monitora. O PostgreSQL gerencia essas cascatas, mas você precisa projetá-las com cuidado: laços de gatilhos podem levar a recursão infinita ou a custos de desempenho silenciosos. Como toda essa cadeia roda dentro da transação original, um erro em qualquer ponto reverte o conjunto inteiro.
Para deixar a cadeia visual, veja o disparo combinado em uma operação que toca duas tabelas em sequência.
sequenceDiagram
participant App as Aplicacao
participant IP as itens_pedido
participant TG as Gatilho AFTER
participant PE as pedidos
App->>IP: INSERT novo item
IP->>TG: dispara FOR EACH ROW
TG->>PE: UPDATE total recalculado
PE-->>App: transacao confirmada
Funções: encapsulando lógica que retorna valor
Mudemos de protagonista. Uma função em um SGBDR é um objeto que encapsula um conjunto de instruções e lógica procedural com um propósito definido e que retorna um resultado. Diferentemente do gatilho, ela é invocada explicitamente — você a usa numa cláusula SELECT, num WHERE, num JOIN ou dentro de outro objeto. As vantagens que ela traz são as que você já intui: reúso de código, modularidade, manutenção centralizada e, frequentemente, desempenho, já que a lógica roda perto dos dados e o SGBDR pode otimizá-la.
As funções costumam ser classificadas pelo que retornam. Uma função escalar devolve um único valor por chamada — uma idade, um desconto, um booleano de validação. Uma função de tabela devolve um conjunto de linhas, comportando-se como uma tabela virtual que você pode consultar no FROM. E uma função agregada condensa um conjunto de valores em um único resultado, como fazem SUM ou AVG, sendo que vários SGBDR permitem definir agregadas personalizadas. Há ainda as funções de sistema, já prontas no SGBDR, como upper, now ou coalesce, que você usa o tempo todo sem pensar nelas como funções.
| Tipo de função | Retorno | Exemplo de uso |
|---|---|---|
| Escalar | Um valor | WHERE calcula_idade(nascimento) > 40 |
| De tabela | Conjunto de linhas | FROM relatorio_vendas('2025-01-01','2025-12-31') |
| Agregada | Um valor sobre um conjunto | SELECT mediana(nota) FROM provas |
| De sistema | Variado, pré-definida | SELECT upper(nome) |
Vou te mostrar uma escalar bem concreta em PL/pgSQL. Ela calcula a idade de uma pessoa de forma exata, levando em conta se o aniversário já passou no ano corrente.
CREATE OR REPLACE FUNCTION calcula_idade(p_nascimento DATE)
RETURNS INTEGER
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
v_idade INTEGER;
BEGIN
v_idade := date_part('year', age(p_nascimento));
RETURN v_idade;
END;
$$;
-- Uso em uma consulta:
SELECT id, nome, calcula_idade(data_nascimento) AS idade
FROM funcionarios
WHERE calcula_idade(data_nascimento) >= 40;Marquei a função como IMMUTABLE. Esse marcador anuncia ao otimizador que, para a mesma entrada, a função sempre devolve a mesma saída, o que permite ao PostgreSQL cachear o resultado e otimizar consultas. Existem três níveis de volatilidade que valem a memorização: IMMUTABLE (saída depende só dos argumentos), STABLE (saída não muda dentro de um mesmo comando, mas pode mudar entre comandos — típico de funções que leem o banco) e VOLATILE (pode mudar a cada chamada, é o padrão e o mais permissivo). Declarar a volatilidade certa não é firula: é desempenho.
CREATE OR REPLACE FUNCTION calcula_desconto(p_total NUMERIC)
RETURNS NUMERIC
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
v_percentual NUMERIC(5,2);
BEGIN
v_percentual := CASE
WHEN p_total < 100 THEN 0
WHEN p_total BETWEEN 100 AND 500 THEN 5
WHEN p_total BETWEEN 501 AND 1000 THEN 10
ELSE 15
END;
RETURN ROUND(p_total * v_percentual / 100, 2);
END;
$$;
SELECT id, total, calcula_desconto(total) AS desconto
FROM pedidos;Agora uma função de tabela, que devolve várias linhas. No PostgreSQL usamos RETURNS TABLE(...) e a instrução RETURN QUERY. Esta função entrega um relatório de vendas para um intervalo de datas, e você a consulta como se fosse uma tabela.
CREATE OR REPLACE FUNCTION relatorio_vendas(p_inicio DATE, p_fim DATE)
RETURNS TABLE (
data_pedido DATE,
produto VARCHAR,
quantidade INTEGER,
preco_unit NUMERIC,
total_venda NUMERIC
)
LANGUAGE plpgsql
STABLE
AS $$
BEGIN
RETURN QUERY
SELECT p.data,
pr.nome,
ip.quantidade,
ip.preco_unitario,
(ip.quantidade * ip.preco_unitario)
FROM pedidos p
JOIN itens_pedido ip ON ip.pedido_id = p.id
JOIN produtos pr ON pr.id = ip.produto_id
WHERE p.data BETWEEN p_inicio AND p_fim;
END;
$$;
-- Uso como se fosse uma tabela:
SELECT * FROM relatorio_vendas('2025-01-01', '2025-12-31')
ORDER BY data_pedido, total_venda DESC;Repare que a marquei como STABLE, pois ela lê tabelas e o resultado depende do estado do banco no momento da consulta. Uma consulta de exemplo poderia devolver algo assim:
| data_pedido | produto | quantidade | preco_unit | total_venda |
|---|---|---|---|---|
| 2025-03-12 | Teclado | 2 | 150.00 | 300.00 |
| 2025-03-12 | Mouse | 3 | 80.00 | 240.00 |
E, para fechar o tema das funções, uma agregada personalizada. O PostgreSQL permite definir agregadas a partir de uma função de transição. Vou mostrar uma mediana simples baseada em SQL puro, que é o caminho mais limpo para esse caso específico, lembrando que a SPEC permite tanto PL/pgSQL quanto SQL como linguagem de função.
Boas práticas com funções. Use nomes descritivos, documente parâmetros e comportamento, declare a volatilidade correta (IMMUTABLE/STABLE/VOLATILE), evite efeitos colaterais — uma função idealmente não deveria modificar dados — e prefira funções determinísticas sempre que possível, pois elas dão mais margem ao otimizador. Teste com casos de borda: valores extremos, NULL e conjuntos vazios.
Procedimentos armazenados e o que os separa das funções
Chegamos ao terceiro protagonista. Um procedimento armazenado é um conjunto nomeado de instruções e lógica procedural guardado no banco para execução repetida. À primeira vista parece uma função, e de fato eles compartilham muito. A diferença histórica e conceitual é que a função sempre devolve um resultado e é pensada para ser usada dentro de uma expressão SQL, ao passo que o procedimento é pensado para executar ações — inserir, atualizar, excluir, orquestrar várias operações — e é chamado isoladamente com CALL, podendo nem retornar dado algum.
No PostgreSQL, essa distinção ganhou um contorno técnico decisivo a partir da versão 11, quando os procedimentos foram introduzidos com o CREATE PROCEDURE: um procedimento pode controlar transações por dentro, com COMMIT e ROLLBACK no próprio corpo, enquanto uma função não pode iniciar nem encerrar transações, porque ela executa dentro da transação de quem a chamou. Esse é, na minha leitura, o ponto mais cobrável da comparação. Se você precisa, por exemplo, processar um lote enorme confirmando a cada mil registros, isso só é possível em um procedimento.
| Aspecto | Função | Procedimento |
|---|---|---|
| Retorno | Sempre retorna (valor ou tabela) | Pode não retornar nada |
| Como se chama | Em SELECT, WHERE, FROM, expressões |
Com CALL nome(...) |
| Controle de transação | Não pode (COMMIT/ROLLBACK) |
Pode confirmar/reverter no corpo |
| Uso típico | Cálculo, validação, consulta reutilizável | Orquestrar operações de escrita |
| Parâmetros de saída | Via RETURNS |
Via INOUT |
Vou te mostrar um procedimento que processa um pedido: ele confere estoque, cria o pedido, registra o item, baixa o estoque e devolve o total por um parâmetro INOUT. Note o uso de RAISE EXCEPTION para abortar quando o estoque é insuficiente.
CREATE OR REPLACE PROCEDURE processa_pedido(
p_cliente_id INTEGER,
p_produto_id INTEGER,
p_quantidade INTEGER,
INOUT p_total NUMERIC DEFAULT 0
)
LANGUAGE plpgsql
AS $$
DECLARE
v_estoque INTEGER;
v_preco NUMERIC(10,2);
v_pedido_id INTEGER;
BEGIN
SELECT estoque, preco
INTO v_estoque, v_preco
FROM produtos
WHERE id = p_produto_id;
IF v_estoque IS NULL THEN
RAISE EXCEPTION 'Produto % inexistente', p_produto_id;
END IF;
IF v_estoque < p_quantidade THEN
RAISE EXCEPTION 'Estoque insuficiente: disponivel %, pedido %',
v_estoque, p_quantidade;
END IF;
INSERT INTO pedidos (cliente_id, data, total)
VALUES (p_cliente_id, CURRENT_DATE, p_quantidade * v_preco)
RETURNING id INTO v_pedido_id;
INSERT INTO itens_pedido (pedido_id, produto_id, quantidade, preco_unitario)
VALUES (v_pedido_id, p_produto_id, p_quantidade, v_preco);
UPDATE produtos
SET estoque = estoque - p_quantidade
WHERE id = p_produto_id;
p_total := p_quantidade * v_preco;
END;
$$;
-- Execucao:
CALL processa_pedido(1, 100, 5, 0);Repare que eu nem precisei abrir uma transação explícita aqui, porque cada CALL já roda em sua própria transação e, se o RAISE EXCEPTION disparar, tudo é revertido automaticamente. Mas, para mostrar o poder exclusivo do procedimento, veja um caso de processamento em lote com confirmações parciais — algo impossível dentro de uma função.
CREATE OR REPLACE PROCEDURE reajusta_precos_em_lote(p_percentual NUMERIC)
LANGUAGE plpgsql
AS $$
DECLARE
v_id INTEGER;
v_cnt INTEGER := 0;
BEGIN
FOR v_id IN SELECT id FROM produtos ORDER BY id LOOP
UPDATE produtos
SET preco = ROUND(preco * (1 + p_percentual / 100), 2)
WHERE id = v_id;
v_cnt := v_cnt + 1;
IF v_cnt % 1000 = 0 THEN
COMMIT; -- confirma a cada mil produtos
END IF;
END LOOP;
COMMIT;
END;
$$;
CALL reajusta_precos_em_lote(5.5);Aquele COMMIT no meio do laço é a assinatura inconfundível de um procedimento. Tente colocá-lo numa função e o PostgreSQL recusará. Esse contraste é exatamente o tipo de pegadinha que a banca adora.
Cuidado com a portabilidade. A sintaxe de funções e procedimentos varia bastante entre SGBDR. O que mostrei é PostgreSQL com PL/pgSQL; SQL Server usa T-SQL com @variaveis e EXEC, Oracle usa PL/SQL, MySQL tem suas próprias regras. Mover lógica para o banco aumenta a dependência do fornecedor e reduz a portabilidade — um trade-off real entre desempenho/centralização de um lado e independência de plataforma do outro.
Como os três se entrelaçam
Não pense nesses objetos como ilhas. Eles formam um ecossistema. Um gatilho frequentemente chama um procedimento para executar uma ação complexa após um evento; uma função entra dentro de um procedimento para fazer um cálculo; e um gatilho usa funções para avaliar condições ou transformar dados antes de gravar. No PostgreSQL, aliás, todo gatilho é implementado por uma função de gatilho, então a integração entre gatilho e função é estrutural, não apenas conveniente.
flowchart LR
EV[Evento DML] --> TR[Gatilho]
TR --> FN[Funcao de calculo]
TR --> PR[Procedimento de acao]
PR --> FN
PR --> TB[Tabelas do banco]
FN --> TB
Esse entrelaçamento tem um efeito arquitetural que vale registrar para a prova: ao mover lógica de negócio para dentro do banco, você ganha consistência — todas as aplicações que tocam aqueles dados obedecem às mesmas regras — e ganha desempenho, porque reduz idas e vindas pela rede. Em compensação, você paga em complexidade de depuração, em risco de cascatas obscuras de gatilhos e na já mencionada perda de portabilidade. Como em quase tudo em banco de dados, a decisão é um equilíbrio, e a banca gosta de cobrar justamente a consciência desse equilíbrio.
Síntese para a prova. Grave a distinção central: gatilhos são reativos e disparam sozinhos em resposta a INSERT/UPDATE/DELETE; funções e procedimentos são invocados explicitamente. Saiba escolher o momento do gatilho — BEFORE para validar ou alterar NEW antes de gravar, AFTER para auditar ou propagar depois de gravado, INSTEAD OF para tornar visões atualizáveis — e o nível — FOR EACH ROW com acesso a OLD/NEW, ou FOR EACH STATEMENT uma vez por comando. No PostgreSQL, lembre que o gatilho exige uma função de gatilho separada que retorna trigger, e que retornar NULL num BEFORE FOR EACH ROW cancela a operação. Para funções, domine os tipos (escalar, de tabela, agregada) e a volatilidade (IMMUTABLE, STABLE, VOLATILE). A diferença decisiva entre função e procedimento é que o procedimento, chamado com CALL, pode controlar transações com COMMIT/ROLLBACK no corpo, enquanto a função roda dentro da transação de quem a chama e sempre retorna um valor. Tudo isso vive dentro da transação corrente, então um erro em qualquer ponto reverte o conjunto — e essa atomicidade é o que torna esses recursos confiáveis para integridade, auditoria e automação.