📏 CAPÍTULO 10: NORMALIZAÇÃO DE DADOS (1FN, 2FN E 3FN)


🎯 Objetivos de Aprendizagem

Ao final deste capítulo (estimativa: 2 horas de estudo autoguiado), você será capaz de:

  • 🔹 Compreender as anomalias graves de banco de dados (Anomalia de Inserção, Atualização e Exclusão) causadas por esquemas desnormalizados.
  • 🔹 Dominar o conceito de Dependência Funcional Total e Transitiva.
  • 🔹 Aplicar o processo de Normalização passo a passo: 1ª Forma Normal (Atomicidade), 2ª Forma Normal (Dependência Total) e 3ª Forma Normal (Sem Transitividade).
  • 🔹 Refatorar bancos desorganizados para atingir o Padrão 3FN eliminando redundâncias.

A Normalização é a "vacina" contra dados ruins. É um processo passo a passo (algoritmo) que aplicamos nas nossas tabelas para eliminar redundâncias (dados repetidos à toa) e prevenir anomalias (erros que acontecem quando tentamos salvar ou apagar algo). 🛡️🧩

🏢 O Cenário Prático (Seu Desafio)

O setor comercial da TecProExpress criou uma planilha gigante para controlar Vendas. Nela, eles colocaram o Nome do Cliente, o Nome do Entregador, a Placa do Caminhão e o Valor do Frete — tudo na mesma linha! Quando o Entregador mudava de caminhão, o pessoal tinha que atualizar essa informação em 500 linhas diferentes manualmente, causando um caos no faturamento.

"Seu desafio é pegar esse 'Galinheiro de Dados' não normalizado e aplicar as três regras de ouro da Normalização para quebrá-lo em tabelas menores, perfeitamente integradas e imunes a anomalias."


🧠 Fundamentos: As Anomalias e a Dependência Funcional

Se não normalizarmos, o banco sofrerá três doenças fatais:

  1. Anomalia de Inserção: Você não consegue cadastrar um Caminhão novo no sistema porque ele ainda não fez nenhuma venda (e a tabela exige os dados da venda na mesma linha).
  2. Anomalia de Atualização: Você precisa alterar o endereço de um cliente em 500 registros antigos. Se o computador travar no registro 250, o banco ficará inconsistente.
  3. Anomalia de Exclusão: Se você deletar o registro da única venda que um cliente fez, você acidentalmente deleta o cadastro do cliente inteiro junto!

📊 O Algoritmo de Normalização

flowchart LR
    UNF["❌ Tabela Caótica<br/>(A Planilha)"] --> FN1["✅ 1FN:<br/>Atomicidade"]
    FN1 --> FN2["✅ 2FN:<br/>Sem Dependência Parcial"]
    FN2 --> FN3["✅ 3FN:<br/>Sem Dependência Transitiva"]

Funil de Normalização de Banco de Dados 1FN-3FN

🔍 Dependência Funcional: O que é isso?

Dizemos que B depende funcionalmente de A (ou A -> B) quando: se eu te der o valor de A, você consegue descobrir com certeza absoluta qual é o valor de B.

  • Exemplo: O CPF determina o Nome (Um CPF só tem um dono). Mas o Nome não determina o CPF (Existem muitos "Joões"). A Chave Primária (PK) deve sempre ser o determinador de tudo na tabela!

📖 A Jornada das Formas Normais (DDL na Prática)

Vamos resolver o problema da TecProExpress passo a passo.

1️⃣ Primeira Forma Normal (1FN)

Regra: Todos os atributos devem ser atômicos. Sem "células duplas" ou grupos repetitivos.

  • A Violação: O Excel tinha a coluna "Itens do Pedido" com "Teclado, Mouse, Monitor" dentro da mesma célula.
  • A Correção (1FN): Quebramos isso. O Pedido vira 3 linhas diferentes.

2️⃣ Segunda Forma Normal (2FN)

Regra: Estar na 1FN + Nenhuma coluna pode depender apenas de metade da Chave Primária Composta.

  • A Violação: Imagine que a Chave da nossa tabela seja (ID_Venda, ID_Produto). A coluna Nome_do_Produto só depende do ID do Produto, ela não quer nem saber qual foi a Venda! Isso é dependência parcial.
  • A Correção (2FN): Puxamos o Produto para uma tabela separada.

