trilha

Fundamentos de Programação/03 - Sistemas Reais/Semana 09 - Banco de Dados e SQL7 min

Modelagem e Normalização

Perguntas-guia
  • 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
Chave natural é uma aposta sobre o mundo

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.
  • sqlc compra 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


Parte de Semana 09 - Banco de Dados e SQL · 00 - MOC Fundamentos de Programação

Buscar

Busca por título, seção e texto das notas