Pular para o conteúdo
10 min de leitura

Modelagem relacional: normalização até a 3FN sem susto

Por Equipe Tech do Sonne ·

Entenda 1FN, 2FN e 3FN com exemplos concretos de tabelas, chaves e dependências funcionais, e quando desnormalizar sem quebrar a integridade.

Neste artigo

Toda aplicação que guarda dados por muito tempo acaba encontrando o mesmo fantasma: informação repetida em vários lugares que, um dia, deixa de concordar entre si. O cliente mudou de endereço em um pedido, mas os outros três pedidos ainda mostram o endereço antigo. O nome da categoria foi corrigido numa linha e permaneceu errado em outras cem. Esse tipo de inconsistência raramente nasce de um bug de código; nasce de uma decisão de modelagem tomada meses antes, quando ninguém parou para perguntar onde cada fato deveria morar.

A normalização é justamente o conjunto de regras que responde a essa pergunta. Ela não é um ritual acadêmico nem uma checklist para impressionar o revisor: é um método para colocar cada fato em um único lugar, de modo que atualizá-lo signifique tocar em uma linha só. As três primeiras formas normais — 1FN, 2FN e 3FN — resolvem a esmagadora maioria dos problemas reais de um schema transacional. Ir além disso costuma ser exagero para a maioria dos sistemas.

Neste artigo vamos percorrer as três formas normais com exemplos tangíveis de tabelas, entender o conceito de dependência funcional que sustenta tudo, e discutir por que a desnormalização deliberada tem seu lugar — desde que você saiba exatamente o que está trocando.

O que é uma dependência funcional#

Antes de falar em formas normais, é preciso um vocabulário. Uma dependência funcional existe quando o valor de uma coluna (ou conjunto de colunas) determina, sem ambiguidade, o valor de outra. Dizemos que cep determina cidade porque, dado um CEP, existe uma e só uma cidade correspondente. Escrevemos isso como cep -> cidade. Já o contrário não vale: uma cidade tem muitos CEPs, então cidade -> cep é falso.

A chave primária de uma tabela é, por definição, aquilo que determina funcionalmente todas as outras colunas da linha. Em uma tabela de usuarios com chave id, temos id -> nome, id -> email, id -> criado_em. Saber o id é saber tudo sobre aquela linha. O trabalho da normalização é garantir que as dependências funcionais de uma tabela apontem sempre e apenas para fora da chave, nunca entre colunas comuns e nunca a partir de um pedaço só da chave.

Quando uma dependência funcional aponta para o lugar errado, ela sinaliza que existe um fato escondido que merecia tabela própria. Reconhecer essas dependências é o coração da modelagem. Tudo o que segue são apenas nomes formais para padrões de dependência mal colocada.

Primeira forma normal: cada célula guarda um valor atômico#

A 1FN exige que cada coluna contenha um único valor indivisível e que não existam grupos repetidos de colunas. Parece óbvio, mas é violada o tempo todo na prática.

O caso clássico é a coluna que guarda uma lista. Imagine uma tabela pedidos com uma coluna produtos que contém o texto "caneta, caderno, borracha". Isso quebra a 1FN. Para saber quantos pedidos incluíram caderno você seria obrigado a fazer busca por substring, o que é lento, frágil e não usa índice de forma sadia. Pior: você não tem onde guardar a quantidade de cada item, nem o preço no momento da compra.

Outra violação comum são os grupos repetidos: colunas como produto_1, produto_2, produto_3. Além de impor um limite artificial (e se o pedido tiver quatro itens?), esse desenho torna qualquer consulta uma bagunça de OR entre colunas. A pergunta "quais pedidos contêm o produto X" vira um pesadelo.

A solução para ambos é a mesma: criar uma tabela filha itens_pedido, com uma linha por item, contendo pedido_id, produto_id, quantidade e preco_unitario. Cada célula volta a guardar um valor atômico, cada item ganha seus próprios atributos e as consultas passam a ser um JOIN limpo. A relação "um pedido tem muitos itens" ganha a estrutura que sempre foi: uma tabela dedicada ao lado "muitos".

