3NF y BCNF
Normalización práctica sin exageración académica: descompón tablas hasta que los atributos no clave dependan de la clave completa, nada más que la clave.
Receta
Tarjeta de referencia rápida - lista para copiar y pegar.
-- Viola 3NF: city depende de zip, no solo de customer_id
-- customers(customer_id, email, zip, city)
-- Solución 3NF: eliminar dependencia 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)
);
-- Solución BCNF: cuando el determinante no es una superclave
-- enrollments(student_id, course_id, instructor_id) con la regla 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)
);Cuándo usar esto:
- Al importar CSVs a una única tabla de staging amplia antes del DDL de producción.
- Al revisar si una nueva columna pertenece a una tabla de hechos (fact table) o a una tabla de dimensiones (dimension table).
- Al depurar duplicados contradictorios en informes (mismo ID, descripciones diferentes).
Ejemplo de Trabajo
BEGIN;
-- Punto de partida: no es 3NF
CREATE TABLE employee_assignments_bad (
employee_id text,
department_id text,
department_name text,
department_head text,
PRIMARY KEY (employee_id, department_id)
);
-- Descomposición 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: los atributos del departamento aparecen una sola vez
SELECT department_id, COUNT(*) FROM departments GROUP BY 1;
COMMIT;Lo que esto demuestra:
department_namedepende dedepartment_id, no de(employee_id, department_id)- se eliminó la dependencia transitiva.- Datos de referencia (
departments) separados de los hechos (employee_departments). - Las actualizaciones del jefe de departamento afectan a una sola fila.
Profundización
Formas Normales de un Vistazo
| Forma | Regla (práctica) | Solución típica |
|---|---|---|
| 1NF | Columnas atómicas, sin grupos repetidos | Dividir tag1,tag2 en filas |
| 2NF | Sin dependencia parcial de clave compuesta | Mover el nombre del producto de (order_id, product_id) si solo depende de product_id |
| 3NF | Sin dependencia transitiva en atributos no clave | Tabla de búsqueda de zip_codes |
| BCNF | Cada determinante es una clave candidata | Descomponer course_id -> instructor_id |
Cómo Funciona
- Una dependencia funcional
X -> Ysignifica que dos filas con el mismoXdeben tener el mismoY. - 3NF: cada columna no clave depende de la clave, de toda la clave y de nada más que la clave.
- BCNF: más estricto - para cada dependencia
X -> Y,Xdebe ser una superclave. - Los esquemas OLTP rara vez necesitan más allá de BCNF; los esquemas de estrella para análisis desnormalizan intencionalmente.
Notas SQL
-- Encontrar determinantes duplicados con valores conflictivos (indicio 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;Trampas
- Sobrenormalización de dimensiones - 6 uniones para renderizar una tarjeta de producto. Solución: desnormalizar modelos de lectura o vistas materializadas después de medir el problema.
- Ignorar claves compuestas - las dependencias parciales se ocultan en tablas
(order_id, line_no). Solución: preguntar de qué depende cada columna no clave. - Obsesión con BCNF en hechos - dividir cada dependencia puede perjudicar la claridad. Solución: detenerse en 3NF a menos que aparezcan contradicciones en los datos.
- Claves naturales como PK única entre inquilinos -
skusolo falla en SaaS multi-inquilino. Solución: clave compuesta(tenant_id, sku)o clave sustituta másUNIQUEcon ámbito. - Tabla de búsqueda sin FK -
zipen clientes pero sin tablazip_codes. Solución: añadir tabla de referencia oCHECKcontra un conjunto conocido.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
| Detenerse en 2NF | Esquemas simples, sin dependencias transitivas visibles | Atributos de ubicación duplicados en hechos |
| Desnormalizar para lecturas | Ruta de unión "caliente" probada | Antes de comprender la normalización |
jsonb para atributos dispersos | Campos opcionales raros | Informes deterministas sobre esos campos |
| Esquema de estrella de data warehouse | Analítica | Ruta de escritura OLTP |
Preguntas Frecuentes
¿Necesito BCNF para cada tabla?
No. La mayoría de las tablas OLTP de producción alcanzan 3NF y se detienen. Aplica BCNF cuando descubras determinantes no clave que causen anomalías de actualización.
¿Qué es una dependencia parcial?
En una clave compuesta (A, B), la columna C depende solo de B. Mueve C a una tabla con clave B.
¿Qué es una dependencia transitiva?
clave -> X -> Y. La columna Y no debe permanecer en la tabla original si X no es una superclave.
¿Cómo afecta la multi-inquilino a las formas normales?
Incluye tenant_id en claves y dependencias. UNIQUE (tenant_id, sku) no UNIQUE (sku) global a menos que los SKUs sean globales.
¿Pueden las restricciones CHECK reemplazar la normalización?
Ayudan con los invariantes pero no eliminan las anomalías de actualización. Aún actualizas muchas filas cuando cambian hechos duplicados.
¿Es suficiente la PK sustituta para 3NF?
Las claves sustitutas ocultan las claves de negocio compuestas. Aún debes descomponer las dependencias transitivas entre las columnas no clave.
¿Cómo normalizo rápidamente una importación CSV?
Carga a staging, SELECT DISTINCT de las claves candidatas, crea tablas de dimensión, luego inserta los hechos con FKs.
¿Perjudica 3NF el rendimiento de los índices?
Más uniones pueden costar en lecturas; indexa las columnas FK y mide. Desnormaliza selectivamente después de tener evidencia, no de forma preventiva.
¿Qué pasa con los datos temporales?
Añade valid_from / valid_to en las tablas de dimensión. La normalización sigue aplicándose; las tablas de historial no son excusa para duplicados amplios.
¿Cómo se relacionan las vistas materializadas con 3NF?
Mantén las OLTP en 3NF; proyecta formas desnormalizadas en vistas materializadas para dashboards con muchas lecturas.
Relacionado
- Conceptos Básicos de Normalización - ejemplos de anomalías
- Desnormalización Intencional - redundancia controlada
- Vistas Materializadas - copias optimizadas para lectura
- Explosión de Uniones Fan-Out - uniones incorrectas por mala descomposición
Versiones de Stack: Esta página fue escrita para PostgreSQL 18.4 (estable 18, mantenimiento 17), pgvector 0.8+, PgBouncer 1.x, Patroni 3.x, y PostGIS 3.5+.