3️⃣ Terceira Forma Normal (3FN)

Regra: Estar na 2FN + Nenhuma coluna "não-chave" pode depender de outra coluna "não-chave" (Dependência Transitiva).

  • A Violação: Na tabela de Venda, temos o ID do Cliente, o Nome do Cliente e a Cidade do Cliente. A Cidade depende do Nome, que depende do ID. Se o Cliente não é a Chave Primária da tabela de Vendas, ele não pode morar lá com seus atributos.
  • A Correção (3FN): Puxamos o Cliente para sua própria tabela.

🛠️ Prática Obrigatória: Quebrando o Monstro (DDL -> DML)

Cenário: O resultado da 3FN. Após a sua análise, a planilha gigante virou três tabelas limpas: Cliente, Produto e a associativa Venda.

🚀 Script de Seed (Gabarito da 3FN)

-- PASSO 1: DDL (As Tabelas Bases - Fortes)
CREATE TABLE cliente_norm (
    id_cliente INT PRIMARY KEY,
    nome VARCHAR(100),
    cidade VARCHAR(50)
);

CREATE TABLE produto_norm (
    id_produto INT PRIMARY KEY,
    descricao VARCHAR(100)
);

-- PASSO 2: DDL (A Tabela Normalizada que liga tudo sem redundância)
CREATE TABLE venda_norm (
    id_venda INT PRIMARY KEY,
    id_cliente INT, -- A FK apontando para o cliente (Nada de escrever a cidade dele aqui!)
    id_produto INT,
    FOREIGN KEY (id_cliente) REFERENCES cliente_norm(id_cliente),
    FOREIGN KEY (id_produto) REFERENCES produto_norm(id_produto)
);

-- PASSO 3: DML (Carga dos Dados Sem Repetição)
INSERT INTO cliente_norm VALUES (1, 'Maria Silva', 'São Paulo');
INSERT INTO produto_norm VALUES (10, 'Teclado Gamer');
-- Agora registramos 100 vendas sem nunca digitar "São Paulo" de novo!
INSERT INTO venda_norm VALUES (1001, 1, 10);

🔍 Detalhamento do Código:

  • Note que a tabela venda_norm é extremamente leve! Ela só guarda os números dos IDs. Se a Maria mudar de 'São Paulo' para 'Rio de Janeiro', você fará o UPDATE em exatamente 1 linha na tabela cliente_norm, e todas as milhares de vendas dela já estarão magicamente atualizadas! Isso é a força da 3FN.


💻 Ponte Prática: Do SQL Manual ao SQLAlchemy 2.0 ORM

Como o SQLAlchemy 2.0 combate as Anomalias de Inserção, Atualização e Exclusão através do modelo normalizado?

🔴 1. A Abordagem Manual (Tabela Monolítica Desnormalizada)

No modelo não-normalizado, para atualizar o endereço de um cliente, você é obrigado a rodar um UPDATE massivo em centenas de linhas de vendas:

# ❌ ABORDAGEM NÃO-NORMALIZADA: Redundância massiva
cursor.execute("UPDATE vendas_monoliticas SET cidade_cliente = 'Rio de Janeiro' WHERE nome_cliente = 'Maria Silva';")
# Se o banco tiver 500.000 vendas, esse UPDATE bloqueará a tabela inteira!

🟢 2. A Abordagem com SQLAlchemy 2.0 (Modelos 3FN Desacoplados)

Com os modelos normalizados em 3FN, alteramos o registro do cliente em exatamente uma linha da tabela clientes. Todas as relações e queries refletem a mudança instantaneamente:

# ✅ ABORDAGEM MODERNA EM 3FN COM SQLALCHEMY 2.0
from sqlalchemy import create_engine, String, Integer, ForeignKey, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session

class Base(DeclarativeBase):
    pass

class ClienteNormModel(Base):
    """3FN: Tabela isolada para a entidade Cliente."""
    __tablename__ = "clientes_norm"

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    nome: Mapped[str] = mapped_column(String(100), nullable=False)
    cidade: Mapped[str] = mapped_column(String(50), nullable=False)

    vendas: Mapped[list["VendaNormModel"]] = relationship("VendaNormModel", back_populates="cliente")

class ProdutoNormModel(Base):
    """3FN: Tabela isolada para a entidade Produto."""
    __tablename__ = "produtos_norm"

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    descricao: Mapped[str] = mapped_column(String(100), nullable=False)

