Modelagem e normalização
Tabelas, chaves e formas normais, e quando desnormalizar de propósito.
As decisões de modelagem que mais aparecem são seis: o tipo da chave primária, onde ficam as regras, como apagar, como guardar o histórico, os tipos das colunas e quando desnormalizar.
O tipo da chave primária
bigint sequencial | UUID v4 | UUID v7 | |
|---|---|---|---|
| Inserção de 2 milhões de linhas (teste do nível Conceito) | 3,8 s | 8,9 s | 5,0 s |
| Tamanho do índice | 43 MB | 76 MB | 60 MB |
| Gerado onde | No banco | Em qualquer lugar | Em qualquer lugar |
| Revela volume | Sim: o pedido 1500 diz quantos pedidos existem | Não | Revela o horário de criação |
| Enumerável | Sim | Não | Difícil |
| Bom para | Tabelas internas, a maioria dos casos | Identificadores públicos em sistemas antigos | Identificadores gerados fora do banco, sistemas distribuídos |
O bigint é o padrão mais eficiente. O UUID v7 é a escolha quando o identificador precisa ser gerado fora do banco (no cliente, em outro serviço) ou não pode revelar o volume. O Postgres 18 gera UUID v7 com uuidv7(); versões anteriores precisam de uma extensão ou da geração na aplicação. O UUID v4 só se justifica onde o v7 não está disponível.
Uma combinação comum: bigint como chave primária, para as junções e os índices, e um identificador público aleatório numa coluna UNIQUE, para as URLs. Lembre do tópico de autorização: identificador difícil de adivinhar não substitui a conferência do dono.
Chave natural ou substituta
| Chave natural (CPF, e-mail, código do produto) | Chave substituta (id gerado) | |
|---|---|---|
| Significado | Vem do negócio | Nenhum |
| Muda? | Muda mais do que parece: e-mails mudam, códigos são refeitos | Nunca |
| Recomendação | UNIQUE, mas não chave primária | Chave primária |
Onde ficam as regras
| Regra | No banco | Na aplicação |
|---|---|---|
| Campo obrigatório | NOT NULL | Mensagem amigável no formulário |
| Valor único | UNIQUE, ou índice único com expressão | Conferência prévia, para a mensagem |
| Faixa de valores, lista fechada | CHECK | Validação da entrada |
| Referência a outra tabela | FOREIGN KEY | — |
| Regras entre várias linhas ou tabelas | Às vezes EXCLUDE ou gatilhos | Na transação |
A regra prática: o banco garante, a aplicação explica. No teste do nível Conceito, só a conferência na aplicação deixou 18 duplicatas em 20 cadastros simultâneos. A aplicação traduz a recusa do banco numa resposta, como mostra a Implementação.
Lista fechada: CHECK, tabela ou enum
| Opção | Mudar a lista | Bom para |
|---|---|---|
CHECK (status IN (...)) | Trocar a restrição, numa migração | Listas que mudam raramente |
| Tabela de referência com chave estrangeira | Inserir uma linha | Listas que o negócio mantém, com descrição e ordem |
CREATE TYPE ... AS ENUM | Acrescentar é fácil; remover ou renomear, não | Listas estáveis |
Como apagar
| Opção | Como funciona | Custos |
|---|---|---|
| Exclusão de verdade | DELETE | O dado some; histórico e auditoria precisam de outro lugar |
| Exclusão lógica | excluido_em timestamptz, e as consultas filtram | Toda consulta precisa lembrar o filtro; UNIQUE precisa de índice parcial (WHERE excluido_em IS NULL) |
| Arquivamento | Mover para uma tabela de arquivo | Duas tabelas para manter |
E para as chaves estrangeiras:
| Ação | Quando |
|---|---|
ON DELETE RESTRICT (o padrão, na prática) | Pai com filhos não pode ser apagado: um cliente com pedidos |
ON DELETE CASCADE | Os filhos não existem sem o pai: os itens de um pedido |
ON DELETE SET NULL | O filho continua, sem o pai: o responsável por um chamado que saiu da empresa |
A exclusão lógica parece segura e cobra caro: cada consulta esquecida mostra dados apagados, e a LGPD pode exigir a exclusão de verdade dos dados pessoais. Use-a quando houver um motivo concreto, como a lixeira que o usuário vê.
Como guardar o histórico
| Necessidade | Modelo |
|---|---|
| O valor na hora de um evento | Copiar para a linha do evento: o preço no item do pedido |
| Quem mudou o quê e quando | Tabela de auditoria, gravada pela aplicação ou por gatilho |
| Consultar o estado em qualquer data | Colunas valido_de e valido_ate em cada versão |
| O histórico como fonte da verdade | Eventos, como mostra o tópico de CQRS e event sourcing |
Os tipos das colunas
| Dado | Use | Evite |
|---|---|---|
| Dinheiro | numeric(12, 2) ou inteiro em centavos | float, double, real |
| Instante | timestamptz | timestamp sem fuso |
| Data sem hora (nascimento, vencimento) | date | timestamptz à meia-noite |
| Texto | text, com CHECK de tamanho se houver regra | varchar(255) por hábito |
| Sim ou não | boolean | char(1) com "S" e "N" |
| Dados variáveis por linha | jsonb | Colunas extra1, extra2, ... |
| Lista de valores com regra | Tabela filha | Texto separado por vírgulas |
Quando desnormalizar
| Motivo | Exemplo | Cuidado |
|---|---|---|
| O valor muda e o passado precisa ficar | Preço no item do pedido | Não é redundância: é outro fato |
| Leitura muito mais frequente que escrita | pedidos.total | Atualizar na mesma transação que muda os itens |
| Consultas analíticas | Tabelas largas para relatórios | Montadas a partir das normalizadas, nunca editadas à mão |
| Evitar junções caras em listagens | Nome do cliente no pedido para exibição | Só se medir a lentidão antes; um índice costuma resolver |
Resumo da decisão
bigintcomo chave primária; UUID v7 quando o identificador nasce fora do banco.- Normalizar até a 3ª forma e desnormalizar só com motivo e fonte da verdade.
- Restrições no banco:
NOT NULL,UNIQUE,CHECK,FOREIGN KEY; a aplicação traduz a recusa. - Índice em toda chave estrangeira.
numericpara dinheiro etimestamptzpara instantes.- Exclusão de verdade por padrão; exclusão lógica só com motivo.
O próximo nível mostra o esquema da loja e o código que traduz as recusas do banco nas cinco linguagens.
Prompts para aprofundar
Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.
Modelar as tabelas do meu sistema
Quero desenhar as tabelas de um sistema novo. Pergunte o que o sistema faz, quais são as entidades principais, como elas se relacionam e quais regras de negócio não podem ser violadas. Depois, comigo, passo a passo: 1. Liste as tabelas e as chaves primárias. 2. Defina os relacionamentos e as chaves estrangeiras, com a ação de exclusão de cada uma. 3. Escolha os tipos das colunas. 4. Escreva as restrições NOT NULL, UNIQUE e CHECK. 5. Aponte onde desnormalizar de propósito, e qual é a fonte da verdade de cada cópia. No final, escreva o SQL completo para Postgres.
Exclusão lógica, histórico e auditoria
Preciso decidir como o meu sistema apaga dados e guarda o histórico. Pergunte quais dados o sistema guarda, quem precisa ver o histórico e se há exigências legais, como a LGPD. Compare para o meu caso: 1. Exclusão de verdade, exclusão lógica e arquivamento. 2. Os cuidados da exclusão lógica com consultas e índices únicos. 3. Tabela de auditoria, versões com validade e eventos. 4. Como atender a um pedido de exclusão de dados pessoais.
Escolher as chaves primárias
Quero escolher o tipo das chaves primárias do meu sistema. Pergunte qual banco eu uso, se os identificadores aparecem em URLs, se são gerados fora do banco e qual é o volume esperado. Compare para o meu caso bigint sequencial, UUID v4, UUID v7 e chaves naturais quanto a desempenho de inserção, tamanho de índice, exposição de volume e geração fora do banco. Recomende uma escolha e explique como combinar uma chave interna com um identificador público.