Modelagem de Entidade-Relacionamento
Cardinalidade, opcionalidades e convenções de nomenclatura transformam substantivos do domínio em tabelas PostgreSQL que permanecem corretas sob carga.
Receita
Cartão de referência rápida - pronto para copiar e colar.
-- Um para muitos: FK na tabela "muitos"
CREATE TABLE posts (
post_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
author_id bigint NOT NULL REFERENCES authors (author_id)
);
-- Relacionamento opcional: FK anulável
CREATE TABLE posts (
post_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
editor_id bigint REFERENCES editors (editor_id) -- NULL = não atribuído
);
-- Muitos para muitos: tabela de junção com duas FKs
CREATE TABLE tag_assignments (
post_id bigint NOT NULL REFERENCES posts (post_id) ON DELETE CASCADE,
tag_id bigint NOT NULL REFERENCES tags (tag_id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_id)
);Quando usar isso:
- Design de esquema Greenfield antes da primeira migração ser enviada.
- Refatoração de um
JOINde fan-out causado pela falta de uma tabela de junção. - Revisão se uma FK anulável ou uma tabela de link separada é a estrutura correta.
- Nomenclatura de tabelas e colunas para que os caminhos de
JOINse pareçam com o domínio.
Exemplo de Trabalho
BEGIN;
CREATE TABLE authors (
author_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL
);
CREATE TABLE editors (
editor_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL
);
CREATE TABLE posts (
post_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
author_id bigint NOT NULL REFERENCES authors (author_id),
editor_id bigint REFERENCES editors (editor_id),
title text NOT NULL,
published boolean NOT NULL DEFAULT false
);
CREATE TABLE tags (
tag_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
label text NOT NULL UNIQUE
);
CREATE TABLE post_tags (
post_id bigint NOT NULL REFERENCES posts (post_id) ON DELETE CASCADE,
tag_id bigint NOT NULL REFERENCES tags (tag_id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_id)
);
CREATE INDEX posts_author_id_idx ON posts (author_id);
CREATE INDEX posts_editor_id_idx ON posts (editor_id) WHERE editor_id IS NOT NULL;
CREATE INDEX post_tags_tag_id_idx ON post_tags (tag_id);
COMMIT;O que isso demonstra:
- Relacionamentos obrigatórios vs. opcionais através de colunas FK
NOT NULLvs. anuláveis. - M:N resolvido com
post_tagsem vez de duplicar linhas de tag emposts. - Índice parcial em
editor_idopcional mantém o tamanho do índice pequeno quando a maioria dos posts não tem editor. ON DELETE CASCADEem linhas de junção quando posts pai ou tags são removidos.
Mergulho Profundo
Cardinalidade em Resumo
| Relacionamento | Forma PostgreSQL | Localização da FK |
|---|---|---|
| 1:1 | FK Única ou PK Compartilhada | Ambas as tabelas; restrição UNIQUE necessária |
| 1:N | FK na filha | Lado "muitos" |
| M:N | Tabela de junção | Ambas as FKs na tabela de link |
| Opcional | FK Anulável | Filha permite NULL |
| Obrigatório | FK NOT NULL | Filha sempre tem pai |
Convenções de Nomenclatura
- Tabelas: substantivos no plural (
orders,line_items) ou singular se sua organização padroniza em singular - escolha um e aplique lint. - Colunas PK:
<entidade>_id(order_id) combigint GENERATED ALWAYS AS IDENTITY. - Colunas FK: correspondem ao nome da PK referenciada (
customer_idreferenciacustomers.customer_id). - Tabelas de junção:
<esquerda>_<direita>ou<esquerda>_<direita>_map(order_items,user_roles).
Regras de Opcionalidade
-- Todo pedido deve ter um cliente (obrigatório)
customer_id bigint NOT NULL REFERENCES customers (customer_id)
-- O envio pode ainda não existir (opcional)
shipment_id bigint REFERENCES shipments (shipment_id)- Relacionamentos obrigatórios:
NOT NULL+ FK. - Relacionamentos opcionais: FK anulável; use índices parciais para buscas seletivas.
- Não modele a opcionalidade apenas no código da aplicação - o banco de dados deve rejeitar estados impossíveis.
Notas SQL
-- Detecta linhas órfãs se as FKs foram adicionadas tarde (deve retornar 0)
SELECT o.order_id
FROM orders o
LEFT JOIN customers c ON c.customer_id = o.customer_id
WHERE c.customer_id IS NULL;
-- 1:1 imposto com UNIQUE na coluna FK
CREATE TABLE user_profiles (
user_id bigint PRIMARY KEY REFERENCES users (user_id),
bio text
);Armadilhas
- M:N modelado como colunas duplicadas - armazenar
tag1,tag2,tag3empostsquebra a normalização e impede a indexação. Correção: tabela de junção com(post_id, tag_id). - FK no lado errado - colocar
order_idemcustomerspara uma lista de pedidos 1:N. Correção: FK emorders.customer_id. - FK anulável tratada como obrigatória em consultas -
INNER JOINs removem linhas com editores não atribuídos. Correção:LEFT JOINquando o relacionamento é opcional. - Índice ausente em colunas FK -
NESTED LOOP JOINs degradam em tabelas grandes. Correção:CREATE INDEX ON child (parent_id). - Junção simétrica sem unicidade - pares duplicados de
(post_id, tag_id). Correção:PRIMARY KEYcomposto ou restriçãoUNIQUE.
Alternativas
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
ENUM do PostgreSQL para status | Conjuntos de valores pequenos e estáveis | Valores mudam semanalmente (dor de migração) |
jsonb para atributos aninhados | Esquema varia por linha, leitura intensiva | Você precisa de integridade de FK em IDs aninhados |
| Chaves naturais compostas | Identificadores de negócios fortes existem globalmente | IDs são compostos entre locatários ou regiões |
| Herança de tabela única | Poucos subtipos, colunas compartilhadas | Subtipos divergem com muitas colunas anuláveis |
FAQs
As chaves primárias devem ser sempre colunas de identidade surrogate bigint?
Chaves surrogate são o padrão para OLTP porque são estreitas, estáveis e amigáveis para índices. Use chaves naturais (email, SKU) como restrições UNIQUE quando o negócio garante unicidade global.
Como modelo um relacionamento opcional um para um?
Coloque uma FK UNIQUE anulável em um lado:
CREATE TABLE passports (
passport_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
person_id bigint NOT NULL UNIQUE REFERENCES persons (person_id),
number text NOT NULL
);O UNIQUE em person_id impõe no máximo um passaporte por pessoa.
Quando uma tabela de junção é excessiva?
Quando a associação não tem atributos e cada lado aparece no máximo uma vez - um 1:1 pode usar uma única FK. Se o link carrega metadados (assigned_at, role), use uma junção ou tabela de link mesmo para baixa cardinalidade.
Devo nomear restrições FK explicitamente?
Sim, para operabilidade:
CONSTRAINT orders_customer_id_fkey
FOREIGN KEY (customer_id) REFERENCES customers (customer_id)Nomes explícitos tornam as diferenças de migração e as mensagens de erro legíveis nos logs de produção.
Como documento a cardinalidade para leitores não-DB?
Use uma legenda ERD simples em seu repositório (1 --< N, N >--< N) e espelhe-a em ADRs. Mantenha a verdade executável no DDL, não apenas em diagramas.
Duas tabelas podem compartilhar a mesma chave primária para 1:1?
Sim - comum para tabelas de extensão:
CREATE TABLE users (user_id bigint PRIMARY KEY, email text NOT NULL);
CREATE TABLE user_settings (
user_id bigint PRIMARY KEY REFERENCES users (user_id),
theme text NOT NULL DEFAULT 'light'
);Qual a diferença entre entidades fracas e fortes na prática?
Uma entidade "fraca" (itens de linha de pedido) não pode existir sem seu pai (pedido). Imponha com FK NOT NULL mais ON DELETE CASCADE ou RESTRICT dependendo das regras de negócio.
Quantas chaves estrangeiras são muitas em uma tabela?
Não há limite rígido, mas tabelas largas com muitas FKs anuláveis frequentemente sinalizam uma tabela de junção ou máquina de estados ausente. Divida quando mais de um punhado de relacionamentos opcionais se acumulam.
Tabelas de junção devem ter sua própria PK surrogate?
Uma PK composta nas duas FKs é suficiente quando o par é único. Adicione um surrogate id apenas se ORMs ou APIs exigirem um identificador de coluna única para a linha de link em si.
Como as opcionalidades afetam o comportamento de DELETE?
ON DELETE SET NULL combina com FKs opcionais. FKs obrigatórias tipicamente usam RESTRICT ou CASCADE - escolha com base se as linhas filhas devem sobreviver à exclusão do pai.
Relacionados
- Noções Básicas de Modelagem de Dados - fluxo do conceitual ao físico
- Limites de Esquema Orientados ao Domínio - agrupamento de entidades por contexto
- Noções Básicas de Normalização - eliminação de anomalias de atualização
- Chaves Estrangeiras Ausentes - falhas de integridade sem FKs
- Explosão de Join Fan-Out - erros de cardinalidade em consultas
Versões do Stack: Esta página foi escrita para PostgreSQL 18.4 (estável 18, manutenção 17), pgvector 0.8+, PgBouncer 1.x, Patroni 3.x, e PostGIS 3.5+.