Normalización
En resumen: La normalización divide los datos en tablas para que cada hecho se guarde en un solo lugar, lo que elimina las anomalías de actualización, inserción y borrado.
Problema
Excel llegó a la base de datos
¿Recuerdas la tabla de pedidos de la primera lección del curso? Alguien la ha pasado a la base de datos tal como estaba: una fila por cada producto de un pedido, y al lado la fecha, el cliente, su correo y su ciudad.
flat_orders
order_idinteger *ordered_attext *customertext *emailtext *citytextproducttext *pricereal *quantityinteger *
11 filas, y solo tres clientes. El nombre y el correo de María están escritos 6 veces. Mira también su ciudad en el pedido 6.
La idea
Anomalías
Cuando un hecho está escrito en muchos sitios, cada cambio es un riesgo. Estos problemas se llaman anomalías. Hay tres tipos.
De actualización. María cambia su correo. Un compañero lo corrige solo en su último pedido:
Ahora la base de datos dice dos cosas a la vez. ¿Cuál es el correo correcto?
Míralo
Inserción y borrado
De inserción. La tienda empieza a vender copos de avena. Nadie los ha pedido todavía. ¿Dónde anotas su precio?
Una fila sin pedido no puede entrar. El producto existe solo cuando alguien lo compra.
De borrado. Elena cancela el pedido 4, y se borra. Era el único pedido con la sartén:
Junto con el pedido desaparece el hecho de que la tienda vende una sartén por 24,90 €.
La idea
Formas normales
Organizar las tablas para que cada hecho esté en un solo lugar se llama normalización. Las reglas vienen por pasos, las formas normales. Cada una incluye la anterior.
Primera (1FN): un valor por celda. Sin listas como «vegano, ecológico», y sin columnas tag1, tag2, tag3.
Solo el pan blanco. El pan integral es vegano, pero su celda dice «vegano, ecológico». Con LIKE '%vegano%' lo encontrarás, pero no puedes contar productos por etiqueta, corregir «ecológico» en todas partes ni evitar una falta de ortografía. Las etiquetas van en una tabla de enlace, como en la primera lección de este módulo.
Segunda (2FN): cada columna depende de toda la clave. Esto importa cuando la clave tiene varias columnas:
order_lines
- PK
order_idinteger - PK
product_idinteger product_nametext *quantityinteger *
El nombre del producto depende solo de product_id, no del pedido. Se repetirá en cada pedido con ese producto. Va en products.
Tercera (3FN): las columnas dependen solo de la clave, no de otra columna:
products
- PK
idinteger nametext *category_idinteger *category_nametext *pricereal *
category_name lo decide category_id, no el producto. Si se cambia el nombre de la categoría, hay que corregir cada producto que está en ella. El nombre va en categories.
Más precisamente, para curiosos
La fórmula corta de las tres formas: cada columna depende de «la clave, toda la clave y nada más que la clave».
También hay formas superiores (Boyce-Codd, cuarta, quinta), pero en la práctica una tabla en tercera forma normal suele dejar de tener estas anomalías.
flat_orders rompe la segunda forma normal. La clave de una fila es el pedido y el producto, mientras que la fecha y el cliente dependen solo del pedido. También rompe la tercera: la ciudad y el correo dependen del cliente, no del pedido.
Pruébalo
¿Qué forma?
La idea
La división
Divides flat_orders en las tablas de la tienda: customers, products, orders y order_items. Empiezas por las que no apuntan a nada.
INSERT puede tomar sus filas de un SELECT en lugar de VALUES. Con DISTINCT, cada producto entra una vez:
products
- PK
idinteger nametext *pricereal *
De 11 filas salen 6 productos, cada uno con un id. El precio también se queda en las filas del pedido, porque es el precio al que se compró. Por qué eso no es una repetición lo verás en la próxima lección.
Míralo
Excel está sucio
Lo mismo para los clientes:
customers
- PK
idinteger nametext *emailtext *citytext
Un error. En el pedido 6, la ciudad de María es «Sofia», sin tilde. DISTINCT compara la fila entera, así que ve dos clientes con un correo, y UNIQUE no los deja entrar. Es bueno: la regla atrapó el error en lugar de llevarlo a la base nueva.
Primero corriges los datos, luego divides:
Casi toda mudanza desde Excel empieza con una limpieza como esta.
Pruébalo
Los pedidos
orders
- PK
idinteger - FK
customer_idinteger *→ customers ordered_attext *
Los productos y los clientes ya están divididos. Rellena orders (id, customer_id, ordered_at) a partir de flat_orders. El número de pedido es order_id, y el cliente se encuentra por el correo.
Solución
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers(id),
ordered_at TEXT NOT NULL
);
INSERT INTO orders (id, customer_id, ordered_at)
SELECT DISTINCT f.order_id, c.id, f.ordered_at
FROM flat_orders f
JOIN customers c ON c.email = f.email;INSERT INTO orders (id, customer_id, ordered_at) SELECT DISTINCT f.order_id, c.id, f.ordered_at FROM flat_orders f JOIN customers c ON c.email = f.email. Unes por el correo porque es único, mientras que un nombre puede repetirse. DISTINCT hace falta porque un pedido con dos productos está en dos filas. 7 pedidos.
Ejercicio
Dos tareas
1. Lo que queda es la tabla de enlace entre pedidos y productos.
order_items
- PK
order_idinteger→ orders - PK
product_idinteger→ products quantityinteger *unit_pricereal *
Rellena order_items a partir de flat_orders: el número de pedido, el id del producto, la cantidad y el precio.
Solución
CREATE TABLE order_items (
order_id INTEGER NOT NULL REFERENCES orders(id),
product_id INTEGER NOT NULL REFERENCES products(id),
quantity INTEGER NOT NULL,
unit_price REAL NOT NULL,
PRIMARY KEY (order_id, product_id)
);
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
SELECT f.order_id, p.id, f.quantity, f.price
FROM flat_orders f
JOIN products p ON p.name = f.product;Cada fila de flat_orders se convierte en una fila de order_items, así que aquí no hay DISTINCT. El producto se encuentra por el nombre, que ahora es UNIQUE en products. 11 filas, tantas como al principio.
2. Ahora el informe de la primera lección del curso se hace con una sola consulta. ¿Cuánto ha gastado cada cliente?
Muestra el nombre de cada cliente y el total de sus compras (cantidad por precio):
Solución
SELECT c.name, SUM(oi.quantity * oi.unit_price) AS spent
FROM customers c
JOIN orders o ON o.customer_id = c.id
JOIN order_items oi ON oi.order_id = o.id
GROUP BY c.id, c.name;Tres tablas y GROUP BY. María 28,20 €, Georgi 25,20 €, Elena 31,20 €. Si María cambia su correo mañana, es una fila en customers.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Restricciones: reglas para los datos · Claves foráneas: vínculos entre tablas · Tipos de relación: 1:1, 1:N, M:N · Diagrama ER: de la descripción al esquema
- Continúa en: Claves naturales y desnormalización · Bases de datos documentales · La panadería, etapa 1: la base de datos y las tablas
- Ejercicios sobre el tema: Diseñar una base de datos