PostgreSQL: Triggers e Triggers de Evento no PostgreSQL

Última atualização: 2026-08-26

1. O Que Você Vai Aprender


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:

  1. Dedução automática de estoque: quando um novo pedido é inserido em order_items, um trigger BEFORE INSERT deduz automaticamente products.stock_qty e rejeita a inserção se o estoque for insuficiente.
  2. 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

SQL
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:

TEXT 📖 Somente leitura
INSERT 0 1

▶ Exemplo: Trigger de Auditoria AFTER UPDATE

SQL
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:

TEXT 📖 Somente leitura
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

SQL
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:

TEXT 📖 Somente leitura
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

SQL
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:

TEXT 📖 Somente leitura
INSERT 0 1

▶ Exemplo: Trigger de Linha Valida Estoque

SQL
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:

TEXT 📖 Somente leitura
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

SQL
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:

TEXT 📖 Somente leitura
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

SQL
-- 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:

TEXT 📖 Somente leitura
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

SQL
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:

TEXT 📖 Somente leitura
INSERT 0 1

▶ Exemplo: INSTEAD OF UPDATE em uma View

SQL
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:

TEXT 📖 Somente leitura
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

SQL
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:

TEXT 📖 Somente leitura
CREATE TABLE

▶ Exemplo: Log de Auditoria de DDL

SQL
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:

TEXT 📖 Somente leitura
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

SQL
-- 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:

TEXT 📖 Somente leitura
-- 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

SQL
-- 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:

TEXT 📖 Somente leitura
  result  
----------
   42.50
(1 row)

9. Fluxograma: Decisão de Escolha de Trigger

100%
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:

SQL
-- 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_a antes de trg_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


📝 Exercícios

  1. ⭐ Escreva uma função de trigger BEFORE UPDATE e um trigger que, quando a coluna email da tabela customers for modificada, defina automaticamente updated_at como NOW().

  2. ⭐⭐ Escreva um trigger AFTER INSERT que, quando um novo pedido for adicionado à tabela orders com total_amount >= 1000 USD, insira automaticamente na tabela high_value_order_log (registrando order_id, total_amount, customer_id, created_at).

  3. ⭐⭐⭐ Crie uma view vw_product_sales (unindo products + order_items para calcular vendas) e escreva um trigger INSTEAD OF UPDATE que mapeie um total_sold modificado na view de volta para atualizar products.stock_qty. Em seguida, escreva um trigger de evento que registre todas as operações DDL CREATE TABLE e DROP TABLE na tabela ddl_audit_log.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%