Vale uma ressalva moderna. Bancos como o PostgreSQL oferecem tipos de array e colunas jsonb, e é tentador achar que eles revogam a 1FN. Não revogam. Eles são ótimos para dados que você trata como um bloco opaco — configurações, payloads de eventos, atributos verdadeiramente heterogêneos. Mas no momento em que você precisa consultar, agrupar ou impor integridade sobre os elementos internos, você está tratando aquilo como relação, e a relação pede tabela.

Segunda forma normal: nada depende de parte da chave#

A 2FN só ganha relevância quando a chave primária é composta, ou seja, formada por mais de uma coluna. Se a sua chave é uma coluna só, a tabela que já está em 1FN está automaticamente em 2FN, e você pode pular direto para a 3FN.

A 2FN exige que toda coluna que não faz parte da chave dependa da chave inteira, e não de apenas um pedaço dela. Chamamos o problema de dependência parcial.

Volte à tabela itens_pedido, mas suponha que ela tenha chave composta por (pedido_id, produto_id) e carregue também a coluna nome_produto. Aqui surge a violação: o nome do produto depende só de produto_id, não do par inteiro. O mesmo produto aparecerá com o mesmo nome em milhares de itens de pedidos diferentes. Se o nome for corrigido, você teria que atualizar todas essas linhas — e se esquecer uma, cria inconsistência.

A dependência parcial produto_id -> nome_produto está gritando que nome_produto pertence a outra tabela: a tabela produtos, cuja chave é justamente produto_id. Em itens_pedido fica apenas a referência produto_id como chave estrangeira. O nome vive em um lugar só, e um JOIN o traz quando necessário.

Repare no princípio: a coluna que depende de um fragmento da chave está, na verdade, descrevendo a entidade daquele fragmento, não a linha inteira. Movê-la para a tabela daquela entidade é o conserto natural. A 2FN é, em essência, a instrução para separar as descrições das entidades das linhas de relacionamento entre elas.

Terceira forma normal: nada depende de coluna que não é chave#

A 3FN ataca o último tipo comum de dependência mal colocada: a dependência transitiva, quando uma coluna comum depende de outra coluna comum, e não diretamente da chave.

A 3FN exige que toda coluna fora da chave dependa da chave, da chave inteira, e de nada mais além da chave. A frase clássica resume: cada atributo depende "da chave, da chave toda e só da chave".

Considere uma tabela funcionarios com chave id e as colunas nome, departamento_id e departamento_nome. Temos id -> departamento_id, o que é correto, mas também temos departamento_id -> departamento_nome. Ou seja, o nome do departamento não depende diretamente do funcionário; depende do departamento, que por sua vez depende do funcionário. Essa cadeia id -> departamento_id -> departamento_nome é a dependência transitiva.

O sintoma é o mesmo padrão de repetição e risco de inconsistência: todos os funcionários do mesmo departamento carregam o nome do departamento repetido, e renomear o departamento exige varrer a tabela inteira. Além disso, um departamento sem nenhum funcionário simplesmente não existiria na base, porque não haveria linha para guardá-lo — a chamada anomalia de exclusão.

O conserto: uma tabela departamentos com id e nome, e em funcionarios fica apenas departamento_id como chave estrangeira. O nome do departamento passa a morar em uma linha única. Renomear é um UPDATE numa linha só. Um departamento vazio existe tranquilamente na sua própria tabela. A dependência transitiva foi eliminada porque o fato "departamento tal se chama assim" ganhou casa própria.

Um roteiro mental para aplicar#

Na prática, você não precisa recitar as definições. Ao desenhar uma tabela, faça três perguntas para cada coluna que não é chave. Ela guarda um valor único, sem listas nem grupos repetidos? Se sim, 1FN. Ela descreve a linha inteira, e não só um pedaço da chave composta? Se sim, 2FN. Ela depende diretamente da chave, e não de outra coluna comum ao lado? Se sim, 3FN. Qualquer "não" aponta um fato que quer sua própria tabela.