class VendaNormModel(Base):
    """3FN: Tabela associativa contendo apenas identificadores e fatos transacionais."""
    __tablename__ = "vendas_norm"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    id_cliente: Mapped[int] = mapped_column(ForeignKey("clientes_norm.id"), nullable=False)
    id_produto: Mapped[int] = mapped_column(ForeignKey("produtos_norm.id"), nullable=False)

    cliente: Mapped["ClienteNormModel"] = relationship("ClienteNormModel", back_populates="vendas")
    produto: Mapped["ProdutoNormModel"] = relationship("ProdutoNormModel")

🛠️ Mini-Projeto 10 (BD): Refatoração de Dados Não-Normalizados (1FN a 3FN)

Objetivo: Demonstrar como uma atualização pontual no cliente normalizado atualiza todas as consultas de vendas sem anomalias de alteração.

📋 Pré-requisitos e Instalação

No terminal do seu ambiente virtual (PowerShell ou Bash), instale a biblioteca necessária:

pip install sqlalchemy

💻 Código Completo e Autocontido (miniprojeto_10_normalizacao.py)

Crie o arquivo miniprojeto_10_normalizacao.py e insira o código abaixo integralmente:

"""
Mini-Projeto 10: Refatoração de Dados Não-Normalizados (1FN a 3FN)
Curso: GTI - Banco de Dados Relacionais e Engenharia de Software
Stack: Python 3.11+ | SQLAlchemy 2.0 | SQLite
"""
import os
from sqlalchemy import create_engine, String, Integer, ForeignKey, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session

# 1. Definição Declarativa Normalizada (3FN)
class Base(DeclarativeBase):
    pass

class ClienteNormModel(Base):
    """3FN: Tabela isolada para a entidade Cliente."""
    __tablename__ = "clientes_norm"

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    nome: Mapped[str] = mapped_column(String(100), nullable=False)
    cidade: Mapped[str] = mapped_column(String(50), nullable=False)

    vendas: Mapped[list["VendaNormModel"]] = relationship("VendaNormModel", back_populates="cliente")

class ProdutoNormModel(Base):
    """3FN: Tabela isolada para a entidade Produto."""
    __tablename__ = "produtos_norm"

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    descricao: Mapped[str] = mapped_column(String(100), nullable=False)

class VendaNormModel(Base):
    """3FN: Tabela associativa contendo apenas identificadores e fatos transacionais."""
    __tablename__ = "vendas_norm"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    id_cliente: Mapped[int] = mapped_column(ForeignKey("clientes_norm.id"), nullable=False)
    id_produto: Mapped[int] = mapped_column(ForeignKey("produtos_norm.id"), nullable=False)

    cliente: Mapped["ClienteNormModel"] = relationship("ClienteNormModel", back_populates="vendas")
    produto: Mapped["ProdutoNormModel"] = relationship("ProdutoNormModel")

# 2. Ponto de Entrada Executável
if __name__ == "__main__":
    DB_FILE = "tecpro_normalizado.db"

    # Reset preventivo para garantir idempotência em testes repetidos
    if os.path.exists(DB_FILE):
        os.remove(DB_FILE)

    engine = create_engine(f"sqlite:///{DB_FILE}", echo=False)
    Base.metadata.create_all(bind=engine)

    print("📐 NORMALIZAÇÃO DE DADOS (3FN) - TECPROEXPRESS")
    print("=" * 65)

    # 1. Povoando o banco normalizado
    with Session(engine) as session:
        cliente = ClienteNormModel(id=1, nome="Maria Silva", cidade="São Paulo")
        prod1 = ProdutoNormModel(id=10, descricao="Teclado Mecânico")
        prod2 = ProdutoNormModel(id=20, descricao="Monitor 4K")

        session.merge(cliente)
        session.merge(prod1)
        session.merge(prod2)
        session.flush()

        venda1 = VendaNormModel(id_cliente=1, id_produto=10)
        venda2 = VendaNormModel(id_cliente=1, id_produto=20)
        session.add_all([venda1, venda2])
        session.commit()
        print("✅ Cliente, Produtos e 2 Vendas gravados em 3FN!")

    # 2. Atualização Pontual (Zero Redundância):
    with Session(engine) as session:
        maria = session.get(ClienteNormModel, 1)
        if maria:
            print(f"\n🏠 Mudando cidade de Maria: {maria.cidade} -> Rio de Janeiro (Atualiza 1 único registro)")
            maria.cidade = "Rio de Janeiro"
            session.commit()

    # 3. Consulta das vendas de Maria após a atualização:
    print("\n🔍 Consultando histórico de vendas após o UPDATE pontual:")
    with Session(engine) as session:
        stmt = select(VendaNormModel).join(VendaNormModel.cliente).join(VendaNormModel.produto)
        for v in session.scalars(stmt).all():
            print(f"  📦 Venda #{v.id} | Cliente: {v.cliente.nome} ({v.cliente.cidade}) | Item: {v.produto.descricao}")
    print("=" * 65)

