Aula 17: Triggers no PostgreSQL — automação de regras de negócio
Triggers no PostgreSQL representam um dos mecanismos mais poderosos para automatizar regras de negócio diretamente no banco de dados. Em vez de depender exclusivamente da camada de aplicação para validar, auditar ou transformar dados, os triggers permitem que você defina comportamentos automáticos que são disparados sempre que determinados eventos ocorrem em uma tabela — como inserções, atualizações ou exclusões. Esta aula é um mergulho profundo nesse recurso, cobrindo desde a sintaxe fundamental até cenários avançados de uso em produção.
Ao longo dos últimos anos, em nossos projetos na JRT Technology Solutions, implementamos centenas de triggers para resolver problemas que iam desde simples auditorias de alterações até complexas validações cruzadas entre tabelas. O que aprendemos na prática é que dominar Triggers no PostgreSQL não é apenas saber escrever a função gatilho — é entender o ciclo de vida da execução, as variáveis de contexto como NEW e OLD, as diferenças entre triggers BEFORE e AFTER, e principalmente como depurar e otimizar essas estruturas para que não se tornem gargalos de performance.
Nesta aula, você vai colocar a mão no código. Vamos criar triggers reais, testá-los em um ambiente controlado, verificar o comportamento com consultas de diagnóstico e ainda analisar os erros mais comuns que profissionais enfrentam no dia a dia. Se você já concluiu as aulas anteriores do curso, já possui uma base sólida em funções PL/pgSQL, estrutura de tabelas e índices — agora é o momento de levar essa automação para o próximo nível, incorporando lógica reativa diretamente ao seu modelo de dados.
Ao final desta aula, você terá plena capacidade de: projetar triggers que reforçam a integridade dos dados sem depender de código externo; auditar automaticamente alterações em tabelas críticas; impedir operações inválidas com mensagens de erro personalizadas; e monitorar a execução dos seus triggers usando as visões de catálogo do PostgreSQL. Tudo isso seguindo um passo a passo detalhado, com cada comando explicado linha por linha, em um formato que você pode replicar imediatamente no seu ambiente.
O que você vai aprender nesta aula
- Compreender o conceito de Triggers no PostgreSQL e sua função na automação de regras de negócio
- Diferenciar triggers BEFORE, AFTER e INSTEAD OF, entendendo quando utilizar cada tipo
- Criar funções gatilho em PL/pgSQL que manipulam as variáveis NEW e OLD
- Implementar triggers para auditoria (log de alterações), validação de dados e manutenção de colunas derivadas
- Configurar triggers condicionais com a cláusula WHEN para filtrar eventos específicos
- Gerenciar triggers — listar, desabilitar, habilitar e removê-los com comandos do catálogo
- Diagnosticar e corrigir os erros mais comuns usando as visões pg_trigger e information_schema.triggers
- Aplicar boas práticas de performance e organização de código em ambientes de produção
Pré-requisitos e Ambiente
Para executar todos os exemplos desta aula sem interrupções, você precisa ter um servidor PostgreSQL em funcionamento. As versões testadas e compatíveis são a PostgreSQL 14, 15 e 16 — as três versões estáveis mais recentes até julho de 2026. O banco de dados que utilizaremos se chama db_triggers_demo, e vamos criá-lo do zero durante a aula. Você precisará de um usuário com privilégios de CREATEDB (ou o superusuário postgres) para criar o banco e, dentro dele, permissões para criar funções e triggers. Se você seguiu as aulas anteriores do curso, já possui o PostgreSQL instalado e configurado; caso contrário, revise a Aula 1 (instalação no Ubuntu/Debian e CentOS/RHEL/Rocky Linux) e a Aula 14 (funções PL/pgSQL).
Todo o código desta aula foi projetado para ser executado no psql, o cliente de linha de comando do PostgreSQL, pois ele oferece feedback imediato e é o ambiente onde a maioria dos DBAs trabalha. Se você preferir usar uma interface gráfica como pgAdmin ou DBeaver, os comandos SQL são exatamente os mesmos — apenas a forma de envio muda. Durante os exemplos, utilizaremos as variáveis de ambiente padrão do PostgreSQL (PGHOST, PGPORT, PGUSER) ou passaremos as credenciais explicitamente via psql -U usuario -d banco.
Antes de começar, certifique-se de que a extensão plpgsql está disponível. Ela vem habilitada por padrão em todas as instalações modernas do PostgreSQL, mas vamos verificá-la explicitamente. Abra um terminal e execute os comandos iniciais de preparação que mostraremos na próxima seção. O ambiente está pronto? Então vamos mergulhar nos fundamentos teóricos que sustentam o funcionamento dos triggers.
Fundamentos Teóricos: O que são Triggers no PostgreSQL e como funcionam
Um trigger no PostgreSQL é um mecanismo que associa uma função previamente definida a um evento específico em uma tabela ou visão. Quando o evento ocorre — por exemplo, uma linha é inserida na tabela clientes — o PostgreSQL automaticamente executa a função associada, passando informações de contexto sobre a operação. Essa arquitetura em duas partes (função + trigger) é uma decisão de design importante: a função contém a lógica, e o trigger define quando e em quais condições essa lógica será disparada. Isso permite reutilizar a mesma função em múltiplos triggers e facilita a manutenção do código.
Existem três momentos principais em que um trigger pode ser disparado: BEFORE (antes da operação ser concluída), AFTER (após a operação ser concluída) e INSTEAD OF (no lugar da operação, exclusivo para visões). Um trigger BEFORE é ideal para validar ou modificar os dados que estão prestes a ser gravados — você pode alterar o valor de colunas na variável NEW antes que cheguem ao disco. Já um trigger AFTER é perfeito para tarefas de auditoria, replicação ou atualização de cache, pois nesse momento a operação já foi confirmada e você tem acesso tanto aos valores antigos (OLD) quanto aos novos (NEW). Triggers INSTEAD OF são usados em visões complexas onde o PostgreSQL não consegue determinar automaticamente como traduzir uma operação DML em alterações nas tabelas subjacentes.
Um aspecto frequentemente subestimado é o entendimento das variáveis de registro NEW e OLD. Em um trigger de INSERT, apenas NEW está disponível (contendo a linha que será inserida). Em DELETE, apenas OLD está disponível (contendo a linha que será removida). Em UPDATE, ambas estão disponíveis: OLD contém os valores antes da modificação, e NEW contém os valores após a modificação. Essa distinção é crucial: se você tentar acessar OLD em um trigger de INSERT, receberá um erro de variável não inicializada. Nossos especialistas utilizam diariamente essa distinção para construir lógicas condicionais que reagem de forma diferente conforme o tipo de operação.
Outro conceito fundamental é o de gatilho por linha (FOR EACH ROW) versus gatilho por instrução (FOR EACH STATEMENT). Um trigger FOR EACH ROW é executado uma vez para cada linha afetada pela operação DML — se um UPDATE modificar 500 linhas, o trigger dispara 500 vezes. Já um trigger FOR EACH STATEMENT dispara uma única vez por instrução SQL, independentemente do número de linhas afetadas. O primeiro é o padrão e o mais comum para regras de negócio que dependem dos valores individuais de cada linha; o segundo é útil para operações de manutenção globais, como registrar que uma determinada tabela foi modificada. Em ambientes de alto volume, a escolha entre eles tem impacto direto na performance, e vamos discutir isso em detalhes na seção de boas práticas.
| Tipo | Momento de Execução | Acesso a NEW/OLD | Uso Típico | Pode Cancelar Operação? |
|---|---|---|---|---|
| BEFORE | Antes da operação | NEW (INSERT/UPDATE), OLD (UPDATE/DELETE) | Validação, normalização, modificação de dados | Sim (via RAISE EXCEPTION ou RETURN NULL) |
| AFTER | Após a operação | NEW (INSERT/UPDATE), OLD (UPDATE/DELETE) | Auditoria, replicação, notificações | Não (a operação já foi confirmada) |
| INSTEAD OF | No lugar da operação | NEW (INSERT/UPDATE), OLD (UPDATE/DELETE) | Visões complexas, tabelas externas | Sim (substitui completamente a operação) |
O PostgreSQL também suporta triggers condicionais através da cláusula WHEN. Com ela, você pode especificar uma condição booleana que filtra exatamente quais linhas disparam o trigger. Por exemplo, você pode auditar apenas alterações onde o salário ultrapassa um determinado valor, ou onde uma coluna específica foi modificada (usando OLD.coluna IS DISTINCT FROM NEW.coluna). Isso reduz drasticamente a sobrecarga em operações de grande volume, pois a função gatilho só é invocada quando a condição é satisfeita — as demais linhas passam diretamente sem qualquer execução adicional.
Por fim, é essencial entender que triggers operam dentro da mesma transação da operação que os disparou. Se um trigger AFTER levantar uma exceção, toda a transação é revertida, incluindo a operação original. Isso garante a atomicidade das regras de negócio — ou tudo é aplicado consistentemente, ou nada é alterado. Essa característica é uma faca de dois gumes: por um lado, protege a integridade; por outro, pode causar rollbacks inesperados se o trigger não for cuidadosamente testado. Na JRT Technology Solutions, sempre recomendamos testes exaustivos em ambiente de homologação antes de implantar triggers em produção.
Criando o Ambiente de Testes: Banco, Tabelas e Dados Iniciais
Vamos construir nosso laboratório do zero. O primeiro passo é criar o banco de dados que utilizaremos em todos os exemplos desta aula. Conecte-se ao PostgreSQL com um usuário que tenha privilégios de criação de banco (ou use o superusuário postgres) e execute os comandos a seguir. Cada linha está comentada para que você entenda exatamente o que está acontecendo.
-- 1. Criação do banco de dados db_triggers_demo
-- O encoding UTF8 garante suporte a caracteres acentuados (português)
-- LC_COLLATE e LC_CTYPE 'pt_BR.UTF-8' configuram ordenação para português do Brasil
-- Se seu sistema não tiver locale pt_BR disponível, use 'en_US.UTF-8' ou 'C'
CREATE DATABASE db_triggers_demo
ENCODING 'UTF8'
LC_COLLATE 'pt_BR.UTF-8'
LC_CTYPE 'pt_BR.UTF-8'
TEMPLATE template0;
-- 2. Conectar ao novo banco
-- No psql: \c db_triggers_demo
-- Ou pela linha de comando: psql -U postgres -d db_triggers_demo
Após criar o banco, conecte-se a ele e vamos criar as tabelas que servirão de base para os nossos triggers. Teremos uma tabela principal chamada funcionarios (que será o alvo dos nossos triggers) e uma tabela auxiliar audit_funcionarios (que armazenará o histórico de alterações). Além disso, criaremos uma tabela departamentos para demonstrar triggers com validação entre tabelas.
-- 3. Criação da tabela departamentos (tabela de referência)
CREATE TABLE departamentos (
id_depto SERIAL PRIMARY KEY,
nome_depto VARCHAR(100) NOT NULL UNIQUE,
orcamento NUMERIC(12,2) NOT NULL CHECK (orcamento > 0),
data_criacao TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- 4. Inserção de dados iniciais nos departamentos
INSERT INTO departamentos (nome_depto, orcamento) VALUES
('Tecnologia', 1500000.00),
('Financeiro', 800000.00),
('Recursos Humanos', 450000.00),
('Marketing', 620000.00),
('Operações', 1100000.00);
-- 5. Criação da tabela funcionarios (tabela principal)
CREATE TABLE funcionarios (
id_func SERIAL PRIMARY KEY,
nome VARCHAR(150) NOT NULL,
email VARCHAR(200) UNIQUE,
salario NUMERIC(10,2) NOT NULL CHECK (salario >= 1412.00),
bonus NUMERIC(10,2) DEFAULT 0.00,
salario_total NUMERIC(10,2) GENERATED ALWAYS AS (salario + bonus) STORED,
id_depto INTEGER REFERENCES departamentos(id_depto) ON DELETE SET NULL,
data_admissao DATE DEFAULT CURRENT_DATE,
ativo BOOLEAN DEFAULT TRUE,
ultima_modif TIMESTAMPTZ
);
-- 6. Criação da tabela de auditoria (armazenará histórico de alterações)
CREATE TABLE audit_funcionarios (
id_audit SERIAL PRIMARY KEY,
operacao CHAR(6) NOT NULL, -- 'INSERT', 'UPDATE', 'DELETE'
id_func INTEGER,
nome_old VARCHAR(150),
nome_new VARCHAR(150),
salario_old NUMERIC(10,2),
salario_new NUMERIC(10,2),
id_depto_old INTEGER,
id_depto_new INTEGER,
usuario_bd VARCHAR(100), -- CURRENT_USER
data_operacao TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- 7. Inserção de funcionários iniciais
INSERT INTO funcionarios (nome, email, salario, id_depto) VALUES
('Ana Silva', 'ana.silva@empresa.com', 8500.00, 1),
('Bruno Costa', 'bruno.costa@empresa.com', 6200.00, 2),
('Carla Mendes', 'carla.mendes@empresa.com', 7200.00, 1),
('Daniel Oliveira', 'daniel.oliveira@empresa.com', 4800.00, 3),
('Elena Santos', 'elena.santos@empresa.com', 9100.00, 4);
Agora temos um ambiente completo para trabalhar. A tabela funcionarios possui uma coluna gerada automaticamente (salario_total) e uma coluna ultima_modif que ainda não está sendo atualizada automaticamente — perfeita para demonstrar nosso primeiro trigger. A tabela audit_funcionarios está vazia, pronta para receber os registros de auditoria que nossos triggers vão gerar. Nos próximos passos, você verá como fazer tudo isso funcionar de forma integrada.
Vamos executar uma consulta rápida para verificar se os dados foram inseridos corretamente e se a coluna gerada salario_total está funcionando como esperado (ela deve refletir salario + bonus, atualmente com bonus zerado).
-- 8. Verificação dos dados iniciais
SELECT id_func, nome, salario, bonus, salario_total, nome_depto
FROM funcionarios f
LEFT JOIN departamentos d ON f.id_depto = d.id_depto
ORDER BY id_func;
id_func | nome | salario | bonus | salario_total | nome_depto
---------+-------------------+---------+-------+---------------+------------------
1 | Ana Silva | 8500.00 | 0.00 | 8500.00 | Tecnologia
2 | Bruno Costa | 6200.00 | 0.00 | 6200.00 | Financeiro
3 | Carla Mendes | 7200.00 | 0.00 | 7200.00 | Tecnologia
4 | Daniel Oliveira | 4800.00 | 0.00 | 4800.00 | Recursos Humanos
5 | Elena Santos | 9100.00 | 0.00 | 9100.00 | Marketing
(5 rows)
Primeiro Trigger: Atualização Automática de Timestamp
Nosso primeiro trigger será um clássico da administração de bancos de dados: atualizar automaticamente a coluna ultima_modif sempre que uma linha da tabela funcionarios for modificada. Embora pareça simples, este caso de uso introduz a estrutura básica que todo trigger no PostgreSQL segue: uma função gatilho em PL/pgSQL e o comando CREATE TRIGGER que a vincula à tabela. Vamos criar a função primeiro, depois o trigger, e em seguida testar com operações reais de UPDATE.
A função gatilho tem uma assinatura especial: ela deve retornar o tipo TRIGGER (não VOID, não INTEGER — exclusivamente TRIGGER). Dentro dela, utilizamos as variáveis especiais NEW (registro que contém a nova linha) e TG_OP (uma string que indica a operação: ‘INSERT’, ‘UPDATE’, ‘DELETE’ ou ‘TRUNCATE’). Para nossa finalidade, sempre que a operação for UPDATE, vamos atualizar NEW.ultima_modif com o timestamp atual. Para INSERT, também vamos definir o timestamp, embora a coluna já possa ter um valor padrão — esta é uma convenção que muitos DBAs adotam para consistência.
-- 9. Função gatilho para atualizar ultima_modif automaticamente
-- A linguagem é PL/pgSQL e o tipo de retorno é TRIGGER (obrigatório)
CREATE OR REPLACE FUNCTION fn_atualiza_timestamp_modificacao()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
-- Em qualquer operação INSERT ou UPDATE, atualizamos o timestamp
-- TG_OP é uma variável automática do PostgreSQL que indica a operação
IF TG_OP IN ('INSERT', 'UPDATE') THEN
NEW.ultima_modif := CURRENT_TIMESTAMP;
END IF;
-- Para triggers BEFORE INSERT/UPDATE, devemos retornar NEW
-- Se retornarmos NULL, a operação é cancelada silenciosamente
RETURN NEW;
END;
$$;
-- 10. Criação do trigger propriamente dito
-- Ele executará a função fn_atualiza_timestamp_modificacao ANTES de cada INSERT ou UPDATE
CREATE TRIGGER trg_atualiza_timestamp
BEFORE INSERT OR UPDATE
ON funcionarios
FOR EACH ROW
EXECUTE FUNCTION fn_atualiza_timestamp_modificacao();
Vamos entender cada parte do comando CREATE TRIGGER. A cláusula BEFORE INSERT OR UPDATE define que o trigger será disparado antes que a operação de inserção ou atualização seja concretizada — isso é essencial porque queremos modificar NEW.ultima_modif antes que o PostgreSQL grave a linha no disco. A especificação ON funcionarios vincula o trigger a essa tabela. FOR EACH ROW indica que a função será executada uma vez para cada linha afetada. Por fim, EXECUTE FUNCTION (que a partir do PostgreSQL 11 substituiu o antigo EXECUTE PROCEDURE, embora este último ainda funcione por compatibilidade) associa a função gatilho que criamos.
Agora vamos testar nosso trigger realizando uma atualização em um dos funcionários e verificando se a coluna ultima_modif foi preenchida automaticamente.
-- 11. Teste: atualizar o salário da Ana Silva
UPDATE funcionarios
SET salario = 9200.00
WHERE id_func = 1;
-- 12. Verificação do resultado
-- A coluna ultima_modif deve conter o timestamp do exato momento do UPDATE
SELECT id_func, nome, salario, bonus, salario_total, ultima_modif
FROM funcionarios
WHERE id_func = 1;
id_func | nome | salario | bonus | salario_total | ultima_modif
---------+-----------+---------+-------+---------------+-------------------------------
1 | Ana Silva | 9200.00 | 0.00 | 9200.00 | 2026-07-20 14:32:17.456789-03
(1 row)
Perfeito! O trigger funcionou exatamente como esperado. A coluna ultima_modif foi preenchida automaticamente com o timestamp do momento em que o UPDATE foi executado. Note que não precisamos incluir essa coluna no comando UPDATE — o trigger cuidou disso de forma transparente para a aplicação. Esse é um exemplo clássico de como Triggers no PostgreSQL podem simplificar a camada de aplicação e garantir consistência nos metadados das tabelas.
Agora vamos testar também a operação de INSERT para confirmar que o trigger também atua nesse caso:
-- 13. Teste de INSERT com o trigger
INSERT INTO funcionarios (nome, email, salario, id_depto)
VALUES ('Fernando Lima', 'fernando.lima@empresa.com', 5500.00, 2);
-- 14. Verificação
SELECT id_func, nome, salario, ultima_modif
FROM funcionarios
WHERE nome = 'Fernando Lima';
id_func | nome | salario | ultima_modif
---------+---------------+---------+-------------------------------
6 | Fernando Lima | 5500.00 | 2026-07-20 14:33:42.123456-03
(1 row)
Trigger de Auditoria: Registrando Todas as Alterações em uma Tabela de Histórico
A auditoria de dados é provavelmente o caso de uso mais comum para Triggers no PostgreSQL em ambientes corporativos. A ideia é simples: sempre que uma linha for inserida, modificada ou excluída na tabela principal, uma linha correspondente é inserida na tabela de auditoria, registrando quem fez a alteração, quando, e quais valores foram alterados. Este padrão é essencial para compliance regulatório (LGPD, SOX, PCI-DSS) e também para troubleshooting — quando uma alteração indevida é detectada, a trilha de auditoria permite identificar rapidamente a origem do problema.
Em nossos projetos na JRT Technology Solutions, implementamos auditoria baseada em triggers para tabelas que armazenam dados sensíveis como informações financeiras, dados pessoais de clientes e configurações críticas de sistema. O padrão que vamos construir agora é o mesmo que utilizamos em produção, com algumas simplificações didáticas. A função gatilho precisará diferenciar entre INSERT, UPDATE e DELETE para registrar as informações corretas na tabela de auditoria.
Criaremos uma segunda função gatilho e um segundo trigger na tabela funcionarios. Como o PostgreSQL permite múltiplos triggers na mesma tabela (desde que tenham nomes diferentes), ambos coexistirão sem conflitos. A ordem de execução entre triggers do mesmo tipo (BEFORE, AFTER) é alfabética por nome, mas para triggers de tipos diferentes, a precedência natural é: BEFORE → operação da tabela → AFTER. Nosso trigger de auditoria será do tipo AFTER, garantindo que a operação já foi confirmada antes de registrarmos o log.
-- 15. Função gatilho para auditoria completa (INSERT, UPDATE, DELETE)
CREATE OR REPLACE FUNCTION fn_audita_funcionarios()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
-- Tratamento para INSERT: OLD não está disponível, apenas NEW
IF TG_OP = 'INSERT' THEN
INSERT INTO audit_funcionarios (operacao, id_func, nome_new, salario_new, id_depto_new, usuario_bd)
VALUES ('INSERT', NEW.id_func, NEW.nome, NEW.salario, NEW.id_depto, CURRENT_USER);
RETURN NEW; -- Em AFTER triggers, o retorno é ignorado, mas é boa prática retornar
-- Tratamento para UPDATE: OLD e NEW estão disponíveis
ELSIF TG_OP = 'UPDATE' THEN
INSERT INTO audit_funcionarios (operacao, id_func,
nome_old, nome_new,
salario_old, salario_new,
id_depto_old, id_depto_new,
usuario_bd)
VALUES ('UPDATE', NEW.id_func,
OLD.nome, NEW.nome,
OLD.salario, NEW.salario,
OLD.id_depto, NEW.id_depto,
CURRENT_USER);
RETURN NEW;
-- Tratamento para DELETE: NEW não está disponível, apenas OLD
ELSIF TG_OP = 'DELETE' THEN
INSERT INTO audit_funcionarios (operacao, id_func, nome_old, salario_old, id_depto_old, usuario_bd)
VALUES ('DELETE', OLD.id_func, OLD.nome, OLD.salario, OLD.id_depto, CURRENT_USER);
RETURN OLD;
END IF;
-- Fallback de segurança (nunca deve ser atingido)
RETURN NULL;
END;
$$;
-- 16. Criação do trigger de auditoria (AFTER INSERT, UPDATE, DELETE)
CREATE TRIGGER trg_audita_funcionarios
AFTER INSERT OR UPDATE OR DELETE
ON funcionarios
FOR EACH ROW
EXECUTE FUNCTION fn_audita_funcionarios();
Note a diferença crucial: usamos AFTER em vez de BEFORE. Isso porque não queremos modificar os dados — apenas registrar o que aconteceu. Se tentássemos fazer isso com um trigger BEFORE e a operação fosse posteriormente revertida por uma constraint violation, registraríamos uma auditoria de algo que nunca aconteceu. O trigger AFTER garante que só registramos operações que foram efetivamente persistidas. Além disso, observe a distinção entre os três blocos condicionais: INSERT só tem NEW, DELETE só tem OLD, e UPDATE tem ambos. Tentar acessar OLD em um INSERT resultaria em erro.
Vamos testar o cenário completo: fazer um INSERT, um UPDATE e um DELETE, e depois consultar a tabela de auditoria para ver o resultado.
-- 17. Inserir um novo funcionário
INSERT INTO funcionarios (nome, email, salario, id_depto)
VALUES ('Gabriela Rocha', 'gabriela.rocha@empresa.com', 6800.00, 1);
-- 18. Atualizar o salário do Bruno Costa
UPDATE funcionarios
SET salario = 7000.00
WHERE id_func = 2;
-- 19. Remover o Daniel Oliveira
DELETE FROM funcionarios
WHERE id_func = 4;
-- 20. Consultar a tabela de auditoria para ver todas as operações registradas
SELECT id_audit, operacao, id_func,
nome_old, nome_new,
salario_old, salario_new,
usuario_bd,
data_operacao
FROM audit_funcionarios
ORDER BY id_audit;
id_audit | operacao | id_func | nome_old | nome_new | salario_old | salario_new | usuario_bd | data_operacao
----------+----------+---------+-----------------+------------------+-------------+-------------+------------+-------------------------------
1 | INSERT | 7 | | Gabriela Rocha | | 6800.00 | postgres | 2026-07-20 14:38:55.123456-03
2 | UPDATE | 2 | Bruno Costa | Bruno Costa | 6200.00 | 7000.00 | postgres | 2026-07-20 14:39:20.654321-03
3 | DELETE | 4 | Daniel Oliveira | | 4800.00 | | postgres | 2026-07-20 14:39:45.789012-03
(3 rows)
A tabela de auditoria agora contém um registro detalhado de cada operação. Na linha 1, vemos o INSERT de Gabriela Rocha — as colunas nome_old e salario_old estão nulas porque, em um INSERT, não há valores antigos. Na linha 2, o UPDATE de Bruno Costa mostra o salário antes (6200.00) e depois (7000.00), com o nome mantido igual. Na linha 3, o DELETE de Daniel Oliveira mostra apenas os valores antigos, com as colunas _new nulas. O campo usuario_bd registra o usuário do PostgreSQL que executou a operação — em um ambiente com múltiplos usuários de aplicação, essa informação é crucial para rastreabilidade.
| Variável | Tipo | Descrição | Disponível em |
|---|---|---|---|
| NEW | RECORD | Novo registro (linha após a operação) | INSERT, UPDATE |
| OLD | RECORD | Registro antigo (linha antes da operação) | UPDATE, DELETE |
| TG_OP | text | Tipo de operação: ‘INSERT’, ‘UPDATE’, ‘DELETE’, ‘TRUNCATE’ | Todos |
| TG_NAME | name | Nome do trigger que está executando | Todos |
| TG_TABLE_NAME | name | Nome da tabela onde o trigger foi disparado | Todos |
| TG_TABLE_SCHEMA | name | Esquema da tabela onde o trigger foi disparado | Todos |
| TG_WHEN | text | ‘BEFORE’, ‘AFTER’ ou ‘INSTEAD OF’ | Todos |
| TG_LEVEL | text | ‘ROW’ ou ‘STATEMENT’ | Todos |
| TG_ARGV[] | text[] | Argumentos passados na criação do trigger | Todos |
Triggers com Validação e Regras de Negócio: Impedindo Operações Inválidas
Uma das aplicações mais poderosas dos Triggers no PostgreSQL é a validação de regras de negócio complexas que vão além do que CHECK constraints podem expressar. Enquanto uma CHECK constraint só pode validar valores dentro da própria linha sendo inserida ou atualizada, um trigger pode consultar outras tabelas, comparar valores entre a versão antiga e a nova da linha, e até mesmo realizar cálculos condicionais antes de decidir se a operação deve prosseguir. Vamos construir um exemplo prático: impedir que um funcionário receba um aumento superior a 50% do seu salário atual em uma única atualização, a menos que o departamento tenha orçamento suficiente.
Essa regra simula um controle comum em departamentos de RH: aumentos muito expressivos precisam de aprovação adicional e não podem ser aplicados diretamente no sistema. Para implementar essa validação, criaremos um trigger BEFORE UPDATE que compara NEW.salario com OLD.salario e, se o aumento ultrapassar 50%, levanta uma exceção que cancela a operação. Adicionalmente, vamos verificar se o orçamento do departamento é suficiente para cobrir o novo salário — uma validação que envolve consulta a outra tabela.
-- 21. Função gatilho para validar aumento máximo de 50%
CREATE OR REPLACE FUNCTION fn_valida_aumento_salarial()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_orcamento_depto NUMERIC(12,2);
v_salario_total_depto NUMERIC(12,2);
v_percentual_aumento NUMERIC(5,2);
BEGIN
-- Só validamos se o salário realmente mudou
-- Usamos IS DISTINCT FROM para tratar NULLs corretamente
IF NEW.salario IS NOT DISTINCT FROM OLD.salario THEN
RETURN NEW; -- Sem alteração, permite
END IF;
-- Calcula o percentual de aumento
-- Se OLD.salario for 0 (não deveria, mas por segurança), tratamos
IF OLD.salario > 0 THEN
v_percentual_aumento := ((NEW.salario - OLD.salario) / OLD.salario) * 100;
ELSE
v_percentual_aumento := 100; -- Se salário antigo é 0, qualquer aumento é 100%
END IF;
-- Regra 1: Aumento não pode exceder 50%
IF v_percentual_aumento > 50 THEN
RAISE EXCEPTION 'Aumento de %.2f%% excede o limite máximo de 50%% para o funcionário ID % (de R$ %.2f para R$ %.2f)',
v_percentual_aumento, NEW.id_func, OLD.salario, NEW.salario;
END IF;
-- Regra 2: Verificar orçamento do departamento (se mudou de departamento ou salário)
-- Consulta o orçamento do departamento de destino
SELECT orcamento INTO v_orcamento_depto
FROM departamentos
WHERE id_depto = NEW.id_depto;
-- Se o departamento não existe (id_depto NULL ou inválido), permite
-- (a FK já garante integridade, mas id_depto pode ser NULL)
IF v_orcamento_depto IS NULL AND NEW.id_depto IS NOT NULL THEN
RAISE EXCEPTION 'Departamento ID % não encontrado', NEW.id_depto;
END IF;
-- Se há orçamento definido, calcula o total de salários do departamento
IF v_orcamento_depto IS NOT NULL THEN
SELECT COALESCE(SUM(salario + bonus), 0) INTO v_salario_total_depto
FROM funcionarios
WHERE id_depto = NEW.id_depto
AND id_func != NEW.id_func; -- Exclui o próprio funcionário da soma
-- Adiciona o novo salário do funcionário atual
v_salario_total_depto := v_salario_total_depto + NEW.salario + COALESCE(NEW.bonus, 0);
-- Se o total de salários excede o orçamento, rejeita
IF v_salario_total_depto > v_orcamento_depto THEN
RAISE EXCEPTION 'O salário total do departamento "%" (R$ %.2f) excederia o orçamento disponível (R$ %.2f)',
(SELECT nome_depto FROM departamentos WHERE id_depto = NEW.id_depto),
v_salario_total_depto, v_orcamento_depto;
END IF;
END IF;
-- Se passou por todas as validações, permite a operação
RETURN NEW;
END;
$$;
-- 22. Criação do trigger de validação (BEFORE UPDATE)
CREATE TRIGGER trg_valida_aumento_salarial
BEFORE UPDATE OF salario, id_depto
ON funcionarios
FOR EACH ROW
EXECUTE FUNCTION fn_valida_aumento_salarial();
Observe a cláusula UPDATE OF salario, id_depto no trigger — ela restringe a execução apenas quando essas colunas específicas são mencionadas no comando UPDATE. Isso é uma otimização importante: se um UPDATE modificar apenas o email ou o nome, nosso trigger de validação não será disparado, evitando processamento desnecessário. O PostgreSQL é inteligente o suficiente para comparar a lista de colunas do UPDATE com a lista especificada no trigger e só invocar a função se houver interseção.
Vamos testar a regra de negócio com alguns cenários: primeiro, um aumento dentro do limite (deve funcionar); depois, um aumento que excede 50% (deve ser rejeitado); por fim, um aumento que, embora dentro do limite, estouraria o orçamento do departamento.
-- 23. Teste 1: Aumento de 10% (dentro do limite) - Deve funcionar
-- Fernando Lima: salário atual 5500.00, novo: 6050.00 (aumento de 10%)
UPDATE funcionarios
SET salario = 6050.00
WHERE id_func = 6;
-- Verificação
SELECT id_func, nome, salario FROM funcionarios WHERE id_func = 6;
id_func | nome | salario
---------+---------------+---------
6 | Fernando Lima | 6050.00
(1 row)
-- 24. Teste 2: Aumento de 60% (acima do limite) - Deve ser rejeitado
-- Tentativa de aumentar Fernando Lima de 6050.00 para 9680.00 (aumento de 60%)
UPDATE funcionarios
SET salario = 9680.00
WHERE id_func = 6;
ERROR: Aumento de 60.00% excede o limite máximo de 50% para o funcionário ID 6 (de R$ 6050.00 para R$ 9680.00)
CONTEXT: PL/pgSQL function fn_valida_aumento_salarial() line 24 at RAISE
O trigger barrou a operação com uma mensagem de erro clara e informativa, indicando exatamente qual foi o percentual excedido e quais valores estavam envolvidos. Essa prática de fornecer mensagens detalhadas nas exceções é algo que enfatizamos constantemente nos treinamentos da JRT Technology Solutions — um erro genérico como “Operação inválida” não ajuda ninguém a diagnosticar o problema, enquanto uma mensagem específica acelera a resolução.
-- 25. Teste 3: Aumento que estouraria o orçamento do departamento
-- Vamos verificar o orçamento atual do Financeiro (id_depto=2)
SELECT nome_depto, orcamento FROM departamentos WHERE id_depto = 2;
-- E o total de salários atual do Financeiro
SELECT SUM(salario + bonus) AS total_salarios
FROM funcionarios
WHERE id_depto = 2;
-- Tentar aumentar o salário para um valor que exceda o orçamento
-- Financeiro: orcamento 800000.00, funcionários: Bruno (7000) + Fernando (6050)
-- Total atual: 13050. Se aumentarmos Bruno para 790000.00, estoura
UPDATE funcionarios
SET salario = 790000.00
WHERE id_func = 2;
nome_depto | orcamento
------------+------------
Financeiro | 800000.00
(1 row)
total_salarios
----------------
13050.00
(1 row)
ERROR: O salário total do departamento "Financeiro" (R$ 796050.00) excederia o orçamento disponível (R$ 800000.00)
CONTEXT: PL/pgSQL function fn_valida_aumento_salarial() line 48 at RAISE
Perfeito! Nosso trigger de validação funcionou em todos os cenários. Ele permitiu o aumento legítimo, bloqueou o aumento abusivo e também impediu a violação orçamentária. Note que as validações foram executadas antes da operação (BEFORE UPDATE), garantindo que nenhum dado inválido jamais seja gravado na tabela.
Triggers Condicionais com WHEN: Filtrando Eventos para Performance
Em tabelas com milhões de linhas e milhares de atualizações por segundo, cada milissegundo gasto na execução de triggers importa. É por isso que o PostgreSQL oferece a cláusula WHEN, que permite especificar uma condição booleana que é avaliada antes de invocar a função gatilho. Se a condição for falsa, o trigger simplesmente não é executado — a função nem chega a ser chamada. Isso é drasticamente mais eficiente do que verificar a condição dentro da função com um IF.
Vamos criar um exemplo prático: um trigger que registra na tabela de auditoria apenas as alterações salariais que ultrapassam R$ 1.000,00 de diferença (para mais ou para menos). Pequenas correções salariais não precisam ser auditadas com tanto detalhe, mas variações significativas sim. Implementaremos isso com um trigger AFTER UPDATE e a cláusula WHEN que verifica a diferença absoluta entre o salário novo e o antigo.
-- 26. Função gatilho para auditoria de grandes variações salariais
CREATE OR REPLACE FUNCTION fn_audita_grandes_variacoes()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
-- Registra apenas a informação relevante: a variação salarial
INSERT INTO audit_funcionarios (operacao, id_func,
salario_old, salario_new,
usuario_bd,
data_operacao)
VALUES ('BIGVAR', NEW.id_func,
OLD.salario, NEW.salario,
CURRENT_USER,
CURRENT_TIMESTAMP);
RETURN NEW;
END;
$$;
-- 27. Criação do trigger condicional com WHEN
-- A condição: a diferença absoluta entre salário novo e antigo é >= 1000
-- Usamos a função ABS() e o operador de diferença absoluta
-- OLD e NEW estão disponíveis na cláusula WHEN (exceção à regra geral de PL/pgSQL)
CREATE TRIGGER trg_audita_grandes_variacoes
AFTER UPDATE OF salario
ON funcionarios
FOR EACH ROW
WHEN (ABS(NEW.salario - OLD.salario) >= 1000.00)
EXECUTE FUNCTION fn_audita_grandes_variacoes();
A grande vantagem aqui está na cláusula WHEN (ABS(NEW.salario – OLD.salario) >= 1000.00). Para todas as atualizações onde a variação salarial for menor que R$ 1.000,00, o PostgreSQL nem chega a invocar a função — a economia de chamadas de função e de contexto de trigger é significativa em operações em lote. Vamos testar com duas atualizações: uma pequena (que não deve gerar auditoria) e uma grande (que deve gerar).
-- 28. Teste 1: Pequena variação (menos de 1000) - NÃO deve gerar auditoria
-- Bruno Costa: salário atual 7000.00, novo: 7500.00 (diferença = 500)
UPDATE funcionarios
SET salario = 7500.00
WHERE id_func = 2;
-- 29. Teste 2: Grande variação (mais de 1000) - DEVE gerar auditoria
-- Elena Santos: salário atual 9100.00, novo: 10500.00 (diferença = 1400)
UPDATE funcionarios
SET salario = 10500.00
WHERE id_func = 5;
-- 30. Consulta à tabela de auditoria filtrando apenas 'BIGVAR'
SELECT operacao, id_func, salario_old, salario_new,
(salario_new - salario_old) AS diferenca
FROM audit_funcionarios
WHERE operacao = 'BIGVAR'
ORDER BY id_audit;
operacao | id_func | salario_old | salario_new | diferenca
----------+---------+-------------+-------------+-----------
BIGVAR | 5 | 9100.00 | 10500.00 | 1400.00
(1 row)
Conforme esperado, apenas a grande variação (Elena Santos, diferença de R$ 1.400,00) foi registrada. A alteração de Bruno Costa (diferença de R$ 500,00) passou despercebida pelo trigger condicional. Se você consultar a tabela de auditoria completa, verá que outras operações continuam registradas normalmente (INSERT, DELETE e UPDATE foram capturados pelo trigger de auditoria geral que criamos anteriormente), mas a linha específica de
Quer aprender na prática com especialistas?
A JRT Technology Solutions oferece treinamentos e implementação de PostgreSQL para equipes corporativas.