PostgreSQL: Restrições e Integridade de Dados no PostgreSQL

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

1. O Que Você Vai Aprender


2. A História

Charlie é um arquiteto de banco de dados em uma plataforma SaaS. Ele precisa impor as seguintes regras de negócio no nível do banco de dados:

  1. O valor de cada pedido deve ser > 0 (CHECK)
  2. Os intervalos de tempo das reservas de sala de reunião não devem se sobrepor (EXCLUSION)
  3. Excluir um cliente deve limpar automaticamente seus pedidos, mas pedidos críticos devem ser protegidos (estratégia de cascata FOREIGN KEY)
  4. Ao importar dados em lote, referências entre linhas podem violar temporariamente restrições e só devem ser verificadas após a conclusão da importação (DEFERRABLE)

Charlie usa restrições em vez de validação na camada de aplicação, tornando o banco de dados a última linha de defesa para integridade de dados.


3. Conceito: PRIMARY KEY

(1) O Papel da Chave Primária

A chave primária identifica unicamente cada linha em uma tabela, combinando UNIQUE + NOT NULL. Uma tabela pode ter apenas uma chave primária.

Propriedade Descrição
Unicidade Valores duplicados não permitidos
Não nulo NULL não permitido
Índice automático O PostgreSQL cria automaticamente um índice único B-Tree para a chave primária
Um por tabela Apenas uma PRIMARY KEY pode ser definida

▶ Exemplo: Chave Primária de Coluna Única

SQL
CREATE TABLE customers (
  customer_id BIGSERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  email VARCHAR(255) UNIQUE
);

Output:

TEXT 📖 Somente leitura
CREATE TABLE

▶ Exemplo: Chave Primária Composta

SQL
CREATE TABLE order_items (
  order_id   BIGINT NOT NULL,
  product_id BIGINT NOT NULL,
  quantity   INT NOT NULL CHECK (quantity > 0),
  PRIMARY KEY (order_id, product_id)
);

Output:

TEXT 📖 Somente leitura
CREATE TABLE

(2) Comparação de Tipos de Coluna de Chave Primária

Tipo Armazenamento Intervalo Caso de uso
SERIAL / BIGSERIAL 4/8 bytes 2B / 9,2×10¹⁸ Maioria das tabelas de negócio
UUID 16 bytes Globalmente único Sistemas distribuídos
Chave natural (ex., email) Variável Raramente usado; risco de mudança de negócio
Chave primária composta Múltiplas colunas Tabelas de junção, tabelas ponte muitos-para-muitos

▶ Exemplo: Chave Primária UUID