🚀 Como Executar

Execute o script no terminal:

python miniprojeto_10_normalizacao.py

🖥️ Saída Esperada no Console

📐 NORMALIZAÇÃO DE DADOS (3FN) - TECPROEXPRESS
=================================================================
✅ Cliente, Produtos e 2 Vendas gravados em 3FN!

🏠 Mudando cidade de Maria: São Paulo -> Rio de Janeiro (Atualiza 1 único registro)

🔍 Consultando histórico de vendas após o UPDATE pontual:
  📦 Venda #1 | Cliente: Maria Silva (Rio de Janeiro) | Item: Teclado Mecânico
  📦 Venda #2 | Cliente: Maria Silva (Rio de Janeiro) | Item: Monitor 4K
=================================================================

💡 Checkpoint de Lógica

Importante

Reflexão Profissional: Um banco de dados 100% normalizado é sempre a melhor escolha? (Resposta: Para sistemas Transacionais/Operacionais, SIM! Mas em Data Warehouses de Analytics - onde a leitura de relatórios pesados é mais importante que a escrita - às vezes os arquitetos aplicam a Desnormalização propositalmente para evitar muitos cruzamentos de JOINs e acelerar a velocidade do painel). 🧠🛡️




🧪 Quiz de Fixação e Autoavaliação — Capítulo 10

1. O que exige a 1ª Forma Normal (1FN) em uma tabela relacional?

  • A) Que todas as colunas sejam do tipo texto.
  • B) Que todos os atributos sejam atômicos (indivisíveis), sem repetição de grupos de dados ou colunas com múltiplos valores na mesma célula (ex: 'Telefone 1, Telefone 2' ou listas separadas por vírgula).
  • C) Que a tabela tenha exatamente 3 chaves estrangeiras.
  • D) Que o banco use criptografia quântica.
💡 Ver Resposta e Justificativa

Resposta Correta: B
Justificativa: A 1FN elimina arrays e campos compostos: cada célula deve guardar exatamente um único valor atômico.


2. Quando dizemos que uma tabela que já está na 1FN atingiu a 2ª Forma Normal (2FN)?

  • A) Quando todos os atributos não-chave dependem TOTALMENTE da Chave Primária por inteiro, eliminando dependências parciais em tabelas com chaves compostas.
  • B) Quando a tabela tem mais de 2 anos de uso.
  • C) Quando o banco é migrado para PostgreSQL.
  • D) Quando todos os valores nulos são apagados.
💡 Ver Resposta e Justificativa

Resposta Correta: A
Justificativa: A 2FN combate dependências parciais em tabelas com PKs compostas: nenhum atributo pode depender de apenas 'metade' da chave primária.


3. O que proíbe a 3ª Forma Normal (3FN) para eliminar redundâncias e inconsistências de dados?

  • A) Proíbe o uso de comandos DELETE.
  • B) Proíbe a existência de Dependências Transitivas (atributos não-chave que dependem de outros atributos não-chave em vez de dependerem diretamente da Chave Primária).
  • C) Proíbe o uso de índices B-Tree.
  • D) Obriga todas as tabelas a terem nomes em inglês.
💡 Ver Resposta e Justificativa

Resposta Correta: B
Justificativa: Regra da 3FN: 'Todo atributo deve depender da chave, de toda a chave e de nada além da chave'. Se cidade depende de cep, eles devem ir para uma tabela separada de Endereços.


🎯 Laboratório Prático

Coloque este conhecimento em prática agora mesmo executando o roteiro autoguiado:
👉 ATIVIDADE 04: NORMALIZAÇÃO