Fundamentos e Importância da Normalização em Bancos de Dados
O que é Normalização?
A normalização é um processo sistemático de design de banco de dados que:
- Reduz a redundância de dados
- Elimina anomalias de inserção, atualização e exclusão
- Melhora a integridade dos dados
- Simplifica consultas e manutenção
- Otimiza o desempenho do banco de dados
O processo segue um conjunto de regras formais chamadas "Formas Normais" (1FN, 2FN, 3FN, BCNF, 4FN, 5FN), cada uma construída sobre a anterior para resolver problemas específicos de design.
Por que Normalizar?
Muitos sistemas começam com estruturas de dados simples e não normalizadas, geralmente por razões como:
- Facilidade inicial de implementação
- Falta de conhecimento em design de banco de dados
- Priorização de velocidade de desenvolvimento sobre qualidade
- Requisitos iniciais limitados que não expõem problemas de escalabilidade
No entanto, à medida que o sistema cresce, surgem diversos problemas que afetam a manutenção, o desempenho e a integridade dos dados.
Problemas Práticos com o Banco não Normalizado
1. Atualização de Informações de Clientes
Cenário: O cliente João Silva muda seu telefone
Problema:
-- Necessário atualizar MULTIPLOS registros
UPDATE pedidos SET cliente_telefone = '(11) 98888-7777'
WHERE cliente_id = 101;
-- Risco de inconsitência se alguma linha não for atualizada
Consequência: Dados desatualizados em pedidos antigos, dificuldade para manter integridade
2. Mudança de Preço de Produtos
Cenário: O preço do "Smartphone XYZ" aumenta para R$ 2.799,90
Problema:
-- Preço antigo permanece nos pedidos existentes
-- Não temos histórico de alterações
-- Consultas para cálculo de faturamento ficam imprecisas
Consequência: Dificuldade para análises financeiras precisas, impossibilidade de calcular inflação/reajustes
3. Pedidos com Muitos Itens
Cenário: Um cliente compra 5 produtos diferentes
Problema:
-- Necessário criar múltiplos registros "fake"
INSERT INTO pedidos (
pedido_id, data_pedido, cliente_id, /*...*/,
produto_id_1, produto_nome_1, /*...*/,
produto_id_2, produto_nome_2, /*... NULL ...*/
) VALUES (...);
-- Ou deixar itens de fora do pedido
Consequência: Sistema não reflete a realidade dos negócios, relatórios imprecisos
4. Gestão de Estoque
Cenário: Atualizar estoque após uma venda
Problema:
-- Não há ligação direta com tabela de produtos
-- Dificuldade para automatizar baixa no estoque
-- Risco de overselling (vender produto sem estoque)
Consequência: Erros operacionais, clientes recebendo produtos esgotados
Processo de Normalização
A seguir, detalhamos o processo de normalização aplicado ao nosso banco de dados, explicando cada forma normal e as alterações realizadas.
Banco de Dados Original (Não Normalizado)
O banco de dados original consistia em uma única tabela pedidos
com a seguinte estrutura:
erDiagram
PEDIDOS {
int pedido_id PK
timestamp data_pedido
int cliente_id
string cliente_nome
string cliente_email
string cliente_telefone
string cliente_endereco
int produto_id_1
string produto_nome_1
string produto_categoria_1
decimal produto_preco_1
int produto_quantidade_1
int produto_id_2
string produto_nome_2
string produto_categoria_2
decimal produto_preco_2
int produto_quantidade_2
decimal valor_total
string forma_pagamento
string status_pedido
}
CREATE TABLE pedidos (
pedido_id SERIAL PRIMARY KEY,
data_pedido TIMESTAMP NOT NULL,
cliente_id INTEGER NOT NULL,
cliente_nome VARCHAR(100) NOT NULL,
cliente_email VARCHAR(100) NOT NULL,
cliente_telefone VARCHAR(20) NOT NULL,
cliente_endereco TEXT NOT NULL,
produto_id_1 INTEGER,
produto_nome_1 VARCHAR(100),
produto_categoria_1 VARCHAR(50),
produto_preco_1 DECIMAL(10,2),
produto_quantidade_1 INTEGER,
produto_id_2 INTEGER,
produto_nome_2 VARCHAR(100),
produto_categoria_2 VARCHAR(50),
produto_preco_2 DECIMAL(10,2),
produto_quantidade_2 INTEGER,
valor_total DECIMAL(10,2) NOT NULL,
forma_pagamento VARCHAR(50) NOT NULL,
status_pedido VARCHAR(20) NOT NULL
);
Esta estrutura apresenta diversos problemas:
- Redundância de dados de clientes
- Limitação de produtos por pedido (apenas 2)
- Repetição de informações de produtos
- Dificuldade para consultas e relatórios
Primeira Forma Normal (1FN)
Princípio: Eliminar grupos repetitivos, garantir que cada coluna contenha apenas valores atômicos (indivisíveis) e que cada tabela tenha uma chave primária.
Problema identificado:
A tabela pedidos
viola a 1FN porque:
- Contém grupos repetitivos (produto_id_1, produto_id_2, etc.)
- Limita artificialmente o número de produtos por pedido a apenas 2
- Tem colunas que podem ficar vazias (NULL) quando um pedido tem menos de 2 produtos
Alterações realizadas:
- Criação de uma tabela separada
itens_pedidos
para armazenar os produtos de cada pedido - Remoção das colunas repetitivas (produto_id_1, produto_nome_1, produto_id_2, etc.) da tabela
pedidos
- Estabelecimento de uma relação um-para-muitos entre
pedidos
eitens_pedidos
- Definição de chaves primárias adequadas para cada tabela
-- Tabela principal de pedidos (sem os grupos repetitivos)
CREATE TABLE pedidos (
pedido_id SERIAL PRIMARY KEY,
data_pedido TIMESTAMP NOT NULL,
cliente_id INTEGER NOT NULL,
cliente_nome VARCHAR(100) NOT NULL,
cliente_email VARCHAR(100) NOT NULL,
cliente_telefone VARCHAR(20) NOT NULL,
cliente_endereco TEXT NOT NULL,
valor_total DECIMAL(10,2) NOT NULL,
forma_pagamento VARCHAR(50) NOT NULL,
status_pedido VARCHAR(20) NOT NULL
);
-- Nova tabela para itens de pedido (resolve o problema dos grupos repetitivos)
CREATE TABLE itens_pedidos (
item_id SERIAL PRIMARY KEY,
pedido_id INTEGER NOT NULL REFERENCES pedidos(pedido_id),
produto_id INTEGER NOT NULL,
produto_nome VARCHAR(100) NOT NULL,
produto_categoria VARCHAR(50) NOT NULL,
preco_unitario DECIMAL(10, 2) NOT NULL,
quantidade INTEGER NOT NULL,
subtotal DECIMAL(10, 2) NOT NULL
);
Benefícios:
- Permite número ilimitado de produtos por pedido
- Elimina valores nulos para pedidos com menos produtos
- Facilita consultas sobre produtos vendidos
- Cada tabela agora tem uma estrutura clara com chave primária
- Todas as colunas contêm valores atômicos (indivisíveis)
- Elimina a redundância estrutural (colunas repetitivas)
erDiagram
PEDIDOS {
int pedido_id PK
timestamp data_pedido
int cliente_id
string cliente_nome
string cliente_email
string cliente_telefone
string cliente_endereco
decimal valor_total
string forma_pagamento
string status_pedido
}
ITENS_PEDIDOS {
int item_id PK
int pedido_id FK
int produto_id
string produto_nome
string produto_categoria
decimal preco_unitario
int quantidade
decimal subtotal
}
PEDIDOS ||--o{ ITENS_PEDIDOS : "contém"
Segunda Forma Normal (2FN)
Princípio: Uma tabela está na 2FN quando já está na 1FN e todos os atributos não-chave dependem totalmente da chave primária, não apenas de parte dela. Isso elimina as dependências parciais.
Problema identificado:
Após aplicar a 1FN, ainda temos problemas na tabela itens_pedidos
:
- Informações do produto (nome, categoria) dependem apenas do
produto_id
, não da chave completa (item_id
ou combinaçãopedido_id
+produto_id
) - Dados repetidos de produtos em múltiplos pedidos
- Inconsistências potenciais quando informações de produtos são atualizadas
Além disso, na tabela pedidos
:
- Informações do cliente (nome, email, telefone, endereço) dependem apenas do
cliente_id
, não dopedido_id
- Dados de clientes repetidos em múltiplos pedidos
Alterações realizadas:
- Criação de tabela
CLIENTES
para armazenar dados dos clientes - Criação de tabela
ENDERECOS
para armazenar endereços dos clientes - Criação de tabela
PRODUTOS
para armazenar informações de produtos - Criação de tabela
CATEGORIAS
para armazenar categorias de produtos - Remoção de atributos dependentes de apenas parte da chave das tabelas originais
- Estabelecimento de relacionamentos via chaves estrangeiras
-- Tabela de clientes (remove dependências parciais da tabela pedidos)
CREATE TABLE CLIENTES (
cliente_id SERIAL PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL,
telefone VARCHAR(20) NOT NULL
);
-- Tabela de endereços (normaliza ainda mais os dados do cliente)
CREATE TABLE ENDERECOS (
endereco_id SERIAL PRIMARY KEY,
cliente_id INTEGER REFERENCES CLIENTES (cliente_id),
cep CHAR(9),
cidade VARCHAR(150),
estado CHAR(2),
numero INTEGER,
complemento VARCHAR(100)
);
-- Tabela de categorias (remove dependências transitivas da tabela produtos)
CREATE TABLE CATEGORIAS (
categoria_id SERIAL PRIMARY KEY,
nome VARCHAR(120) NOT NULL,
descricao VARCHAR(150)
);
-- Tabela de produtos (remove dependências parciais da tabela itens_pedidos)
CREATE TABLE PRODUTOS (
produto_id INTEGER PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
categoria_id INTEGER REFERENCES CATEGORIAS (categoria_id),
preco DECIMAL(10, 2) NOT NULL,
estoque INTEGER DEFAULT 0
);
-- Tabela de pedidos atualizada (sem dados de clientes)
CREATE TABLE pedidos (
pedido_id SERIAL PRIMARY KEY,
data_pedido TIMESTAMP NOT NULL,
cliente_id INTEGER NOT NULL REFERENCES CLIENTES (cliente_id),
endereco_id INTEGER NOT NULL REFERENCES ENDERECOS (endereco_id),
valor_total DECIMAL(10, 2) NOT NULL,
forma_pagamento VARCHAR(50) NOT NULL,
status_pedido VARCHAR(20) NOT NULL
);
-- Tabela de itens de pedido atualizada (sem dados de produtos)
CREATE TABLE itens_pedidos (
item_id SERIAL PRIMARY KEY,
pedido_id INTEGER NOT NULL REFERENCES pedidos (pedido_id),
produto_id INTEGER NOT NULL REFERENCES PRODUTOS (produto_id),
quantidade INTEGER NOT NULL,
preco_unitario DECIMAL(10, 2) NOT NULL,
subtotal DECIMAL(10, 2) NOT NULL
);
Benefícios:
- Elimina redundância de dados de cliente e produto
- Facilita atualizações de informações (alterações em um único lugar)
- Melhora a integridade dos dados
Exemplo prático:
Antes da 2FN, atualizar o nome de um produto exigiria:
-- Atualização complexa e propensa a erros
UPDATE itens_pedidos
SET produto_nome = 'Novo Nome do Produto'
WHERE produto_id = 5001;
Depois da 2FN, a mesma atualização é simples e segura:
-- Atualização em um único lugar
UPDATE PRODUTOS
SET nome = 'Novo Nome do Produto'
WHERE produto_id = 5001;
erDiagram
CLIENTES {
int cliente_id PK
string nome
string email
string telefone
}
ENDERECOS {
int endereco_id PK
int cliente_id FK
string cep
string cidade
string estado
int numero
string complemento
}
CATEGORIAS {
int categoria_id PK
string nome
string descricao
}
PRODUTOS {
int produto_id PK
string nome
int categoria_id FK
decimal preco
int estoque
}
PEDIDOS {
int pedido_id PK
timestamp data_pedido
int cliente_id FK
int endereco_id FK
decimal valor_total
string forma_pagamento
string status_pedido
}
ITENS_PEDIDOS {
int item_id PK
int pedido_id FK
int produto_id FK
int quantidade
decimal preco_unitario
decimal subtotal
}
CLIENTES ||--o{ ENDERECOS : "possui"
CLIENTES ||--o{ PEDIDOS : "faz"
ENDERECOS ||--o{ PEDIDOS : "usado em"
CATEGORIAS ||--o{ PRODUTOS : "classifica"
PEDIDOS ||--o{ ITENS_PEDIDOS : "contém"
PRODUTOS ||--o{ ITENS_PEDIDOS : "incluído em"
Terceira Forma Normal (3FN)
Princípio: Uma tabela está na 3FN quando já está na 2FN e todos os atributos não-chave dependem diretamente da chave primária e não de outros atributos não-chave. Isso elimina as dependências transitivas.
Problema identificado:
Após aplicar a 2FN, ainda temos algumas dependências transitivas:
- Na tabela
PRODUTOS
, o nome da categoria depende dacategoria_id
, que por sua vez depende da chave primáriaproduto_id
- Na tabela
ENDERECOS
, informações como cidade e estado poderiam depender do CEP, que por sua vez depende da chave primáriaendereco_id
Alterações realizadas:
- Refinamento da tabela
CATEGORIAS
para armazenar todas as informações relacionadas a categorias - Garantia de que todos os atributos não-chave dependam diretamente da chave primária
- Implementação de colunas calculadas para valores derivados (como
subtotal
emitens_pedidos
)
-- Tabela de categorias (remove dependências transitivas da tabela produtos)
CREATE TABLE CATEGORIAS (
categoria_id SERIAL PRIMARY KEY,
nome VARCHAR(120) NOT NULL,
descricao VARCHAR(150),
departamento VARCHAR(100),
ativo BOOLEAN DEFAULT TRUE
);
-- Tabela de produtos refinada
CREATE TABLE PRODUTOS (
produto_id INTEGER PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
categoria_id INTEGER REFERENCES CATEGORIAS (categoria_id),
preco DECIMAL(10, 2) NOT NULL,
estoque INTEGER DEFAULT 0,
peso DECIMAL(8, 3),
dimensoes VARCHAR(50),
data_cadastro TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Tabela de pedidos com relações bem definidas
CREATE TABLE pedidos (
pedido_id SERIAL PRIMARY KEY,
data_pedido TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
cliente_id INTEGER NOT NULL REFERENCES CLIENTES (cliente_id),
endereco_id INTEGER NOT NULL REFERENCES ENDERECOS (endereco_id),
valor_total DECIMAL(10, 2) NOT NULL,
forma_pagamento VARCHAR(50) NOT NULL,
status_pedido VARCHAR(20) NOT NULL,
data_atualizacao TIMESTAMP
);
-- Tabela de itens com coluna calculada
CREATE TABLE itens_pedidos (
item_id SERIAL PRIMARY KEY,
pedido_id INTEGER NOT NULL REFERENCES pedidos (pedido_id),
produto_id INTEGER NOT NULL REFERENCES PRODUTOS (produto_id),
quantidade INTEGER NOT NULL CHECK (quantidade > 0),
preco_unitario DECIMAL(10, 2) NOT NULL,
subtotal DECIMAL(10, 2) GENERATED ALWAYS AS (quantidade * preco_unitario) STORED,
UNIQUE (pedido_id, produto_id) -- Evita duplicação de produtos no mesmo pedido
);
Benefícios:
- Elimina completamente dependências transitivas
- Melhora a organização lógica dos dados
- Facilita a manutenção e evolução do esquema
- Reduz ainda mais a redundância de dados
erDiagram
CLIENTES {
int cliente_id PK
string nome
string email
string telefone
}
ENDERECOS {
int endereco_id PK
int cliente_id FK
string cep
string cidade
string estado
int numero
string complemento
}
CATEGORIAS {
int categoria_id PK
string nome
string descricao
string departamento
boolean ativo
}
PRODUTOS {
int produto_id PK
string nome
int categoria_id FK
decimal preco
int estoque
decimal peso
string dimensoes
timestamp data_cadastro
}
PEDIDOS {
int pedido_id PK
timestamp data_pedido
int cliente_id FK
int endereco_id FK
decimal valor_total
string forma_pagamento
string status_pedido
timestamp data_atualizacao
}
ITENS_PEDIDOS {
int item_id PK
int pedido_id FK
int produto_id FK
int quantidade
decimal preco_unitario
decimal subtotal
constraint unique_produto
}
CLIENTES ||--o{ ENDERECOS : "possui"
CLIENTES ||--o{ PEDIDOS : "faz"
ENDERECOS ||--o{ PEDIDOS : "usado em"
CATEGORIAS ||--o{ PRODUTOS : "classifica"
PEDIDOS ||--o{ ITENS_PEDIDOS : "contém"
PRODUTOS ||--o{ ITENS_PEDIDOS : "incluído em"
Melhorias Adicionais
Além da normalização básica até a 3FN, implementamos outras melhorias que aumentam a robustez e a usabilidade do banco de dados:
-
Restrições de Integridade:
- Adição de restrições CHECK para validar dados
quantidade INTEGER NOT NULL CHECK (quantidade > 0)
- Uso de UNIQUE para prevenir duplicações
UNIQUE (pedido_id, produto_id) -- Evita duplicação de produtos no mesmo pedido
- Restrições NOT NULL para campos obrigatórios
nome VARCHAR(100) NOT NULL
-
Colunas Calculadas:
- Adição de
subtotal
como coluna calculada emitens_pedidos
subtotal DECIMAL(10, 2) GENERATED ALWAYS AS (quantidade * preco_unitario) STORED
- Benefícios: consistência garantida, simplificação de consultas, melhor desempenho
- Adição de
-
Valores Default e Timestamps Automáticos:
- Valores padrão para campos comuns
estoque INTEGER DEFAULT 0 ativo BOOLEAN DEFAULT TRUE
- Timestamps automáticos para auditoria
data_cadastro TIMESTAMP DEFAULT CURRENT_TIMESTAMP
Quando Desnormalizar?
Embora a normalização seja geralmente benéfica existem situações específicas onde a desnormalização controlada pode ser vantajosa:
Cenários para Desnormalização
-
Otimização de Desempenho de Leitura:
- Tabelas de relatórios pré-calculados
- Consultas frequentes que envolvem muitos JOINs
-
Requisitos de Alta Performance:
- Sistemas com milhões de transações por segundo
- Aplicações onde o tempo de resposta é crítico
- Casos onde os JOINs se tornam gargalos de desempenho
-
Armazenamento de Dados Históricos:
- Preservação do estado exato no momento da transação
- Exemplo: manter o preço do produto no momento da venda
Técnicas de Desnormalização Controlada
erDiagram
RELATORIO_VENDAS_DIARIAS {
date data PK
int total_pedidos
decimal valor_total
int qtd_produtos_vendidos
decimal ticket_medio
string categoria_mais_vendida
timestamp ultima_atualizacao
}
PRODUTOS_VENDIDOS_CACHE {
int produto_id PK
string nome
string categoria
int total_vendas
decimal valor_total
timestamp ultima_venda
}
PEDIDOS_COMPLETOS {
int pedido_id PK
timestamp data_pedido
int cliente_id
string cliente_nome
string cliente_email
string cliente_telefone
string cliente_endereco
decimal valor_total
string forma_pagamento
string status_pedido
string produtos_json
}
-- Exemplo: Tabela de pedidos com dados desnormalizados para relatórios
CREATE TABLE relatorio_vendas_diarias (
data DATE PRIMARY KEY,
total_pedidos INTEGER NOT NULL,
valor_total DECIMAL(12, 2) NOT NULL,
qtd_produtos_vendidos INTEGER NOT NULL,
ticket_medio DECIMAL(10, 2) GENERATED ALWAYS AS (CASE WHEN total_pedidos > 0 THEN valor_total / total_pedidos ELSE 0 END) STORED,
categoria_mais_vendida VARCHAR(100),
ultima_atualizacao TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Conclusão
A normalização é um processo fundamental no design de bancos de dados relacionais que transforma estruturas caóticas e redundantes em modelos organizados, eficientes e escaláveis. Através da aplicação sistemática das formas normais, conseguimos:
- Eliminar redundâncias que causam inconsistências e desperdício de espaço
- Prevenir anomalias de inserção, atualização e exclusão
- Melhorar a integridade dos dados através de relacionamentos bem definidos