Contenido de esta guía
Qué es la normalización de bases de datos
La normalización organiza las tablas de una base de datos relacional según las dependencias entre sus atributos. Su objetivo es reducir redundancias que puedan provocar datos contradictorios al insertar, modificar o borrar. Las primeras etapas se conocen como primera, segunda y tercera forma normal: 1FN, 2FN y 3FN.
No consiste en separar cada columna en una tabla ni en eliminar cualquier valor repetido. Por ejemplo, repetir un identificador de cliente en varios pedidos expresa una relación válida. El problema sería guardar varias copias de su nombre actual y tener que actualizarlas todas a la vez.
El problema: un pedido con listas dentro de sus columnas
Imagina una hoja con una fila por pedido y columnas productos, cantidades y precios. El pedido 101 contiene «Cuaderno, Lápiz», «2, 3» y «350, 100». Para saber cuánto se vendió de cada producto, tendrías que dividir tres listas y confiar en que todas mantengan el mismo orden.
Además, cada pedido repite el nombre del cliente y los nombres actuales de los productos. Esto genera tres problemas concretos:
- Actualización: cambiar el nombre de un cliente exige revisar todos sus pedidos; si falta uno, hay dos versiones del nombre.
- Inserción: no puedes registrar un nuevo producto sin inventar un pedido que lo contenga.
- Borrado: si eliminas el único pedido de un producto, también podrías perder sus datos de catálogo.
Vamos a diseñar tablas separadas a partir de las reglas de este negocio ficticio. El capítulo de normalización de Database Design explica las formas normales y las dependencias que fundamentan este proceso.
Primero identifica las claves y las dependencias funcionales
Una dependencia funcional A → B significa que un valor de A determina un único valor de B en cualquier estado válido del modelo. No basta con que ocurra por casualidad en una muestra. En nuestra tienda, estas son reglas explícitas:
- Cada
pedido_iddetermina un cliente y una fecha. - Cada
cliente_iddetermina el nombre actual del cliente. - Cada
producto_iddetermina el nombre actual del producto. - El par
(pedido_id, numero_linea)determina el producto, la cantidad y el precio acordado en esa línea.
El número de línea empieza dentro de cada pedido. Por eso la línea 1 del pedido 101 y la línea 1 del 102 son distintas. Permitimos que un mismo producto aparezca en dos líneas de un pedido; no usamos (pedido_id, producto_id) como clave. Si el negocio prohibiera esa repetición, habría que expresarlo con otra restricción.
Una clave candidata es un conjunto mínimo de atributos que identifica una fila. Puede haber más de una; la primaria es la elegida para identificarla habitualmente. La normalización considera las dependencias respecto de las claves candidatas, no solo la columna marcada como PRIMARY KEY.
1FN: una fila por línea y valores del dominio elegido
En nuestro caso, llegar a 1FN significa quitar las listas paralelas y representar cada línea como una fila. Los valores se tratan como unidades del dominio definido: no hace falta dividir cualquier texto en caracteres o palabras. Sí necesitamos separar los productos porque queremos consultarlos y relacionarlos individualmente.
| Pedido | Línea | Cliente (id / nombre) | Producto (id / nombre) | Cantidad | Precio en céntimos |
|---|---|---|---|---|---|
| 101 | 1 | 7 / Ana | 20 / Cuaderno | 2 | 350 |
| 101 | 2 | 7 / Ana | 21 / Lápiz | 3 | 100 |
| 102 | 1 | 8 / Luis | 20 / Cuaderno | 1 | 400 |
La tabla también tendría la fecha del pedido: 10 de septiembre para el 101 y 11 de septiembre para el 102. Cada fila ya tiene una clave, pero seguimos repitiendo información que no pertenece exclusivamente a la línea. Estar en 1FN no resuelve por sí solo las anomalías de actualización.
2FN: elimina las dependencias de una parte de la clave
En 2FN, además de cumplir 1FN, cada atributo no primo depende de la totalidad de cada clave candidata y no solo de una parte. Un atributo es primo si forma parte de alguna clave candidata. En nuestro ejemplo asumimos que la única clave candidata de la tabla de líneas es (pedido_id, numero_linea).
La fecha y el cliente dependen únicamente de pedido_id. No necesitan el número de línea para quedar determinados. Los movemos a una tabla de pedidos. Conservamos en las líneas el producto, su nombre, la cantidad y el precio acordado; el siguiente paso resolverá la dependencia del nombre del producto.
| Tabla | Clave | Otros datos |
|---|---|---|
| pedidos | pedido_id | cliente_id, nombre_cliente, fecha |
| lineas | pedido_id + numero_linea | producto_id, nombre_producto, cantidad, precio_unitario_centimos |
Añadir un id artificial a la tabla original no arreglaría automáticamente esas dependencias. Hay que seguir analizando las reglas y las demás claves candidatas. El propósito de una clave técnica es identificar; el de normalizar es organizar los hechos que dependen de esa identificación.
3FN: separa las dependencias transitivas que sobran
En pedidos tenemos pedido_id → cliente_id → nombre_cliente. En líneas tenemos (pedido_id, numero_linea) → producto_id → nombre_producto. El nombre actual del cliente pertenece al cliente; el del producto pertenece al catálogo. Los trasladamos a sus propias tablas.
Formalmente, en 3FN toda dependencia funcional no trivial X → A cumple que X es una superclave o A es un atributo primo. Una superclave identifica una fila, aunque pueda contener atributos de más; una dependencia es no trivial cuando el atributo de la derecha no forma parte del conjunto de la izquierda. Para este modelo, con las claves y reglas indicadas, basta con separar esos datos descriptivos para eliminar las dependencias transitivas de atributos no primos.
| Tabla | Qué representa | Relación |
|---|---|---|
| clientes | Un cliente y su nombre actual | Un cliente puede tener varios pedidos. |
| productos | Un producto y su nombre actual | Un producto puede aparecer en varias líneas. |
| pedidos | Un pedido, su cliente y su fecha | Cada pedido pertenece a un cliente. |
| lineas | Una posición de un pedido con producto, cantidad y precio | Relaciona pedidos con productos. |
El precio de 350 del Cuaderno en el pedido 101 y el de 400 en el 102 se conservan en las líneas. Son precios acordados en ventas distintas. No existe aquí la regla «producto_id determina precio de venta»: descuentos y cambios de tarifa hacen que esa afirmación sea falsa.
Diseño normalizado en SQL: crear, insertar y consultar
El siguiente script está preparado para SQLite en una conexión nueva de pruebas. Activa las claves foráneas antes de iniciar una transacción. Los nombres terminados en _normal_demo distinguen estas tablas de las del curso. Las cantidades monetarias se guardan como céntimos enteros.
PRAGMA foreign_keys = ON;
CREATE TABLE clientes_normal_demo (
id INTEGER PRIMARY KEY,
nombre TEXT NOT NULL
);
CREATE TABLE productos_normal_demo (
id INTEGER PRIMARY KEY,
nombre TEXT NOT NULL
);
CREATE TABLE pedidos_normal_demo (
id INTEGER PRIMARY KEY,
cliente_id INTEGER NOT NULL,
fecha TEXT NOT NULL,
FOREIGN KEY (cliente_id) REFERENCES clientes_normal_demo(id)
);
CREATE TABLE lineas_normal_demo (
pedido_id INTEGER NOT NULL,
numero_linea INTEGER NOT NULL,
producto_id INTEGER NOT NULL,
cantidad INTEGER NOT NULL CHECK (cantidad > 0),
precio_unitario_centimos INTEGER NOT NULL
CHECK (precio_unitario_centimos >= 0),
PRIMARY KEY (pedido_id, numero_linea),
FOREIGN KEY (pedido_id) REFERENCES pedidos_normal_demo(id),
FOREIGN KEY (producto_id) REFERENCES productos_normal_demo(id)
);Las claves primarias identifican filas y las foráneas comprueban referencias. NOT NULL exige las relaciones obligatorias; CHECK impide cantidades negativas o nulas y precios negativos. Las restricciones ayudan a aplicar el diseño, pero no pueden decidir por ti cuáles son las dependencias del negocio.
INSERT INTO clientes_normal_demo (id, nombre)
VALUES (7, 'Ana'), (8, 'Luis');
INSERT INTO productos_normal_demo (id, nombre)
VALUES (20, 'Cuaderno'), (21, 'Lápiz');
INSERT INTO pedidos_normal_demo (id, cliente_id, fecha)
VALUES (101, 7, '2026-09-10'), (102, 8, '2026-09-11');
INSERT INTO lineas_normal_demo
(pedido_id, numero_linea, producto_id, cantidad,
precio_unitario_centimos)
VALUES
(101, 1, 20, 2, 350),
(101, 2, 21, 3, 100),
(102, 1, 20, 1, 400);Podemos reconstruir el informe mediante JOIN sin duplicar los nombres actuales en cada línea. Esta consulta incluye pedidos que tienen al menos una línea, porque utiliza INNER JOIN.
SELECT pe.id AS pedido_id, c.nombre AS cliente,
SUM(l.cantidad * l.precio_unitario_centimos) AS total_centimos
FROM pedidos_normal_demo AS pe
JOIN clientes_normal_demo AS c ON c.id = pe.cliente_id
JOIN lineas_normal_demo AS l ON l.pedido_id = pe.id
GROUP BY pe.id, c.nombre
ORDER BY pe.id;| pedido_id | cliente | total_centimos |
|---|---|---|
| 101 | Ana | 1000 |
| 102 | Luis | 400 |
El pedido 101 suma 2 × 350 + 3 × 100 = 1000 céntimos. El 102 suma 1 × 400 = 400. El mismo producto aparece con precios distintos y cada pedido conserva su total correcto.
Descargar el ejemplo normalizado
Qué cambia al usar otro motor
Las dependencias y las formas normales no dependen de SQLite. El DDL sí: PRAGMA foreign_keys es específico de SQLite. PostgreSQL, MySQL y SQL Server tienen sus propios tipos, opciones de identidad y reglas de restricciones. Para producción, elige un tipo de fecha nativo cuando exista y un tipo monetario exacto o una unidad entera adecuada al negocio. Consulta la guía de tipos de datos antes de trasladar las declaraciones literalmente.
Histórico, desnormalización y límites de la regla
Este modelo muestra el nombre actual del cliente. Si debes conservar el nombre o la dirección que figuraban en una factura, guarda explícitamente esa versión histórica en el documento o en un modelo de versiones. «Nombre actual» y «nombre al emitir la factura» son hechos diferentes; no elimines un dato histórico necesario por parecer repetido.
Tampoco se deduce que normalizar siempre haga todas las consultas más rápidas. Un almacén analítico o una tabla de resumen puede repetir datos por un motivo medido. Si desnormalizas, define qué dato es la fuente, cuándo se recalcula la copia y cómo detectas divergencias. Empieza por un diseño correcto y justifica cada duplicación con una necesidad concreta.
En nuestro esquema, registrar un pedido sin líneas todavía es posible: las claves foráneas exigen que cada línea tenga un pedido, pero no que cada pedido tenga líneas. Si tu proceso requiere al menos una al confirmar una venta, aplica esa regla en la operación de confirmación. Una base normalizada también necesita reglas de negocio.
Ejercicio: cuántas unidades se vendieron de cada producto
Usa el modelo normalizado. Muestra el identificador, el nombre actual y las unidades vendidas de cada producto con ventas. El Cuaderno se vendió a dos precios: eso no debe dividirlo en dos productos.
Ver solución y explicación
SELECT p.id, p.nombre,
SUM(l.cantidad) AS unidades
FROM productos_normal_demo AS p
JOIN lineas_normal_demo AS l ON l.producto_id = p.id
GROUP BY p.id, p.nombre
ORDER BY p.id;Obtienes 20, Cuaderno, 3 y 21, Lápiz, 3. Agrupas por la identidad del producto y su nombre, y sumas cantidades. El precio pertenece a cada venta y no forma parte de la agrupación solicitada.
Preguntas frecuentes
¿Normalizar es quitar filas duplicadas con DISTINCT?
No. DISTINCT actúa sobre la salida de una consulta. Normalizar revisa el modelo y sus dependencias para evitar inconsistencias al modificar datos. Quitar duplicados de un resultado no corrige dónde se guarda un hecho.
¿Una tabla con clave primaria ya está en 3FN?
No. La clave permite identificar, pero una fila puede seguir guardando atributos que dependen solo de parte de una clave o de otro atributo. Hay que examinar las reglas, no solo la sintaxis de CREATE TABLE.
Qué debes recordar
- Declara las reglas del negocio antes de deducir dependencias.
- 1FN elimina listas repetidas en este modelo; 2FN resuelve dependencias parciales; 3FN resuelve las transitivas descritas.
- El precio vendido pertenece a la línea; el nombre actual del producto pertenece al catálogo.
- Prueba las restricciones y reconstruye informes con JOIN para comprobar el diseño.
Continúa aprendiendo
Fuentes y alcance de la validación
El caso de la tienda, sus reglas, datos y ejercicios son una elaboración propia. Se han ejecutado las tablas, inserciones y consultas en SQLite, y se han comprobado rechazos de referencias inexistentes y claves de línea duplicadas.