O papel das chaves estrangeiras#

A normalização espalha os fatos por várias tabelas, e é a chave estrangeira que os costura de volta com integridade garantida. Quando itens_pedido.produto_id referencia produtos.id, o banco recusa a inserção de um item que aponte para um produto inexistente. Essa é a diferença entre "espero que os dados estejam consistentes" e "o banco não me deixa torná-los inconsistentes".

As chaves estrangeiras também definem o comportamento na exclusão. Você decide se apagar um produto deve ser bloqueado enquanto houver itens que o referenciam (ON DELETE RESTRICT), se deve apagar em cascata os itens (ON DELETE CASCADE), ou se deve anular a referência (ON DELETE SET NULL). Essa escolha é parte da modelagem tanto quanto a estrutura das tabelas, porque codifica a regra de negócio sobre o que acontece quando uma entidade some.

Sem chaves estrangeiras, a normalização vira uma promessa sem fiador. As tabelas ficam separadas, mas nada impede que as referências apontem para o vazio. É comum ver sistemas que dividem os dados corretamente e depois esquecem de declarar as restrições, colhendo o pior dos dois mundos: a complexidade dos JOIN sem a garantia de integridade.

Quando desnormalizar de propósito#

Normalizar é o padrão, mas não é dogma. A 3FN otimiza para escrita consistente e ausência de anomalias; ela cobra esse benefício em JOIN no momento da leitura. Em sistemas com leitura pesada, latência apertada e um mesmo conjunto de JOIN repetido milhões de vezes, pode fazer sentido guardar um dado redundante de propósito. Isso é desnormalização, e a palavra-chave é "de propósito".

Um exemplo legítimo: uma tabela de pedidos que guarda total_pedido como coluna, em vez de recalculá-lo somando os itens a cada leitura. O total é derivável, logo redundante, logo uma violação técnica. Mas se o relatório de vendas roda a toda hora e somar itens é caro, materializar o total pode ser a escolha certa. Outro exemplo é guardar o nome_produto no item do pedido — mas aqui por uma razão sutil e correta: você quer preservar o nome no momento da compra, mesmo que o produto seja renomeado depois. Isso não é redundância; é um fato histórico diferente do nome atual, e ele merece mesmo ser copiado.

A diferença entre desnormalização sadia e schema bagunçado está na intenção e na disciplina. Ao guardar um dado redundante você assume a responsabilidade de mantê-lo em dia — normalmente com uma transação que atualiza a fonte e a cópia juntas, ou com um gatilho, ou com um recálculo periódico. Você trocou consistência automática por velocidade de leitura, e precisa pagar essa conta explicitamente. Desnormalizar sem esse cuidado é só criar as anomalias que a normalização existia para evitar.

A regra prática é começar normalizado até a 3FN, medir onde dói de verdade, e desnormalizar cirurgicamente apenas os pontos comprovadamente quentes. Otimização especulativa antes de medir costuma introduzir bugs de consistência para resolver um problema de desempenho que talvez nem exista.

Fechando o raciocínio#

A normalização até a 3FN não é complicada quando você para de decorar definições e passa a enxergar dependências funcionais. Cada forma normal é apenas um nome para um jeito de um fato estar no lugar errado: valor não-atômico na 1FN, dependência de parte da chave na 2FN, dependência transitiva na 3FN. O conserto é sempre o mesmo movimento — dar ao fato uma tabela própria e ligá-la de volta por chave estrangeira.

O ganho é um schema em que cada informação tem um endereço único, onde atualizar significa tocar uma linha e onde o banco impede as inconsistências em vez de torcer para que não aconteçam. A desnormalização continua sendo uma ferramenta válida, mas é uma exceção medida, tomada depois que os números pedem, e nunca um atalho para pular o trabalho de modelar direito. Comece normalizado, entenda por que cada tabela existe, e você terá uma base que envelhece bem.

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly