Diseño de bases de datos · Ejemplo hasta 3FN

Normalización de bases de datos: de una tabla a 3FN

Transforma una tabla de pedidos en un modelo que evita inconsistencias. Sigue las dependencias, conserva el precio histórico y comprueba el resultado con SQL.

Actualizado: 29 septiembre 2026Ejemplos comprobados en SQLite
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_id determina un cliente y una fecha.
  • Cada cliente_id determina el nombre actual del cliente.
  • Cada producto_id determina 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.

Tabla en 1FN: todavía repite datos de clientes y productos
PedidoLíneaCliente (id / nombre)Producto (id / nombre)CantidadPrecio en céntimos
10117 / Ana20 / Cuaderno2350
10127 / Ana21 / Lápiz3100
10218 / Luis20 / Cuaderno1400

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.

Separación al alcanzar 2FN en este ejemplo
TablaClaveOtros datos
pedidospedido_idcliente_id, nombre_cliente, fecha
lineaspedido_id + numero_lineaproducto_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.

Modelo final y responsabilidad de cada tabla
TablaQué representaRelación
clientesUn cliente y su nombre actualUn cliente puede tener varios pedidos.
productosUn producto y su nombre actualUn producto puede aparecer en varias líneas.
pedidosUn pedido, su cliente y su fechaCada pedido pertenece a un cliente.
lineasUna posición de un pedido con producto, cantidad y precioRelaciona 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.

SQLite · tablas y restricciones
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.

SQLite · datos del ejemplo
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.

SQLite · total de cada pedido
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;
Resultado comprobado: total de los pedidos
pedido_idclientetotal_centimos
101Ana1000
102Luis400

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
SQLite · unidades por producto
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.