3NF & BCNF
Normalização prática sem excesso acadêmico: decomponha tabelas até que atributos não-chave dependam da chave inteira, e nada além da chave.
Receita
Cartão de receita de referência rápida - pronto para copiar e colar.
-- Viola 3NF: city depende de zip, não apenas de customer_id
-- customers(customer_id, email, zip, city)
-- Correção 3NF: remove dependência transitiva zip -> city
CREATE TABLE zip_codes (zip text PRIMARY KEY, city text NOT NULL);
CREATE TABLE customers (
customer_id bigint PRIMARY KEY,
email text NOT NULL UNIQUE,
zip text NOT NULL REFERENCES zip_codes (zip)
);
-- Correção BCNF: quando o determinante não é uma superchave
-- enrollments(student_id, course_id, instructor_id) com a regra course_id -> instructor_id
CREATE TABLE course_instructors (
course_id text PRIMARY KEY,
instructor_id text NOT NULL
);
CREATE TABLE enrollments (
student_id text NOT NULL,
course_id text NOT NULL REFERENCES course_instructors (course_id),
PRIMARY KEY (student_id, course_id)
);Quando usar isso:
- Importando CSVs para uma única tabela de staging ampla antes do DDL de produção.
- Revisando se uma nova coluna pertence a uma tabela de fatos ou a uma tabela de dimensão.
- Depurando duplicatas contraditórias em relatórios (mesmo ID, descrições diferentes).
Exemplo de Trabalho
BEGIN;
-- Ponto de partida: não é 3NF
CREATE TABLE employee_assignments_bad (
employee_id text,
department_id text,
department_name text,
department_head text,
PRIMARY KEY (employee_id, department_id)
);
-- Decomposição 3NF
CREATE TABLE departments (
department_id text PRIMARY KEY,
department_name text NOT NULL,
department_head text NOT NULL
);
CREATE TABLE employee_departments (
employee_id text NOT NULL,
department_id text NOT NULL REFERENCES departments (department_id),
PRIMARY KEY (employee_id, department_id)
);
INSERT INTO departments (department_id, department_name, department_head)
VALUES ('D1', 'Engineering', 'Alex');
INSERT INTO employee_departments (employee_id, department_id)
VALUES ('E100', 'D1');
-- Verificar: atributos do departamento aparecem uma vez
SELECT department_id, COUNT(*) FROM departments GROUP BY 1;
COMMIT;O que isso demonstra:
department_namedepende dedepartment_id, não de(employee_id, department_id)- dependência transitiva removida.- Dados de referência (
departments) separados de fatos (employee_departments). - Atualizações no chefe do departamento afetam uma linha.
Mergulho Profundo
Formas Normais em Resumo
| Forma | Regra (prática) | Correção Típica |
|---|---|---|
| 1NF | Colunas atômicas, sem grupos repetidos | Dividir tag1,tag2 em linhas |
| 2NF | Sem dependência parcial de chave composta | Mover nome do produto de (order_id, product_id) se depende apenas de product_id |
| 3NF | Sem dependência transitiva em atributos não-chave | Tabela de consulta zip_codes |
| BCNF | Todo determinante é uma chave candidata | Decompor course_id -> instructor_id |
Como Funciona
- Uma dependência funcional
X -> Ysignifica que duas linhas com o mesmoXdevem ter o mesmoY. - 3NF: cada coluna não-chave depende da chave, da chave inteira e nada além da chave.
- BCNF: mais rigoroso - para cada dependência
X -> Y,Xdeve ser uma superchave. - Esquemas OLTP raramente precisam além de BCNF; esquemas estrela para análise intencionalmente desnormalizam.
Notas SQL
-- Encontrar determinantes duplicados com valores conflitantes (sinal de 3NF)
SELECT product_id, COUNT(DISTINCT product_name) AS names
FROM order_items_bad
GROUP BY product_id
HAVING COUNT(DISTINCT product_name) > 1;Armadilhas
- Normalizar demais dimensões - 6 junções para renderizar um cartão de produto. Correção: desnormalizar modelos de leitura ou views materializadas após medir o problema.
- Ignorar chaves compostas - dependências parciais se escondem em tabelas
(order_id, line_no). Correção: perguntar de que cada coluna não-chave depende. - Obsessão por BCNF em fatos - dividir cada dependência pode prejudicar a clareza. Correção: parar em 3NF a menos que contradições apareçam nos dados.
- Chaves naturais como PK única entre inquilinos -
skusozinho falha em SaaS multi-inquilino. Correção: chave composta(tenant_id, sku)ou substituta maisUNIQUEcom escopo. - Tabela de consulta sem FK -
zipem clientes mas sem tabelazip_codes. Correção: adicionar tabela de referência ouCHECKcontra conjunto conhecido.
Alternativas
| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
| Parar em 2NF | Esquemas simples, sem dependências transitivas visíveis | Atributos de localização duplicados em fatos |
| Desnormalizar para leituras | Caminho de junção quente comprovado | Antes que a normalização seja compreendida |
jsonb para atributos esparsos | Campos opcionais raros | Relatórios determinísticos sobre esses campos |
| Esquema estrela de data warehouse | Análise | Caminho de escrita OLTP |
FAQs
Preciso de BCNF para toda tabela?
Não. A maioria das tabelas OLTP de produção atinge 3NF e para. Aplique BCNF quando descobrir determinantes não-chave causando anomalias de atualização.
O que é uma dependência parcial?
Em chave composta (A, B), a coluna C depende apenas de B. Mova C para uma tabela com chave B.
O que é uma dependência transitiva?
key -> X -> Y. A coluna Y não deve permanecer na tabela original se X não for uma superchave.
Como a multi-inquilino afeta as formas normais?
Inclua tenant_id em chaves e dependências. UNIQUE (tenant_id, sku) não UNIQUE (sku) global a menos que SKUs sejam globais.
Constraints CHECK podem substituir a normalização?
Elas ajudam em invariantes, mas não removem anomalias de atualização. Você ainda atualiza muitas linhas quando fatos duplicados mudam.
PK substituta é suficiente para 3NF?
Chaves substitutas escondem chaves de negócio compostas. Você ainda deve decompor dependências transitivas entre colunas não-chave.
Como normalizar rapidamente uma importação CSV?
Carregue para staging, SELECT DISTINCT chaves candidatas, crie tabelas de dimensão, e então insira fatos com FKs.
3NF prejudica o desempenho do índice?
Mais junções podem custar leituras; indexe colunas FK e meça. Desnormalize seletivamente após evidências, não preventivamente.
E os dados temporais?
Adicione valid_from / valid_to em tabelas de dimensão. A normalização ainda se aplica; tabelas de histórico não são desculpa para duplicatas amplas.
Como views materializadas se relacionam com 3NF?
Mantenha OLTP em 3NF; projete formas desnormalizadas em views materializadas para dashboards com muitas leituras.
Relacionados
- Noções Básicas de Normalização - exemplos de anomalias
- Desnormalização Intencional - redundância controlada
- Views Materializadas - cópias otimizadas para leitura
- Explosão de Junção Fan-Out - junções ruins de decomposição ruim
Versões da Stack: Esta página foi escrita para PostgreSQL 18.4 (estável 18, manutenção 17), pgvector 0.8+, PgBouncer 1.x, Patroni 3.x, e PostGIS 3.5+.