flowchart TD
A[Tabela em análise] --> B{Toda célula tem valor único e atômico?}
B -- Nao --> C[Explodir grupos repetitivos em linhas separadas]
C --> D[Definir chave primaria]
B -- Sim --> D
D --> E[Relacao em 1FN]
Normalização na prática: aplicação, desnormalização e desafios
No módulo anterior você e eu construímos toda a teoria das formas normais: dependências funcionais, 1FN, 2FN, 3FN e a BCNF. Agora vou te pedir uma mudança de postura. Saber definir a 3FN não é o que cai com mais peso numa prova como a da USP; o que diferencia o candidato é a capacidade de pegar uma tabela bagunçada, real, cheia de redundância, e conduzi-la passo a passo até um esquema limpo, justificando cada decomposição com a dependência funcional que a motivou. É isso que vou treinar com você aqui. Vamos sujar as mãos: parto de uma única tabela não normalizada, aplico cada forma normal explicando o porquê, mostro o antes e o depois em SQL e em tabelas, e depois faço o caminho inverso — discuto quando vale a pena desnormalizar de propósito. Quero que, ao terminar, você consiga olhar para qualquer esquema e dizer com segurança “isto fere a 2FN por causa desta dependência parcial aqui”.
O caso que vamos atravessar inteiro
Imagine um sisteminha de pedidos. Alguém, sem pensar muito em modelagem, resolveu guardar tudo numa tabela só — a clássica “planilha que virou banco”. Repare comigo nessa tabela inicial:
| ID_Pedido | Data_Pedido | ID_Cliente | Nome_Cliente | Endereco_Cliente | ID_Produto | Nome_Produto | Qtd | Preco_Unit |
|---|---|---|---|---|---|---|---|---|
| 1 | 2023-05-01 | C1 | João Silva | Rua A, 123 | P1 | Lápis | 10 | 1.00 |
| 1 | 2023-05-01 | C1 | João Silva | Rua A, 123 | P2 | Caneta | 5 | 2.00 |
| 2 | 2023-05-02 | C2 | Maria Santos | Rua B, 456 | P1 | Lápis | 8 | 1.00 |
| 2 | 2023-05-02 | C2 | Maria Santos | Rua B, 456 | P3 | Borracha | 3 | 0.50 |
Olhe o que essa estrutura já te grita. O nome e o endereço de João aparecem duas vezes; se ele mudar de rua, eu preciso caçar todas as linhas dele — e se esquecer uma, o banco fica mentindo. Não consigo cadastrar um produto novo enquanto ninguém o pedir, porque não existe linha sem pedido. Se o pedido 2 for cancelado e suas linhas apagadas, perco junto a informação de que a borracha custa 0.50. São, respectivamente, as anomalias de atualização, inserção e exclusão que estudamos no módulo de Normalização. Elas não são detalhe estético: são defeitos que corrompem dados em produção.
Vou definir a chave dessa relação antes de qualquer coisa, porque sem chave não há análise de dependência possível.
A chave candidata da tabela bruta é o par composto \{ID\_Pedido, ID\_Produto\}. Faz sentido: um mesmo pedido aparece em várias linhas (uma por produto), e um mesmo produto aparece em vários pedidos. Só a combinação dos dois identifica uma linha única. Guarde essa chave — ela é a régua contra a qual vou medir as próximas formas normais.
-- A tabela bruta, do jeito que o estagiário criou
CREATE TABLE pedido_bruto (
id_pedido INT,
data_pedido DATE,
id_cliente VARCHAR(10),
nome_cliente VARCHAR(100),
endereco_cliente VARCHAR(200),
id_produto VARCHAR(10),
nome_produto VARCHAR(100),
qtd INT,
preco_unit NUMERIC(10,2),
PRIMARY KEY (id_pedido, id_produto)
);Passo 1: garantindo a 1FN
A primeira pergunta é sempre a mais simples: cada célula contém um único valor atômico, sem grupos repetitivos? Na nossa tabela, sim. Não há uma coluna produtos guardando “Lápis, Caneta” separados por vírgula, nem uma lista de telefones empacotada num campo. Cada linha tem exatamente um produto. Portanto a tabela já está em 1FN.
Quero que você não passe batido por esse passo achando que é trivial. A violação de 1FN é justamente o pecado mais comum de quem vem de planilha. Se a tabela original tivesse sido assim:
| ID_Pedido | Cliente | Produtos |
|---|---|---|
| 1 | João Silva | Lápis(10); Caneta(5) |
aí teríamos um grupo repetitivo embutido na célula. A correção da 1FN é “explodir” esse campo em linhas, exatamente como a nossa tabela bruta já está. O diagrama abaixo resume o teste mental que eu faço:
Passo 2: caçando dependências parciais para chegar à 2FN
Agora começa a parte que separa quem decorou de quem entendeu. A 2FN ataca um defeito específico: atributos não-chave que dependem apenas de parte de uma chave composta. Como nossa chave é o par \{ID\_Pedido, ID\_Produto\}, eu preciso interrogar cada atributo não-chave: “você precisa do pedido E do produto para ser determinado, ou parte da chave já basta?”.
Vamos uma a uma. A Data_Pedido depende só de ID_Pedido — o pedido 1 foi feito no dia 1 de maio independentemente de qual produto eu olhe na linha. O ID_Cliente, Nome_Cliente e Endereco_Cliente também dependem só de ID_Pedido. Já Nome_Produto depende só de ID_Produto — a caneta se chama caneta em qualquer pedido. E Preco_Unit? Aqui preciso de uma decisão de modelagem que a prova adora cobrar: se o preço é o preço de catálogo do produto, ele depende só de ID_Produto; mas se for o preço praticado naquela venda específica (que pode variar por promoção), ele depende do par completo. Vou adotar a leitura mais comum e didática: Preco_Unit é o preço do produto, então depende só de ID_Produto. A Qtd, essa sim, depende genuinamente do par inteiro — quantas unidades daquele produto naquele pedido.
Escrevendo as dependências funcionais com a notação rigorosa que treinamos:
ID\_Pedido \rightarrow Data\_Pedido,\ ID\_Cliente,\ Nome\_Cliente,\ Endereco\_Cliente ID\_Produto \rightarrow Nome\_Produto,\ Preco\_Unit \{ID\_Pedido,\ ID\_Produto\} \rightarrow Qtd
As duas primeiras são dependências parciais — determinadas por um pedaço da chave. É exatamente o que a 2FN proíbe. O diagrama de dependência funcional deixa o crime visível:
flowchart LR
P[ID_Pedido] --> DT[Data_Pedido]
P --> CL[ID_Cliente]
P --> NC[Nome_Cliente]
P --> EC[Endereco_Cliente]
PR[ID_Produto] --> NP[Nome_Produto]
PR --> PU[Preco_Unit]
P --> QT[Qtd]
PR --> QT
A receita da 2FN é mecânica depois que você enxergou isso: cada conjunto de atributos que depende de uma parte da chave vira uma tabela própria, tendo essa parte como chave; o que sobra depende da chave inteira e fica na tabela de associação. Decompondo, saio de uma tabela para três:
Pedidos (chave: ID_Pedido)
| ID_Pedido | Data_Pedido | ID_Cliente | Nome_Cliente | Endereco_Cliente |
|---|---|---|---|---|
| 1 | 2023-05-01 | C1 | João Silva | Rua A, 123 |
| 2 | 2023-05-02 | C2 | Maria Santos | Rua B, 456 |
Produtos (chave: ID_Produto)
| ID_Produto | Nome_Produto | Preco_Unit |
|---|---|---|
| P1 | Lápis | 1.00 |
| P2 | Caneta | 2.00 |
| P3 | Borracha | 0.50 |
Itens_Pedido (chave: {ID_Pedido, ID_Produto})
| ID_Pedido | ID_Produto | Qtd |
|---|---|---|
| 1 | P1 | 10 |
| 1 | P2 | 5 |
| 2 | P1 | 8 |
| 2 | P3 | 3 |
Repare no ganho imediato: a borracha agora existe na tabela Produtos por si só. Posso cadastrar um produto antes de qualquer venda (some a anomalia de inserção), e se o pedido 2 for apagado a borracha continua viva no catálogo (some a anomalia de exclusão). Em SQL:
CREATE TABLE produtos (
id_produto VARCHAR(10) PRIMARY KEY,
nome_produto VARCHAR(100) NOT NULL,
preco_unit NUMERIC(10,2) NOT NULL
);
CREATE TABLE pedidos (
id_pedido INT PRIMARY KEY,
data_pedido DATE NOT NULL,
id_cliente VARCHAR(10),
nome_cliente VARCHAR(100),
endereco_cliente VARCHAR(200)
);
CREATE TABLE itens_pedido (
id_pedido INT REFERENCES pedidos(id_pedido),
id_produto VARCHAR(10) REFERENCES produtos(id_produto),
qtd INT NOT NULL,
PRIMARY KEY (id_pedido, id_produto)
);Passo 3: dependências transitivas e a 3FN
A 2FN limpou as dependências parciais, mas olhe de novo a tabela Pedidos. A chave é ID_Pedido, e de fato ID_Pedido determina ID_Cliente. Só que ID_Cliente, por sua vez, determina Nome_Cliente e Endereco_Cliente. Temos uma cadeia: a chave determina o cliente, e o cliente determina os dados dele. Isso é uma dependência transitiva — um atributo não-chave (Nome_Cliente) dependendo de outro atributo não-chave (ID_Cliente) em vez de depender diretamente da chave. É precisamente o que a 3FN bane.
ID\_Pedido \rightarrow ID\_Cliente \rightarrow Nome\_Cliente,\ Endereco\_Cliente
O sintoma prático ainda está lá: o nome e o endereço de João continuam se repetindo a cada pedido que ele faz. A 2FN não resolveu isso porque o defeito é de outra natureza. Vou então quebrar a transitividade extraindo o cliente para sua própria tabela:
flowchart LR
PED[ID_Pedido] --> IDC[ID_Cliente]
IDC --> NOM[Nome_Cliente]
IDC --> END[Endereco_Cliente]
style IDC fill:#f9d
Clientes (chave: ID_Cliente)
| ID_Cliente | Nome_Cliente | Endereco_Cliente |
|---|---|---|
| C1 | João Silva | Rua A, 123 |
| C2 | Maria Santos | Rua B, 456 |
Pedidos (agora enxuta)
| ID_Pedido | Data_Pedido | ID_Cliente |
|---|---|---|
| 1 | 2023-05-01 | C1 |
| 2 | 2023-05-02 | C2 |
CREATE TABLE clientes (
id_cliente VARCHAR(10) PRIMARY KEY,
nome_cliente VARCHAR(100) NOT NULL,
endereco_cliente VARCHAR(200)
);
ALTER TABLE pedidos DROP COLUMN nome_cliente;
ALTER TABLE pedidos DROP COLUMN endereco_cliente;
ALTER TABLE pedidos
ADD CONSTRAINT fk_pedido_cliente
FOREIGN KEY (id_cliente) REFERENCES clientes(id_cliente);Agora o endereço de João vive em exatamente uma linha. Atualizar a rua dele é um único UPDATE, e a consistência é garantida pela estrutura, não pela disciplina do programador. Esse é o coração do ganho da 3FN.
Síntese das três primeiras passagens, na frase que vale para a prova: a 1FN exige atomicidade; a 2FN elimina dependências de parte da chave (só importa quando a chave é composta); a 3FN elimina dependências entre atributos não-chave. As três juntas garantem que cada atributo não-chave dependa “da chave, da chave inteira e de nada além da chave”.
Passo 4: quando a 3FN não basta — a BCNF
Nosso esquema resultante já está em 3FN, e para esse caso a 3FN é suficiente — não há determinantes problemáticos sobrando. Mas não quero que você saia daqui achando que 3FN é sempre o fim da linha, porque a prova gosta de exigir BCNF justo no caso em que ela difere da 3FN. Deixe eu construir um exemplo curto e específico para isso.
Suponha uma tabela de atendimentos onde um cliente é sempre atendido pelo mesmo gerente de conta, e cada gerente atende clientes em uma única agência:
| Cliente | Gerente | Agencia |
|---|---|---|
| João | Ana | Centro |
| Maria | Bruno | Centro |
| Pedro | Ana | Centro |
As dependências são Cliente \rightarrow Gerente e Gerente \rightarrow Agencia. A chave candidata é Cliente. Essa tabela está em 3FN — desde que consideremos Agencia como atributo primo numa leitura, mas o ponto que interessa é o determinante Gerente: ele determina Agencia sem ser uma superchave. A BCNF é mais rígida: exige que todo determinante de uma dependência funcional não trivial seja superchave. Como Gerente não é superchave, a tabela fere a BCNF. A correção decompõe assim:
| Cliente | Gerente |
|---|---|
| João | Ana |
| Maria | Bruno |
| Pedro | Ana |
| Gerente | Agencia |
|---|---|
| Ana | Centro |
| Bruno | Centro |
\text{BCNF}: \forall\ (X \rightarrow Y)\ \text{não trivial},\ X\ \text{é superchave}
flowchart TD
UN[Relacao nao normalizada] --> F1[1FN: atomicidade]
F1 --> F2[2FN: sem dependencia parcial]
F2 --> F3[3FN: sem dependencia transitiva]
F3 --> BC[BCNF: todo determinante e superchave]
BC --> OK[Esquema final]
Decomposição sem perdas e preservação de dependências
Aqui está o tópico que separa a normalização ingênua da normalização correta, e que cai em prova de forma sorrateira. Quando eu quebro uma tabela em duas, preciso garantir duas propriedades, e elas não são automáticas.
A primeira é a decomposição sem perdas (lossless join). Significa que, se eu juntar de volta as tabelas decompostas com um JOIN, recupero exatamente a tabela original — nem uma linha a mais (linhas espúrias), nem uma a menos. O teorema é direto: uma decomposição de R em R_1 e R_2 é sem perdas se o atributo comum entre elas for chave de pelo menos uma das duas. Formalmente, R_1 \cap R_2 \rightarrow R_1 ou R_1 \cap R_2 \rightarrow R_2.
Veja que na nossa extração de Clientes isso vale: o atributo comum entre Pedidos e Clientes é ID_Cliente, e ele é chave de Clientes. Logo o join reconstrói o original sem inventar linhas:
-- Reconstruindo a tabela original a partir das decompostas:
-- se a decomposicao for sem perdas, este resultado bate
-- linha a linha com a tabela bruta inicial.
SELECT pe.id_pedido, pe.data_pedido,
c.id_cliente, c.nome_cliente, c.endereco_cliente,
i.id_produto, pr.nome_produto, i.qtd, pr.preco_unit
FROM pedidos pe
JOIN clientes c ON pe.id_cliente = c.id_cliente
JOIN itens_pedido i ON pe.id_pedido = i.id_pedido
JOIN produtos pr ON i.id_produto = pr.id_produto;Por que isso é uma armadilha? Porque é possível decompor de um jeito que parece razoável e perde informação. Se você partir uma relação por um atributo que não é chave de nenhum dos lados, o join de volta produz linhas que nunca existiram — combinações falsas. Numa questão de prova, sempre verifique mentalmente: “o campo que liga as duas tabelas é chave em algum dos lados?”. Se não for, desconfie da decomposição.
A segunda propriedade é a preservação de dependências. Quero que toda dependência funcional do esquema original ainda possa ser verificada olhando uma única tabela, sem precisar fazer join para checar uma restrição. Quando isso falha, eu fico impedido de impor a regra de negócio com uma simples constraint local — teria que validar a cada inserção juntando tabelas, o que é caro e frágil. O detalhe fino, e que separa BCNF de 3FN, é este:
| Propriedade | 3FN garante? | BCNF garante? |
|---|---|---|
| Decomposição sem perdas | Sim | Sim |
| Preservação de dependências | Sim | Nem sempre |
| Eliminação de toda redundância por DF | Não totalmente | Sim |
Esse é o trade-off teórico fundamental: a 3FN sempre consegue uma decomposição que é, ao mesmo tempo, sem perdas e preservadora de dependências. A BCNF é mais limpa contra redundância, mas em alguns esquemas você é forçado a escolher entre BCNF e preservar dependências — não dá para ter ambos. Por isso muitos projetos param deliberadamente na 3FN.
Desnormalização controlada: o caminho de volta, de propósito
Até agora caminhei numa direção: dividir para conquistar. Mas seria desonesto da minha parte te deixar com a ideia de que mais normalização é sempre melhor. Em sistemas reais, a normalização cobra um preço — joins. Cada tabela que eu separei é um join a mais que toda consulta de leitura precisa pagar. Numa tela de e-commerce que mostra milhares de produtos por segundo, juntar Produtos, Categorias e Fornecedores a cada requisição pode ser o gargalo entre uma página rápida e um cliente que desiste.
Desnormalizar é reintroduzir redundância de propósito, com os olhos abertos, para acelerar leitura. Não é o erro do estagiário do início — é uma decisão de engenharia tomada depois de medir. A diferença é toda de intenção e de controle. Veja o contraste:
| Aspecto | Esquema normalizado | Esquema desnormalizado |
|---|---|---|
| Redundância | Mínima | Introduzida deliberadamente |
| Integridade | Garantida pela estrutura | Depende de processo extra |
| Velocidade de leitura | Mais joins, mais lenta | Menos joins, mais rápida |
| Velocidade de escrita | Atualização em um ponto | Atualização em vários pontos |
| Quando usar | Padrão, sistemas transacionais | Leitura intensa, relatórios |
Um exemplo concreto: para a vitrine, eu posso manter o esquema normalizado como fonte de verdade para vendas, mas criar uma tabela achatada só para exibição rápida, combinando o que viria de três tabelas:
A leitura dessa tabela é um SELECT sem nenhum join. O custo? Quando o nome de uma categoria muda, eu tenho que propagar a mudança para todas as linhas de produto_vitrine. Esse é o trade-off central da desnormalização: leitura barata em troca de escrita cara e risco de inconsistência.
A forma profissional de pagar esse custo sem ficar à mercê do programador é automatizar a propagação. Em PostgreSQL, uma das ferramentas favoritas é a visão materializada, que armazena o resultado de um join e você refresca quando quiser — você ganha a desnormalização sem manter cópias manuais:
CREATE MATERIALIZED VIEW mv_produto_vitrine AS
SELECT pr.id_produto, pr.nome_produto,
cat.nome_categoria, forn.nome_fornecedor,
pr.preco_unit, est.estoque
FROM produtos pr
JOIN categorias cat ON pr.id_categoria = cat.id_categoria
JOIN fornecedores forn ON pr.id_fornecedor = forn.id_fornecedor
JOIN estoque est ON pr.id_produto = est.id_produto;
-- Atualiza o "cache" desnormalizado sob demanda:
REFRESH MATERIALIZED VIEW mv_produto_vitrine;A outra ferramenta é o trigger, que mantém a coluna redundante sincronizada no exato instante em que a fonte muda:
CREATE OR REPLACE FUNCTION sincroniza_categoria()
RETURNS TRIGGER AS $$
BEGIN
UPDATE produto_vitrine
SET nome_categoria = NEW.nome_categoria
WHERE id_produto IN (
SELECT id_produto FROM produtos
WHERE id_categoria = NEW.id_categoria
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_sincroniza_categoria
AFTER UPDATE ON categorias
FOR EACH ROW EXECUTE FUNCTION sincroniza_categoria();Regra de ouro que eu quero que você leve: normalize primeiro, desnormalize por exceção e com medição. Comece sempre com um esquema em 3FN ou BCNF. Só desnormalize um ponto específico depois de identificar, com dados de desempenho na mão, que aquele join é de fato o gargalo. E quando desnormalizar, automatize a consistência com visões materializadas ou triggers. Desnormalizar “por precaução”, antes de medir, costuma trazer todos os problemas de redundância e nenhum ganho real.
Desafios e trade-offs no mundo real
Quero fechar com os desafios honestos da normalização, porque a prova frequentemente cobra justamente o senso crítico de quem entende que normalização não é dogma.
O primeiro desafio é de complexidade de análise. Identificar corretamente todas as dependências funcionais de um sistema grande é difícil, e exige conhecer o domínio de negócio, não só as regras formais. Aquela decisão que tomei lá atrás sobre o Preco_Unit — preço de catálogo ou preço da venda? — não se resolve com teoria; se resolve perguntando a quem entende do negócio. Modelar errado a dependência leva a um esquema que está formalmente normalizado mas semanticamente errado.
O segundo é o custo de desempenho em leitura. Quanto mais normalizado, mais tabelas, mais joins, mais I/O em consultas que precisam reunir tudo. Para cargas analíticas e relatórios, esse custo é real e às vezes proibitivo — é por isso que data warehouses costumam adotar esquemas propositadamente desnormalizados, como o esquema estrela.
O terceiro é o equilíbrio evolutivo. O esquema certo hoje pode não ser o certo daqui a um ano, quando os padrões de acesso mudam. A abordagem madura é híbrida: o grosso do sistema normalizado, garantindo integridade, com ilhas desnormalizadas e controladas onde a medição justifica. Vale também lembrar — sem entrar no mérito — que os bancos NoSQL orientados a documentos partem de outra premissa, frequentemente desnormalizando para escalar leitura e aceitando consistência eventual; são uma resposta diferente ao mesmo trade-off que você acabou de estudar.
flowchart TD
REQ[Requisito de negocio] --> NORM[Modelar normalizado ate 3FN/BCNF]
NORM --> MED{Ha gargalo de leitura medido?}
MED -- Nao --> FIM[Manter normalizado]
MED -- Sim --> DES[Desnormalizar ponto especifico]
DES --> AUTO[Automatizar consistencia: view materializada ou trigger]
AUTO --> FIM
Síntese para a prova. A normalização aplicada é um processo passo a passo guiado por dependências funcionais: 1FN garante atomicidade; 2FN remove dependências parciais de chave composta; 3FN remove dependências transitivas entre atributos não-chave; BCNF exige que todo determinante seja superchave. Toda decomposição precisa ser sem perdas — o atributo de junção deve ser chave de pelo menos um dos lados — e idealmente preservar dependências; lembre que a 3FN sempre consegue as duas propriedades, mas a BCNF pode forçar a abrir mão da preservação de dependências, e por isso muitos projetos param na 3FN. As anomalias de inserção, atualização e exclusão são a motivação prática de tudo isso. A desnormalização controlada é a reintrodução deliberada de redundância para acelerar leitura, justificada por medição, e mantida consistente com visões materializadas ou triggers — nunca confunda desnormalização planejada com tabela mal modelada. O lema final: normalize por princípio, desnormalize por exceção e sempre com a integridade sob controle.