PostgreSQL: Triggers e Triggers de Evento no PostgreSQL
Última atualização: 2026-08-26
1. O Que Você Vai Aprender
- CREATE TRIGGER (BEFORE / AFTER / INSTEAD OF)
- Triggers de linha vs triggers de instrução
- Variáveis de registro NEW / OLD
- Triggers condicionais (cláusula WHEN)
- Ordem de execução dos triggers (alfabética por nome)
- Triggers INSTEAD OF (em views)
- Triggers de evento (eventos DDL: CREATE/ALTER/DROP TABLE)
- Habilitar/desabilitar triggers
- Triggers vs lógica de aplicação
2. A História
Alice é uma arquiteta de banco de dados em uma plataforma de e-commerce. Ela precisa implementar duas lógicas centrais de automação:
- Dedução automática de estoque: quando um novo pedido é inserido em
order_items, um trigger BEFORE INSERT deduz automaticamenteproducts.stock_qtye rejeita a inserção se o estoque for insuficiente. - Log de auditoria de preço: quando o preço de um produto é atualizado, um trigger AFTER UPDATE registra os preços antigo e novo na tabela
price_audit_log.
Alice escolhe implementar isso com triggers, garantindo que as regras de negócio sejam aplicadas consistentemente, independentemente de qual aplicação grava os dados.
3. Conceito: Fundamentos de Triggers
(1) Visão Geral dos Tipos de Trigger
| Momento | Nível de linha (FOR EACH ROW) | Nível de instrução (FOR EACH STATEMENT) |
|---|---|---|
| BEFORE | Pode modificar NEW, rejeitar operação | Sem NEW/OLD, pode validar/preparar |
| AFTER | Pode ler NEW/OLD, registrar | Bom para estatísticas de resumo |
| INSTEAD OF | Apenas views, substitui a operação original | Não aplicável ao nível de instrução |
▶ Exemplo: Criar um Trigger Básico BEFORE INSERT de Linha
CREATE OR REPLACE FUNCTION fn_before_order_item()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.created_at := NOW();
NEW.line_total := NEW.quantity * NEW.unit_price;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_before_order_item
BEFORE INSERT ON order_items
FOR EACH ROW
EXECUTE FUNCTION fn_before_order_item();
Output:
INSERT 0 1
▶ Exemplo: Trigger de Auditoria AFTER UPDATE
CREATE OR REPLACE FUNCTION fn_audit_price_change()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.unit_price IS DISTINCT FROM OLD.unit_price THEN
INSERT INTO price_audit_log
(product_id, old_price, new_price, changed_by, changed_at)
VALUES
(NEW.product_id, OLD.unit_price, NEW.unit_price,
CURRENT_USER, NOW());
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_audit_price
AFTER UPDATE OF unit_price ON products
FOR EACH ROW
EXECUTE FUNCTION fn_audit_price_change();
Output:
INSERT 0 1
(2) Variáveis NEW e OLD
| Momento | NEW | OLD | Modificável |
|---|---|---|---|
| BEFORE INSERT | Sim (linha a ser inserida) | Não | Pode modificar NEW |
| BEFORE UPDATE | Sim (novo valor) | Sim (valor antigo) | Pode modificar NEW |
| BEFORE DELETE | Não | Sim (linha a ser excluída) | Não modificável |
| AFTER INSERT | Sim (somente leitura) | Não | Não modificável |
| AFTER UPDATE | Sim (somente leitura) | Sim (somente leitura) | Não modificável |
▶ Exemplo: BEFORE UPDATE Modifica NEW
CREATE OR REPLACE FUNCTION fn_auto_update_timestamp()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at := NOW();
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_products_updated_at
BEFORE UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION fn_auto_update_timestamp();
Output:
CREATE TABLE
4. Conceito: Triggers de Linha vs Triggers de Instrução
(1) Diferenças de Frequência de Execução
| Dimensão | FOR EACH ROW | FOR EACH STATEMENT |
|---|---|---|
| Execuções | Uma vez por linha afetada | Uma vez por instrução SQL |
| NEW/OLD | Disponível | Não disponível |
| Impacto no desempenho | Escala com linhas afetadas | Sobrecarga fixa |
| Uso típico | Validação, cálculo de coluna, cascata | Estatísticas de resumo, atualização de cache |
▶ Exemplo: Trigger de Instrução Atualiza Resumo
CREATE OR REPLACE FUNCTION fn_refresh_order_stats()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_order_stats;
RETURN NULL;
END;
$$;
CREATE TRIGGER trg_refresh_order_stats
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH STATEMENT
EXECUTE FUNCTION fn_refresh_order_stats();
Output:
INSERT 0 1
▶ Exemplo: Trigger de Linha Valida Estoque
CREATE OR REPLACE FUNCTION fn_check_stock()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_stock INT;
BEGIN
SELECT stock_qty INTO v_stock
FROM products
WHERE product_id = NEW.product_id;
IF v_stock < NEW.quantity THEN
RAISE EXCEPTION 'Estoque insuficiente: produto % tem % unidades, solicitado %',
NEW.product_id, v_stock, NEW.quantity;
END IF;
UPDATE products
SET stock_qty = stock_qty - NEW.quantity
WHERE product_id = NEW.product_id;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_check_stock
BEFORE INSERT ON order_items
FOR EACH ROW
EXECUTE FUNCTION fn_check_stock();
Output:
INSERT 0 1
5. Conceito: Triggers Condicionais e Ordem de Execução
(1) Cláusula WHEN
A cláusula WHEN faz um trigger executar apenas quando uma condição é atendida, reduzindo a sobrecarga de invocação desnecessária.
▶ Exemplo: Trigger Condicional com WHEN
CREATE OR REPLACE FUNCTION fn_log_big_order()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO big_order_log (order_id, total_amount, created_at)
VALUES (NEW.order_id, NEW.total_amount, NOW());
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_log_big_order
AFTER INSERT ON orders
FOR EACH ROW
WHEN (NEW.total_amount >= 5000)
EXECUTE FUNCTION fn_log_big_order();
Output:
INSERT 0 1
(2) Regras de Ordem de Execução dos Triggers
| Regra | Descrição |
|---|---|
| BEFORE antes de AFTER | Todos os BEFORE executam antes de qualquer AFTER |
| Mesmo momento, ordem alfabética por nome | trg_a executa antes de trg_b |
| INSTEAD OF substitui a operação original | Apenas views, não executa o INSERT/UPDATE/DELETE original |
| BEFORE retornando NULL bloqueia a operação | Para UPDATE/INSERT, retornar NULL pula essa linha |
▶ Exemplo: Ordem de Execução de Múltiplos Triggers
-- Estes triggers executam em ordem alfabética: a -> b -> c
CREATE TRIGGER trg_a_validate
BEFORE INSERT ON orders FOR EACH ROW
EXECUTE FUNCTION fn_validate_order();
CREATE TRIGGER trg_b_calc_tax
BEFORE INSERT ON orders FOR EACH ROW
EXECUTE FUNCTION fn_calc_order_tax();
CREATE TRIGGER trg_c_notify
AFTER INSERT ON orders FOR EACH ROW
EXECUTE FUNCTION fn_notify_new_order();
Output:
INSERT 0 1
6. Conceito: Triggers INSTEAD OF
(1) INSTEAD OF em Views
Uma view por si só não suporta INSERT/UPDATE/DELETE direto; um trigger INSTEAD OF intercepta a operação e personaliza a lógica de execução.
| Propriedade | Descrição |
|---|---|
| Apenas views | Não pode usar INSTEAD OF em tabelas |
| Deve ser FOR EACH ROW | Nível de instrução não suportado |
| Substitui a operação original | O comportamento padrão não é executado |
| Bom para views atualizáveis | Mapeia escritas da view para tabelas base |
▶ Exemplo: INSTEAD OF INSERT em uma View
CREATE VIEW vw_customer_orders AS
SELECT
c.customer_id,
c.first_name,
c.last_name,
o.order_id,
o.total_amount
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id;
CREATE OR REPLACE FUNCTION fn_insert_customer_order()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO customers (first_name, last_name)
VALUES (NEW.first_name, NEW.last_name)
ON CONFLICT DO NOTHING;
INSERT INTO orders (customer_id, total_amount, order_date)
SELECT customer_id, NEW.total_amount, CURRENT_DATE
FROM customers
WHERE first_name = NEW.first_name AND last_name = NEW.last_name;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_insert_customer_order
INSTEAD OF INSERT ON vw_customer_orders
FOR EACH ROW
EXECUTE FUNCTION fn_insert_customer_order();
Output:
INSERT 0 1
▶ Exemplo: INSTEAD OF UPDATE em uma View
CREATE OR REPLACE FUNCTION fn_update_customer_order()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE customers
SET first_name = NEW.first_name,
last_name = NEW.last_name
WHERE customer_id = NEW.customer_id;
UPDATE orders
SET total_amount = NEW.total_amount
WHERE order_id = NEW.order_id;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_update_customer_order
INSTEAD OF UPDATE ON vw_customer_orders
FOR EACH ROW
EXECUTE FUNCTION fn_update_customer_order();
Output:
CREATE TABLE
7. Conceito: Triggers de Evento
(1) Triggers de Evento DDL (Especialidade do PG)
Triggers de evento disparam em comandos DDL (CREATE/ALTER/DROP), independentes de qualquer tabela específica.
| Evento | Momento |
|---|---|
ddl_command_start |
Antes da execução do DDL |
ddl_command_end |
Depois da execução do DDL |
sql_drop |
Antes da execução do comando DROP |
table_rewrite |
Antes da reescrita da tabela (ex.: ALTER TYPE) |
▶ Exemplo: Trigger de Evento que Proíbe DROP TABLE
CREATE OR REPLACE FUNCTION fn_block_drop_table()
RETURNS EVENT_TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
RAISE EXCEPTION 'DROP TABLE não é permitido em produção!';
END;
$$;
CREATE EVENT TRIGGER etg_block_drop
ON sql_drop
WHEN tag IN ('DROP TABLE')
EXECUTE FUNCTION fn_block_drop_table();
Output:
CREATE TABLE
▶ Exemplo: Log de Auditoria de DDL
CREATE TABLE ddl_audit_log (
id SERIAL PRIMARY KEY,
event_type TEXT,
tag TEXT,
object_type TEXT,
object_name TEXT,
command_text TEXT,
current_user TEXT,
event_time TIMESTAMP DEFAULT NOW()
);
CREATE OR REPLACE FUNCTION fn_log_ddl()
RETURNS EVENT_TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_obj RECORD;
BEGIN
v_obj := NULL;
INSERT INTO ddl_audit_log (event_type, tag, object_type, object_name, current_user)
VALUES (TG_EVENT, TG_TAG,
v_obj.object_type, v_obj.object_identity,
CURRENT_USER);
RAISE NOTICE 'DDL registrado: % %', TG_EVENT, TG_TAG;
END;
$$;
CREATE EVENT TRIGGER etg_log_ddl
ON ddl_command_end
EXECUTE FUNCTION fn_log_ddl();
Output:
INSERT 0 1
(2) Triggers de Evento vs Triggers de Tabela
| Dimensão | Trigger de tabela | Trigger de evento |
|---|---|---|
| Objeto vinculado | Tabela/view específica | Eventos DDL globais |
| DML/DDL | DML (INSERT/UPDATE/DELETE) | DDL (CREATE/ALTER/DROP) |
| NEW/OLD | Disponível | Nenhum |
| TG_TAG | Nenhum | Sim (rótulo como DROP TABLE) |
| Uso típico | Validação, auditoria, cascata | Auditoria DDL, controle de segurança |
8. Conceito: Habilitar/Desabilitar e Triggers vs Lógica de Aplicação
(1) Habilitar/Desabilitar Triggers
| Comando | Efeito |
|---|---|
ALTER TABLE t DISABLE TRIGGER nome_trg; |
Desabilitar um trigger específico |
ALTER TABLE t ENABLE TRIGGER nome_trg; |
Habilitar um trigger específico |
ALTER TABLE t DISABLE TRIGGER ALL; |
Desabilitar todos os triggers |
ALTER TABLE t ENABLE TRIGGER ALL; |
Habilitar todos os triggers |
▶ Exemplo: Desabilitar Triggers para Importação em Massa
-- Desabilitar triggers para desempenho na importação em massa
ALTER TABLE products DISABLE TRIGGER ALL;
COPY products(product_name, unit_price, category, stock_qty)
FROM '/data/products_bulk.csv' WITH (FORMAT csv, HEADER true);
-- Reabilitar após a importação
ALTER TABLE products ENABLE TRIGGER ALL;
Output:
-- instrução SQL executada com sucesso
(2) Triggers vs Lógica de Aplicação
| Dimensão | Trigger | Lógica de aplicação |
|---|---|---|
| Consistência | Dispara para qualquer caminho de escrita | Depende da aplicação honrar as regras |
| Dificuldade de depuração | Implícito, difícil de rastrear | Chamada explícita, fácil de depurar |
| Desempenho | Sobrecarga extra por linha | Pode ser processado em lote/otimizado |
| Portabilidade | Sintaxe específica do PG | Linguagem de propósito geral |
| Caso de uso | Impor invariantes, auditoria, cascata | Fluxos complexos, entre sistemas |
▶ Exemplo: Trigger Impõe Consistência de Dados
-- Impor: total do pedido deve sempre igualar a soma dos itens
CREATE OR REPLACE FUNCTION fn_enforce_order_total()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_calculated NUMERIC;
BEGIN
SELECT COALESCE(SUM(line_total), 0) INTO v_calculated
FROM order_items
WHERE order_id = NEW.order_id;
IF v_calculated IS DISTINCT FROM (
SELECT total_amount FROM orders WHERE order_id = NEW.order_id
) THEN
UPDATE orders SET total_amount = v_calculated
WHERE order_id = NEW.order_id;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_enforce_order_total
AFTER INSERT OR UPDATE ON order_items
FOR EACH ROW
EXECUTE FUNCTION fn_enforce_order_total();
Output:
result
----------
42.50
(1 row)
9. Fluxograma: Decisão de Escolha de Trigger
flowchart TD
A[Precisa de lógica de automação?] --> B{DML ou DDL?}
B -->|DDL| C[Trigger de evento<br/>EVENT TRIGGER]
B -->|DML| D{Tabela ou view?}
D -->|View| E[Trigger INSTEAD OF<br/>FOR EACH ROW]
D -->|Tabela| F{Modificar dados antes da operação?}
F -->|Sim| G[Trigger BEFORE<br/>pode modificar NEW]
F -->|Não| H{Precisa de log/cascata?}
H -->|Sim| I[Trigger AFTER<br/>pode ler NEW/OLD]
H -->|Não| J[Nenhum trigger necessário]
G --> K{Por linha ou por SQL?}
I --> K
K -->|Por linha| L[FOR EACH ROW]
K -->|Por SQL| M[FOR EACH STATEMENT]
L --> N{Precisa de condição?}
N -->|Sim| O[Cláusula WHEN]
N -->|Não| P[Sem condição]
style C fill:#fff9c4
style E fill:#e1bee7
style G fill:#c8e6c9
style I fill:#bbdefb
10. Exemplo Completo
O esquema de triggers de e-commerce de Alice—dedução de estoque + auditoria de preço + atualização automática de timestamp:
-- 1. Atualização automática de timestamp em alterações de produto
CREATE OR REPLACE FUNCTION fn_products_timestamp()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at := NOW();
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_products_timestamp
BEFORE UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION fn_products_timestamp();
-- 2. Diminuir estoque ao inserir item de pedido, rejeitar se insuficiente
CREATE OR REPLACE FUNCTION fn_decrease_stock()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_stock INT;
BEGIN
SELECT stock_qty INTO v_stock FROM products
WHERE product_id = NEW.product_id FOR UPDATE;
IF v_stock IS NULL THEN
RAISE EXCEPTION 'Produto % não encontrado', NEW.product_id;
ELSIF v_stock < NEW.quantity THEN
RAISE EXCEPTION 'Estoque insuficiente: produto % (estoque=%, solicitado=%)',
NEW.product_id, v_stock, NEW.quantity;
END IF;
UPDATE products SET stock_qty = stock_qty - NEW.quantity
WHERE product_id = NEW.product_id;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_decrease_stock
BEFORE INSERT ON order_items
FOR EACH ROW
EXECUTE FUNCTION fn_decrease_stock();
-- 3. Log de auditoria quando o preço muda
CREATE OR REPLACE FUNCTION fn_price_audit()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.unit_price IS DISTINCT FROM OLD.unit_price THEN
INSERT INTO price_audit_log
(product_id, old_price, new_price, changed_by, changed_at)
VALUES
(NEW.product_id, OLD.unit_price, NEW.unit_price,
CURRENT_USER, NOW());
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_price_audit
AFTER UPDATE OF unit_price ON products
FOR EACH ROW
WHEN (OLD.unit_price IS DISTINCT FROM NEW.unit_price)
EXECUTE FUNCTION fn_price_audit();
❓ Perguntas Frequentes
P: O que acontece se um trigger BEFORE retornar NULL? R: Para INSERT/UPDATE, retornar NULL significa pular a operação daquela linha (sem escrita real e sem execução de triggers subsequentes). Não tem efeito para DELETE. Note que o valor de retorno de um trigger AFTER é ignorado.
P: Se uma tabela tem múltiplos triggers no mesmo momento, como a ordem é decidida? R: O PostgreSQL os executa em ordem alfabética por nome do trigger—ex.:
trg_aantes detrg_b. Todos os triggers BEFORE executam antes de qualquer trigger AFTER.
P: Um trigger pode executar COMMIT? R: Um trigger de tabela normal não pode executar COMMIT/ROLLBACK (ele executa dentro de uma transação). Para transações autônomas, use dblink dentro da função do trigger ou a abordagem procedural do PG 14+.
P: Uma condição da cláusula WHEN pode referenciar outras tabelas? R: Não. Uma condição WHEN só pode referenciar colunas NEW/OLD—sem subconsultas ou referências a outras tabelas. Para condições complexas, verifique dentro do corpo da função do trigger.
P: Um trigger INSTEAD OF pode usar FOR EACH STATEMENT? R: Não. Um trigger INSTEAD OF deve ser FOR EACH ROW, porque precisa decidir por linha como mapear para operações da tabela base.
P: O que é TG_TAG em um trigger de evento? R: TG_TAG é o rótulo do comando DDL que disparou o evento, ex.:
CREATE TABLE,ALTER TABLE,DROP TABLE. Você pode filtrar com WHEN tag IN (...).
P: A importação COPY é mais rápida após desabilitar triggers? R: Sim. Desabilitar triggers de linha durante grandes importações de dados melhora significativamente o desempenho, mas após reabilitar você deve tratar manualmente a lógica que os triggers teriam executado (ex.: colunas calculadas, registros de auditoria).
P: Como evitar chamadas recursivas de trigger? R: O trigger A atualizando a tabela T pode disparar A novamente. Evite com: uma condição WHEN, uma variável de estado (ex.: variável de nível de pacote) ou mudando para um trigger AFTER com verificação condicional.
📖 Resumo
- Momento: BEFORE (pode modificar NEW), AFTER (log), INSTEAD OF (mapeamento de view)
- Triggers de linha executam por linha; triggers de instrução executam uma vez por instrução SQL
- NEW contém o novo valor, OLD contém o valor antigo; NEW é modificável na fase BEFORE
- A cláusula WHEN filtra condições, reduzindo invocações desnecessárias de triggers
- Triggers no mesmo momento executam em ordem alfabética de nome
- INSTEAD OF é apenas para views e deve ser FOR EACH ROW
- Triggers de evento (especialidade do PG) escutam comandos DDL—bons para auditoria e controle de segurança
- DISABLE/ENABLE TRIGGER controla o estado do trigger; desabilitar durante importação em massa acelera
- Triggers garantem consistência de dados mas adicionam complexidade de depuração—avalie cuidadosamente
📝 Exercícios
-
⭐ Escreva uma função de trigger BEFORE UPDATE e um trigger que, quando a coluna
emailda tabelacustomersfor modificada, defina automaticamenteupdated_atcomoNOW(). -
⭐⭐ Escreva um trigger AFTER INSERT que, quando um novo pedido for adicionado à tabela
orderscomtotal_amount >= 1000USD, insira automaticamente na tabelahigh_value_order_log(registrando order_id, total_amount, customer_id, created_at). -
⭐⭐⭐ Crie uma view
vw_product_sales(unindo products + order_items para calcular vendas) e escreva um trigger INSTEAD OF UPDATE que mapeie umtotal_soldmodificado na view de volta para atualizarproducts.stock_qty. Em seguida, escreva um trigger de evento que registre todas as operações DDLCREATE TABLEeDROP TABLEna tabeladdl_audit_log.