SQL
CREATE TABLE global_events (
  event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  event_name VARCHAR(200) NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

Output:

TEXT 📖 Somente leitura
CREATE TABLE

4. Conceito: FOREIGN KEY

(1) Chave Estrangeira e as Cinco Estratégias de Cascata

Uma chave estrangeira garante integridade referencial: o valor da coluna de referência na tabela filha deve existir na chave primária/única da tabela pai.

Estratégia Comportamento ON DELETE Comportamento ON UPDATE Cenário típico
CASCADE Excluir em cascata linhas filhas Atualizar em cascata linhas filhas Itens de pedido excluídos com o pedido
SET NULL Definir linha filha como NULL Definir linha filha como NULL Associação opcional; exclusão sem impacto
SET DEFAULT Definir linha filha como padrão Definir linha filha como padrão Raramente usado
RESTRICT Rejeitar exclusão (imediato) Rejeitar atualização (imediato) Proteger dados críticos
NO ACTION Rejeitar exclusão (adiável) Rejeitar atualização (adiável) Comportamento padrão

▶ Exemplo: CASCADE Exclusão em Cascata

SQL
CREATE TABLE orders (
  order_id    BIGSERIAL PRIMARY KEY,
  customer_id BIGINT NOT NULL
    REFERENCES customers(customer_id) ON DELETE CASCADE,
  total_amount DECIMAL(12,2) NOT NULL
);

Output:

TEXT 📖 Somente leitura
CREATE TABLE

Quando um cliente é excluído, todos os seus pedidos são excluídos automaticamente.

▶ Exemplo: RESTRICT Protege Dados Críticos

SQL
CREATE TABLE invoices (
  invoice_id  BIGSERIAL PRIMARY KEY,
  order_id    BIGINT NOT NULL
    REFERENCES orders(order_id) ON DELETE RESTRICT,
  invoice_date DATE NOT NULL
);

Output:

TEXT 📖 Somente leitura
CREATE TABLE

Se um pedido já tem uma fatura, a exclusão do pedido é rejeitada.

(2) Comparação CASCADE vs RESTRICT

Dimensão CASCADE RESTRICT
Excluir linha pai Linhas filhas também excluídas Exclusão rejeitada
Segurança dos dados Conveniente mas arriscado Seguro mas precisa de limpeza manual
Caso de uso Dados auxiliares Dados críticos / financeiros

▶ Exemplo: Cascata de Múltiplos Níveis

SQL
CREATE TABLE customers (
  customer_id BIGSERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL
);

CREATE TABLE orders (
  order_id    BIGSERIAL PRIMARY KEY,
  customer_id BIGINT REFERENCES customers(customer_id) ON DELETE CASCADE
);

CREATE TABLE order_items (
  item_id     BIGSERIAL PRIMARY KEY,
  order_id    BIGINT REFERENCES orders(order_id) ON DELETE CASCADE,
  product_id  BIGINT NOT NULL
);

Output:

TEXT 📖 Somente leitura
CREATE TABLE

Excluir cliente → excluir automaticamente pedidos → excluir automaticamente order_items, uma cascata de três níveis.

▶ Exemplo: SET NULL

SQL
CREATE TABLE reviews (
  review_id   BIGSERIAL PRIMARY KEY,
  product_id  BIGINT REFERENCES products(product_id) ON DELETE SET NULL,
  content     TEXT NOT NULL
);

Output:

TEXT 📖 Somente leitura
CREATE TABLE

Após um produto ser excluído, o product_id da avaliação é definido como NULL, mas a avaliação é mantida.


5. Conceito: UNIQUE e NOT NULL

(1) Restrição UNIQUE

UNIQUE garante que valores de coluna (ou combinações de colunas) não sejam duplicados e permite NULL (múltiplos NULLs não contam como duplicatas).

Dimensão PRIMARY KEY UNIQUE
Quantidade por tabela Apenas uma Múltiplas permitidas
NULL permitido Não permitido Permitido (múltiplos NULLs não conflitam)
Índice automático Sim Sim
Semântica Identifica a linha Garante unicidade

▶ Exemplo: Restrição UNIQUE de Múltiplas Colunas

SQL
CREATE TABLE user_accounts (
  user_id BIGSERIAL PRIMARY KEY,
  email   VARCHAR(255) UNIQUE,
  phone   VARCHAR(20),
  UNIQUE (email, phone)
);

Output:

TEXT 📖 Somente leitura
CREATE TABLE

email único por si só + (email, phone) único como combinação.

(2) Restrição NOT NULL

NOT NULL proíbe uma coluna de armazenar valores NULL; é a restrição de integridade mais básica.

Escrita Descrição
col TYPE NOT NULL Restrição em nível de coluna
CONSTRAINT nn_col CHECK (col IS NOT NULL) Forma equivalente

▶ Exemplo: Combinação NOT NULL

SQL
CREATE TABLE products (
  product_id   BIGSERIAL PRIMARY KEY,
  name         VARCHAR(200) NOT NULL,
  unit_price   DECIMAL(10,2) NOT NULL,
  description  TEXT
);

Output:

TEXT 📖 Somente leitura
CREATE TABLE

description permite NULL; as outras colunas-chave não.


6. Conceito: Restrição CHECK

(1) Básico da Restrição CHECK

Uma restrição CHECK exige que um valor de coluna satisfaça uma expressão booleana dada. O CHECK do PostgreSQL pode referenciar outras colunas na mesma linha (o padrão SQL também suporta, mas muitos bancos de dados não).

Propriedade Descrição
Verificação em nível de linha Só pode referenciar colunas da linha atual
Recurso do PostgreSQL Pode referenciar outras linhas (via subconsulta, com limites)
CHECK em nível de tabela Pode restringir múltiplas colunas de uma vez
NO INHERIT Não propagado para tabelas filhas (recurso do PG)

▶ Exemplo: Valor Deve Ser Positivo

SQL
CREATE TABLE orders (
  order_id    BIGSERIAL PRIMARY KEY,
  total_amount DECIMAL(12,2) NOT NULL CHECK (total_amount > 0),
  discount    DECIMAL(12,2) DEFAULT 0 CHECK (discount >= 0)
);

Output:

TEXT 📖 Somente leitura
CREATE TABLE

▶ Exemplo: Restrição CHECK de Múltiplas Colunas

SQL
CREATE TABLE campaigns (
  campaign_id BIGSERIAL PRIMARY KEY,
  start_date  DATE NOT NULL,
  end_date    DATE NOT NULL,
  budget      DECIMAL(12,2) NOT NULL CHECK (budget > 0),
  CONSTRAINT chk_date_range CHECK (end_date >= start_date),
  CONSTRAINT chk_budget_limit CHECK (budget <= 1000000)
);

Output:

TEXT 📖 Somente leitura
CREATE TABLE

(2) Padrões Comuns de CHECK

Regra de negócio Expressão CHECK
Valor positivo CHECK (amount > 0)
Intervalo de desconto CHECK (discount BETWEEN 0 AND 1)
Ordem de datas CHECK (end_date >= start_date)
Valores de enum CHECK (status IN ('active', 'inactive', 'pending'))
Comprimento da string CHECK (LENGTH(phone) >= 10)
Porcentagem CHECK (rate >= 0 AND rate <= 100)

▶ Exemplo: Restrições Nomeadas para Facilitar a Gestão

SQL
CREATE TABLE subscriptions (
  sub_id    BIGSERIAL PRIMARY KEY,
  plan      VARCHAR(50) NOT NULL,
  price     DECIMAL(10,2) NOT NULL,
  CONSTRAINT chk_price_positive CHECK (price > 0),
  CONSTRAINT chk_plan_valid CHECK (plan IN ('free', 'basic', 'pro', 'enterprise'))
);

Output:

TEXT 📖 Somente leitura
CREATE TABLE

▶ Exemplo: Adicionar um CHECK a uma Tabela Existente

SQL
ALTER TABLE orders
ADD CONSTRAINT chk_amount_positive CHECK (total_amount > 0);

Output:

TEXT 📖 Somente leitura
-- instrução SQL executada com sucesso

7. Conceito: Restrição EXCLUSION

(1) Como EXCLUSION Funciona

Uma restrição EXCLUSION garante: se duas linhas são "iguais" em colunas especificadas (comparadas com o operador =), elas não se "sobrepõem" na dimensão especificada (comparada com um operador de sobreposição). Este é um recurso específico do PostgreSQL.

Cenário Restrição Operador
Intervalos de tempo não se sobrepõem Os horários do mesmo recurso não podem cruzar =, &&
Sem assentos duplicados Os números de assento da mesma sessão não se repetem =, = (equivalente a UNIQUE)

▶ Exemplo: Reservas de Sala de Reunião Não se Sobrepoem no Tempo

SQL
CREATE TABLE room_bookings (
  booking_id BIGSERIAL PRIMARY KEY,
  room_id    INT NOT NULL,
  time_range TSTZRANGE NOT NULL,
  booked_by  VARCHAR(100) NOT NULL,
  CONSTRAINT excl_room_no_overlap
    EXCLUDE USING GiST (room_id WITH =, time_range WITH &&)
);

Output:

TEXT 📖 Somente leitura
CREATE TABLE

▶ Exemplo: Inserção Conflitante

SQL
INSERT INTO room_bookings (room_id, time_range, booked_by)
VALUES (1, '[2025-07-13 09:00, 2025-07-13 11:00)', 'Alice');

INSERT INTO room_bookings (room_id, time_range, booked_by)
VALUES (1, '[2025-07-13 10:00, 2025-07-13 12:00)', 'Bob');
TEXT 📖 Somente leitura
ERROR: conflicting key value violates exclusion constraint "excl_room_no_overlap"
DETAIL: Key (room_id, time_range)=(1, [2025-07-13 10:00,2025-07-13 12:00))
conflicts with existing key (room_id, time_range)=(1, [2025-07-13 09:00,2025-07-13 11:00)).

(2) Comparação EXCLUSION vs UNIQUE

Dimensão UNIQUE EXCLUSION
Operador de comparação Apenas = Personalizado (=, &&, <->, etc.)
Sobreposição de intervalo Não suportado Suportado
Tipo de índice B-Tree GiST / SP-GiST
Flexibilidade Baixa Alta
Caso de uso Unicidade de valor discreto Intervalos não sobrepostos, restrições de distância

▶ Exemplo: Intervalos de Preço Não se Sobrepoem

SQL
CREATE TABLE discount_tiers (
  tier_id    BIGSERIAL PRIMARY KEY,
  product_id INT NOT NULL,
  price_range NUMRANGE NOT NULL,
  discount   DECIMAL(5,4) NOT NULL,
  CONSTRAINT excl_price_no_overlap
    EXCLUDE USING GiST (product_id WITH =, price_range WITH &&)
);

Output:

TEXT 📖 Somente leitura
CREATE TABLE

8. Conceito: Restrição Adiada DEFERRABLE

(1) Mecanismo de Restrição Adiada

Por padrão, as restrições são verificadas imediatamente após cada instrução. Uma restrição DEFERRABLE permite que a verificação seja adiada até o commit da transação, resolvendo o problema de referência circular em operações em lote.

Modo Momento da verificação Sintaxe
IMMEDIATE Após cada instrução Comportamento padrão
DEFERRABLE INITIALLY IMMEDIATE Após cada instrução (comutável) DEFERRABLE INITIALLY IMMEDIATE
DEFERRABLE INITIALLY DEFERRED No commit da transação DEFERRABLE INITIALLY DEFERRED

▶ Exemplo: Solução para Referências Circulares

SQL
CREATE TABLE departments (
  dept_id   INT PRIMARY KEY,
  name      VARCHAR(100) NOT NULL,
  manager_id INT
);

ALTER TABLE departments
ADD CONSTRAINT fk_dept_manager
  FOREIGN KEY (manager_id) REFERENCES departments(dept_id)
  DEFERRABLE INITIALLY DEFERRED;

Output:

TEXT 📖 Somente leitura
CREATE TABLE

▶ Exemplo: Operações Dentro de uma Transação

SQL
BEGIN;

INSERT INTO departments (dept_id, name, manager_id)
VALUES (1, 'Engineering', 2);

INSERT INTO departments (dept_id, name, manager_id)
VALUES (2, 'QA', 1);

COMMIT;

Output:

TEXT 📖 Somente leitura
INSERT 0 1

Executar o primeiro INSERT sozinho falharia (manager_id=2 ainda não existe), mas DEFERRABLE adia a verificação até COMMIT, então uma vez que ambos INSERTs são concluídos, a validação passa.

(2) Alternar Dinamicamente o Momento da Verificação

▶ Exemplo: SET CONSTRAINTS

SQL
BEGIN;
SET CONSTRAINTS fk_dept_manager DEFERRED;

INSERT INTO departments (dept_id, name, manager_id) VALUES (3, 'Sales', 4);
INSERT INTO departments (dept_id, name, manager_id) VALUES (4, 'Marketing', 3);

SET CONSTRAINTS fk_dept_manager IMMEDIATE;
COMMIT;

Output:

TEXT 📖 Somente leitura
INSERT 0 1

(3) Comparação IMMEDIATE vs DEFERRED

Dimensão IMMEDIATE DEFERRED
Momento da verificação Após cada instrução No commit da transação
Desempenho Mais rápido (feedback imediato) Ligeiramente mais lento (verificação em lote)
Referência circular Não resolve Solucionável
Risco Baixo Violação descoberta apenas no fim da transação
Caso de uso Maioria das restrições Referências circulares, importação em lote

9. Conceito: Nomenclatura e Gestão de Restrições

(1) Convenção de Nomenclatura de Restrições

Uma boa nomenclatura facilita localizar problemas e executar operações.

Tipo de restrição Prefixo recomendado Exemplo
PRIMARY KEY pk_ pk_orders
FOREIGN KEY fk_ fk_orders_customer
UNIQUE uq_ uq_users_email
CHECK chk_ chk_amount_positive
EXCLUSION excl_ excl_room_no_overlap

▶ Exemplo: Nomear Explicitamente Todas as Restrições

SQL
CREATE TABLE orders (
  order_id     BIGSERIAL      CONSTRAINT pk_orders PRIMARY KEY,
  customer_id  BIGINT NOT NULL CONSTRAINT fk_orders_customer
    REFERENCES customers(customer_id) ON DELETE CASCADE,
  total_amount DECIMAL(12,2)  CONSTRAINT chk_amount_positive CHECK (total_amount > 0),
  status       VARCHAR(20)    CONSTRAINT chk_status_valid
    CHECK (status IN ('pending', 'shipped', 'delivered', 'cancelled')),
  order_date   TIMESTAMPTZ    NOT NULL DEFAULT NOW()
);

Output:

TEXT 📖 Somente leitura
CREATE TABLE

(2) Operações de Gestão de Restrições

Operação Sintaxe
Adicionar restrição ALTER TABLE t ADD CONSTRAINT name CHECK (...)
Remover restrição ALTER TABLE t DROP CONSTRAINT name
Visualizar restrições SELECT * FROM pg_constraint WHERE conrelid = 't'::regclass
Desabilitar triggers (desabilitar restrições indiretamente) ALTER TABLE t DISABLE TRIGGER ALL

▶ Exemplo: Visualizar Todas as Restrições de uma Tabela

SQL
SELECT
  conname AS constraint_name,
  contype AS type,
  pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'orders'::regclass;
TEXT 📖 Somente leitura
 constraint_name       | type | definition
-----------------------+------+--------------------------------------------
 pk_orders             | p    | PRIMARY KEY (order_id)
 fk_orders_customer    | f    | FOREIGN KEY (customer_id) REFERENCES ...
 chk_amount_positive   | c    | CHECK ((total_amount > 0))
 chk_status_valid      | c    | CHECK ((status = ANY (ARRAY['pending'::...

10. Exemplo Abrangente

Esquema de restrições da plataforma SaaS do Charlie — cobrindo chave primária, cascata de chave estrangeira, CHECK, EXCLUSION, DEFERRABLE:

SQL
CREATE TABLE customers (
  customer_id BIGSERIAL    CONSTRAINT pk_customers PRIMARY KEY,
  name        VARCHAR(100) NOT NULL,
  email       VARCHAR(255) CONSTRAINT uq_customers_email UNIQUE,
  credit_limit DECIMAL(12,2) DEFAULT 0
    CONSTRAINT chk_credit_non_negative CHECK (credit_limit >= 0)
);

CREATE TABLE orders (
  order_id     BIGSERIAL      CONSTRAINT pk_orders PRIMARY KEY,
  customer_id  BIGINT NOT NULL CONSTRAINT fk_orders_customer
    REFERENCES customers(customer_id) ON DELETE RESTRICT,
  total_amount DECIMAL(12,2) NOT NULL
    CONSTRAINT chk_amount_positive CHECK (total_amount > 0),
  status       VARCHAR(20) DEFAULT 'pending'
    CONSTRAINT chk_status CHECK (status IN ('pending','shipped','delivered','cancelled')),
  created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE room_bookings (
  booking_id BIGSERIAL   CONSTRAINT pk_bookings PRIMARY KEY,
  room_id    INT NOT NULL,
  time_range TSTZRANGE NOT NULL,
  booked_by  VARCHAR(100) NOT NULL,
  CONSTRAINT excl_room_no_overlap
    EXCLUDE USING GiST (room_id WITH =, time_range WITH &&)
);

ALTER TABLE orders
ADD CONSTRAINT fk_orders_customer_deferred
  FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
  ON DELETE RESTRICT
  DEFERRABLE INITIALLY IMMEDIATE;

11. Fluxo de Execução

Onde a verificação de restrições se encaixa na execução de uma instrução DML:

100%
flowchart TD
    A[INSERT/UPDATE/DELETE] --> B{Verificação NOT NULL}
    B -->|passa| C{Verificação CHECK}
    B -->|falha| Z[Erro, reverter]
    C -->|passa| D{Verificação UNIQUE}
    C -->|falha| Z
    D -->|passa| E{Verificação PRIMARY KEY}
    D -->|falha| Z
    E -->|passa| F{Verificação EXCLUSION}
    E -->|falha| Z
    F -->|passa| G{Momento da restrição?}
    F -->|falha| Z
    G -->|IMMEDIATE| H[Verificar FK imediatamente]
    G -->|DEFERRED| I[Adiar verificação FK para COMMIT]
    H -->|passa| J[Instrução concluída]
    I --> K[COMMIT]
    K --> L[Verificar todas FK DEFERRED]
    L -->|passa| M[Transação confirmada]
    L -->|falha| Z

    style Z fill:#ffccbc
    style J fill:#c8e6c9
    style M fill:#c8e6c9

❓ Perguntas Frequentes

P: Qual a diferença entre RESTRICT e NO ACTION? R: Funcionalmente quase idênticos — ambos rejeitam exclusões/atualizações. A diferença é que NO ACTION pode ser combinado com DEFERRABLE para adiar a verificação, enquanto RESTRICT sempre verifica imediatamente.

P: Uma restrição UNIQUE permite múltiplos NULLs? R: No PostgreSQL, uma coluna UNIQUE permite múltiplos NULLs, porque NULL != NULL. Se você precisa que NULL também seja único, adicione uma restrição NOT NULL.

P: Uma restrição CHECK pode referenciar linhas em outras tabelas? R: Você pode escrever uma subconsulta, mas o CHECK do PostgreSQL só garante a restrição para a linha atual — ele não garante consistência entre linhas (outra sessão pode modificar os dados referenciados). Para consistência entre tabelas, use uma chave estrangeira.

P: Qual índice uma restrição EXCLUSION precisa? R: Precisa de um índice GiST ou SP-GiST para suporte. O PostgreSQL cria automaticamente o índice correspondente para uma restrição EXCLUSION.

P: DEFERRABLE pode ser usado para restrições CHECK? R: Não. DEFERRABLE se aplica apenas a restrições FOREIGN KEY e UNIQUE. CHECK e NOT NULL são sempre verificados imediatamente.

P: Uma exclusão CASCADE grande afeta o desempenho? R: Sim. Exclusões em cascata executam linha por linha e podem gerar bloqueios pesados e I/O. Para grandes exclusões em lote, é recomendado excluir manualmente os dados da tabela filha primeiro, depois excluir a linha pai, para evitar transações longas.


📖 Resumo


📝 Exercícios

  1. ⭐ Crie as tabelas customers e orders, defina as chaves primária e estrangeira (ON DELETE CASCADE) e adicione uma restrição CHECK garantindo que o valor do pedido > 0.

  2. ⭐ Crie uma tabela products com uma coluna SKU UNIQUE e uma restrição CHECK garantindo preço > 0 e desconto <= preço.

  3. ⭐⭐ Use uma restrição EXCLUSION para criar uma tabela room_bookings, garantindo que os intervalos de tempo da mesma sala de reunião não se sobreponham e teste uma inserção conflitante.

  4. ⭐⭐ Crie duas tabelas com referência mútua (employees referencia departments e o manager_id de departments referencia employees) e use DEFERRABLE para resolver a referência circular.

  5. ⭐⭐⭐ Projete um modelo de dados completo de tenant SaaS com: uma tabela tenants, uma tabela users (cascata de chave estrangeira), uma tabela subscriptions (CHECK validando datas e valores), uma tabela resource-booking (EXCLUSION prevenindo sobreposição de tempo) e nomeie todas as restrições.

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%