DevGuideTK
← Trilha Backend

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 sequencialUUID v4UUID v7
Inserção de 2 milhões de linhas (teste do nível Conceito)3,8 s8,9 s5,0 s
Tamanho do índice43 MB76 MB60 MB
Gerado ondeNo bancoEm qualquer lugarEm qualquer lugar
Revela volumeSim: o pedido 1500 diz quantos pedidos existemNãoRevela o horário de criação
EnumerávelSimNãoDifícil
Bom paraTabelas internas, a maioria dos casosIdentificadores públicos em sistemas antigosIdentificadores 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)
SignificadoVem do negócioNenhum
Muda?Muda mais do que parece: e-mails mudam, códigos são refeitosNunca
RecomendaçãoUNIQUE, mas não chave primáriaChave primária

Onde ficam as regras

RegraNo bancoNa aplicação
Campo obrigatórioNOT NULLMensagem amigável no formulário
Valor únicoUNIQUE, ou índice único com expressãoConferência prévia, para a mensagem
Faixa de valores, lista fechadaCHECKValidação da entrada
Referência a outra tabelaFOREIGN KEY—
Regras entre várias linhas ou tabelasÀs vezes EXCLUDE ou gatilhosNa 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çãoMudar a listaBom para
CHECK (status IN (...))Trocar a restrição, numa migraçãoListas que mudam raramente
Tabela de referência com chave estrangeiraInserir uma linhaListas que o negócio mantém, com descrição e ordem
CREATE TYPE ... AS ENUMAcrescentar é fácil; remover ou renomear, nãoListas estáveis

Como apagar

OpçãoComo funcionaCustos
Exclusão de verdadeDELETEO dado some; histórico e auditoria precisam de outro lugar
Exclusão lógicaexcluido_em timestamptz, e as consultas filtramToda consulta precisa lembrar o filtro; UNIQUE precisa de índice parcial (WHERE excluido_em IS NULL)
ArquivamentoMover para uma tabela de arquivoDuas tabelas para manter

E para as chaves estrangeiras:

AçãoQuando
ON DELETE RESTRICT (o padrão, na prática)Pai com filhos não pode ser apagado: um cliente com pedidos
ON DELETE CASCADEOs filhos não existem sem o pai: os itens de um pedido
ON DELETE SET NULLO 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

NecessidadeModelo
O valor na hora de um eventoCopiar para a linha do evento: o preço no item do pedido
Quem mudou o quê e quandoTabela de auditoria, gravada pela aplicação ou por gatilho
Consultar o estado em qualquer dataColunas valido_de e valido_ate em cada versão
O histórico como fonte da verdadeEventos, como mostra o tópico de CQRS e event sourcing

Os tipos das colunas

DadoUseEvite
Dinheironumeric(12, 2) ou inteiro em centavosfloat, double, real
Instantetimestamptztimestamp sem fuso
Data sem hora (nascimento, vencimento)datetimestamptz à meia-noite
Textotext, com CHECK de tamanho se houver regravarchar(255) por hábito
Sim ou nãobooleanchar(1) com "S" e "N"
Dados variáveis por linhajsonbColunas extra1, extra2, ...
Lista de valores com regraTabela filhaTexto separado por vírgulas

Quando desnormalizar

MotivoExemploCuidado
O valor muda e o passado precisa ficarPreço no item do pedidoNão é redundância: é outro fato
Leitura muito mais frequente que escritapedidos.totalAtualizar na mesma transação que muda os itens
Consultas analíticasTabelas largas para relatóriosMontadas a partir das normalizadas, nunca editadas à mão
Evitar junções caras em listagensNome do cliente no pedido para exibiçãoSó se medir a lentidão antes; um índice costuma resolver

Resumo da decisão

  1. bigint como chave primária; UUID v7 quando o identificador nasce fora do banco.
  2. Normalizar até a 3ª forma e desnormalizar só com motivo e fonte da verdade.
  3. Restrições no banco: NOT NULL, UNIQUE, CHECK, FOREIGN KEY; a aplicação traduz a recusa.
  4. Índice em toda chave estrangeira.
  5. numeric para dinheiro e timestamptz para instantes.
  6. 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.