Fundamentos de Programação/03 - Sistemas Reais/Semana 09 - Banco de Dados e SQL7 min
Modelagem e Normalização
- 1NF, 2NF, 3NF: qual anomalia cada uma elimina?
- Chave primária, estrangeira, natural, surrogate: quando usar cada tipo?
- Como modelar um relacionamento muitos-para-muitos, e onde os atributos da relação moram?
- Quando desnormalizar é decisão técnica e quando é preguiça? (ver Denormalization)
Conceito
Normalizar é organizar o schema para que cada fato seja armazenado exatamente uma vez. Cada forma normal elimina uma classe específica de anomalia:
| Forma | Regra | Anomalia que elimina |
|---|---|---|
| 1NF | valores atômicos; sem grupos repetidos | não dá para consultar nem indexar "SP,RJ,MG" numa coluna |
| 2NF | 1NF + nenhum atributo depende de parte da chave composta | atualizar em um lugar e não em outro |
| 3NF | 2NF + nenhum atributo depende de outro não-chave | dado derivado desatualizado |
As três anomalias que a normalização ataca, e vale nomeá-las:
- de inserção — não consigo cadastrar um cliente sem inventar um pedido
- de atualização — o mesmo endereço em 40 linhas; mudo 39
- de exclusão — apago o último pedido e perco o cadastro do cliente
Chaves:
| Tipo | O que é | Quando dói |
|---|---|---|
| primária | identifica a linha | — |
| estrangeira | referencia outra tabela | sem ela, integridade é responsabilidade do código (e falha) |
| natural | atributo do mundo real (CPF, e-mail, ISBN) | quando muda — e ele muda |
| surrogate | inventada, sem significado (serial, UUID) | precisa de índice único na natural para evitar duplicata |
CPF parece imutável até você descobrir CPF digitado errado, pessoa jurídica, estrangeiro sem CPF, e a necessidade de corrigir. E-mail muda. Placa de carro muda.
A regra prática: surrogate como PK, natural como UNIQUE. Você ganha estabilidade das
referências e mantém a garantia de unicidade.
Muitos-para-muitos exige uma terceira tabela. E o ponto que quase todo mundo descobre
tarde: se a relação tem atributos, ela é uma entidade. pedido_item com quantidade e
preco_unitario não é "tabela de ligação" — é a entidade item de pedido, e o preço mora
ali porque preço de catálogo muda e o preço da venda não pode mudar retroativamente.
Quando desnormalizar — e a diferença entre decisão técnica e preguiça:
| Decisão técnica | Preguiça |
|---|---|
leitura mediu-se lenta, com EXPLAIN na mão |
"join é lento" por fé |
| existe plano de como manter a cópia consistente | espera-se que ninguém mude o original |
| a inconsistência tem janela aceitável e documentada | descobre-se a divergência em produção |
Desnormalização é cache, e todo cache tem o problema de invalidação.
Na prática
O schema mínimo do entregável desta semana, com as decisões visíveis:
CREATE TABLE cliente (
id BIGSERIAL PRIMARY KEY, -- surrogate: estável
cpf CHAR(11) NOT NULL UNIQUE, -- natural: UNIQUE, não PK
email TEXT NOT NULL UNIQUE,
nome TEXT NOT NULL,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE endereco ( -- 3NF: cidade/uf não moram em cliente
id BIGSERIAL PRIMARY KEY,
cliente_id BIGINT NOT NULL REFERENCES cliente(id) ON DELETE CASCADE,
cep CHAR(8) NOT NULL,
logradouro TEXT NOT NULL,
principal BOOLEAN NOT NULL DEFAULT false
);
CREATE TABLE produto (
id BIGSERIAL PRIMARY KEY,
sku TEXT NOT NULL UNIQUE,
nome TEXT NOT NULL,
preco BIGINT NOT NULL CHECK (preco >= 0) -- centavos: inteiro, nunca float
);
CREATE TABLE pedido (
id BIGSERIAL PRIMARY KEY,
cliente_id BIGINT NOT NULL REFERENCES cliente(id),
status TEXT NOT NULL CHECK (status IN ('rascunho','pago','enviado','cancelado')),
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- N:N COM atributos: é uma entidade, não uma "tabela de ligação"
CREATE TABLE pedido_item (
pedido_id BIGINT NOT NULL REFERENCES pedido(id) ON DELETE CASCADE,
produto_id BIGINT NOT NULL REFERENCES produto(id),
quantidade INT NOT NULL CHECK (quantidade > 0),
preco_unitario BIGINT NOT NULL, -- CÓPIA do preço na hora da venda: correto, não redundante
PRIMARY KEY (pedido_id, produto_id)
);
O preco_unitario em pedido_item parece violar a 3NF (duplica produto.preco) e não
viola: são fatos diferentes. produto.preco é "quanto custa hoje"; pedido_item.preco_unitario
é "quanto custou naquela venda". Confundir os dois faz o histórico de vendas mudar quando
alguém reajusta o catálogo.
Esse é o teste para distinguir redundância de fato distinto: se o original mudar, essa cópia deve mudar também? Se sim, é redundância. Se não, é outro fato.
Em Go
Go não tem ORM na stdlib, e o schema vive em migrations, não em structs. Isso é uma diferença cultural relevante em relação a Django/Rails/Hibernate: em Go, o schema é a fonte da verdade e o código se adapta.
Ferramentas de migration: golang-migrate, goose, atlas. Nenhuma na stdlib.
As duas direções possíveis:
| Direção | Ferramenta | Consequência |
|---|---|---|
| schema → tipos Go | sqlc |
gera struct e função a partir do SQL; valida a query contra o schema em build |
| struct Go → schema | GORM (AutoMigrate) | conveniente, e o schema passa a ser efeito colateral do código |
sqlc é a escolha idiomática hoje justamente porque pega no build o erro que doeria em
runtime: coluna renomeada, tipo trocado, query inválida.
Dinheiro é int64 de centavos, nunca float64. Isso é regra em qualquer linguagem, e
em Go a tipagem ajuda a impor:
type Centavos int64 // a unidade está no tipo ([[1. Code Smells]])
NUMERIC do Postgres não tem tipo nativo em Go — vem como []byte ou string no
database/sql. Mais um motivo para inteiro.
Tags de struct são contrato serializado, e a divergência entre a tag e a coluna é um erro só visível na integração (2. Teste de Integração):
type Cliente struct {
ID int64 `db:"id" json:"id"`
CPF string `db:"cpf" json:"-"` // não vaza CPF na API
Email string `db:"email" json:"email"`
}
Respostas às perguntas-guia
1. 1NF, 2NF, 3NF: qual anomalia cada uma elimina?
1NF elimina o valor não atômico (que impede consulta e índice). 2NF elimina a dependência parcial de chave composta. 3NF elimina a dependência transitiva. Todas atacam o mesmo problema de fundo: o mesmo fato armazenado em mais de um lugar.
2. Chave primária, estrangeira, natural, surrogate: quando usar cada tipo?
Surrogate como PK (estabilidade), natural como UNIQUE (garantia), FK sempre que houver
relação — integridade referencial no banco é a única que não depende de ninguém lembrar.
Em Go: BIGSERIAL mapeia para int64; UUID para string ou [16]byte. UUID v7 é
ordenável por tempo, o que preserva localidade de índice (5. Índices) — UUID v4 aleatório
fragmenta o B-tree.
3. Como modelar um relacionamento muitos-para-muitos, e onde os atributos da relação moram?
Terceira tabela com FK para as duas pontas e PK composta. Os atributos da relação moram nela — e a presença deles significa que ela é uma entidade de domínio, com nome próprio.
4. Quando desnormalizar é decisão técnica e quando é preguiça?
Técnica: você mediu, tem plano de consistência, e a janela de divergência é aceitável e documentada. Preguiça: você supôs que join é lento e não tem plano.
Em Go: o plano de consistência costuma ser uma transação cobrindo as duas escritas (6. Transações e ACID) ou um job de reconciliação. Sem um dos dois, a cópia divergiu.
Trade-offs
Do conceito:
- Normalizar compra consistência e integridade; cobra joins (1. Joins) e mais tabelas.
- Desnormalizar compra leitura rápida e cobra invalidação — que é o problema difícil de qualquer cache.
- Chave natural compra menos joins e cobra instabilidade quando o mundo muda.
- FK compra integridade garantida e cobra rigidez de ordem de escrita e custo em carga massiva.
Em Go:
- Schema em migrations compra controle e cobra ferramenta externa.
sqlccompra verificação em build e cobra um passo de geração.- Sem ORM por padrão: mais código, menos surpresa — e nenhum lazy loading acidental (7. Problema N+1).
Comandos e consultas essenciais
-- integridade declarada, não confiada ao código
ALTER TABLE pedido ADD CONSTRAINT fk_cliente
FOREIGN KEY (cliente_id) REFERENCES cliente(id);
-- unicidade da chave natural, com surrogate como PK
CREATE UNIQUE INDEX uq_cliente_cpf ON cliente(cpf);
-- unicidade PARCIAL: só um endereço principal por cliente (Postgres)
CREATE UNIQUE INDEX uq_end_principal ON endereco(cliente_id) WHERE principal;
-- domínio fechado sem enum
ALTER TABLE pedido ADD CONSTRAINT ck_status
CHECK (status IN ('rascunho','pago','enviado','cancelado'));
-- auditar violação de 3NF: o mesmo fato em dois lugares divergiu?
SELECT count(*) FROM pedido_item i JOIN produto p ON p.id = i.produto_id
WHERE i.preco_unitario <> p.preco; -- aqui divergir é ESPERADO (fatos distintos)
Relacionado
- 1. Joins — normalizar cria joins; é o custo assumido
- 5. Índices — a chave natural quase sempre precisa de índice único
- 6. Transações e ACID — o que mantém a desnormalização consistente
- 1. Code Smells — semana 6,
Centavoscontra primitive obsession
Parte de Semana 09 - Banco de Dados e SQL · 00 - MOC Fundamentos de Programação