Claves naturales y desnormalización
En resumen: Una clave natural sale de los propios datos, como un correo; una clave sustituta es un número sin significado, y la desnormalización es una copia de datos que guardas a propósito.
Problema
María cambió su correo
Si diseñas tú los clientes, el correo parece una buena clave primaria. Es único, todo cliente tiene uno, es con lo que inicia sesión. Aquí hay un diseño así: los clientes tienen el correo como clave, y los pedidos apuntan al correo.
customers
- PK
emailtext nametext *citytext
orders
- PK
idinteger - FK
customer_emailtext *→ customers ordered_attext *
María cambia su correo:
La base de datos rechaza el cambio, porque tres pedidos apuntan al correo antiguo. Si cambias también esos, quedan las facturas ya enviadas, los informes y todos los demás sistemas que recordaron la clave antigua.
La idea
Claves naturales y sustitutas
Una clave tomada del mundo (un correo, un teléfono, el número de un libro) es una clave natural. Un número que la base de datos asigna sola, sin significado fuera de ella, es una clave sustituta. El id de la tienda es una.
Una clave sustituta nunca cambia, porque nada en el mundo la cambia. Así el correo cambia en una fila, y los pedidos ni se enteran:
El correo conserva su regla. Ya no es la clave, pero es UNIQUE, así que dos clientes no pueden tener el mismo correo:
¿Y un número de identificación nacional (en Bulgaria, el EGN)? No. Son datos personales, y la clave se copia en cada tabla que apunta al cliente. Un cliente del extranjero normalmente no lo tiene. Y la tienda no necesita saberlo.
Más precisamente, para curiosos
ON UPDATE CASCADE en REFERENCES hace que la base de datos cambie también las filas que apuntan a la clave antigua. Eso ayuda dentro de la base de datos, pero no con las copias de la clave fuera de ella: en correos, en registros, en otro sistema.
Una clave natural encaja cuando el valor realmente nunca cambia y se conoce desde fuera. Ejemplos son un código de moneda (EUR) o un código de país (BG).
Un UUID es otro tipo de clave sustituta: un número de 128 bits escrito como 550e8400-e29b-41d4-a716-446655440000. Se puede generar en cualquier parte, sin preguntar a la base de datos (en la aplicación, en un teléfono sin internet), dos bases de datos se pueden fusionar sin choques, y la dirección /orders/1043 ya no dice cuántos pedidos tiene la tienda. El costo es que es más largo (16 bytes frente a 8 de un entero grande), y los UUID aleatorios caen en lugares aleatorios del índice y lo ralentizan. Por eso existen también los UUID ordenados por tiempo (versión 7). PostgreSQL tiene un tipo uuid y una función gen_random_uuid(). SQLite no tiene un tipo aparte y lo guarda como texto o como BLOB.
Pruébalo
Un correo nuevo
El cliente 12, Boris, escribe que su nuevo correo es boris.m@example.com. Corrígelo.
Solución
UPDATE customers SET email = 'boris.m@example.com' WHERE id = 12;UPDATE customers SET email = 'boris.m@example.com' WHERE id = 12. Una fila. Sus tres pedidos apuntan a id 12 y siguen siendo suyos.
La idea
El precio en la fila del pedido
order_items tiene unit_price, y products tiene price. Parece una repetición que la normalización quita. Sube el precio del café:
Los pedidos antiguos recuerdan 14,90 €, mientras el producto cuesta 15,90 €. No son dos copias de un hecho, sino dos hechos distintos: «lo que cuesta hoy» y «lo que pagó el cliente». Si el pedido tomara su precio de products, cada subida de precio cambiaría también las facturas pasadas.
La idea
Desnormalización
El total del pedido es diferente. De verdad se calcula a partir de las líneas. Pero la lista de pedidos de la web lo muestra para cada pedido, y en lugar de sumar las líneas cada vez, puedes guardarlo en orders:
Una copia deliberada de datos que se podrían calcular se llama desnormalización. La haces cuando leer es mucho más frecuente que cambiar y el cálculo es caro.
Míralo
El total que miente
Un compañero añade café al pedido 60 pero se olvida de total:
La web muestra 14 €, mientras las líneas suman 28,90 €. Nada en la base de datos lo impidió. El costo de la copia es que cada cambio en las líneas debe cambiar también el total: en el programa, en cada lugar que escribe en order_items, o con un trigger (el módulo «Transacciones»).
Ejercicio
Dos tareas
1. Contabilidad sospecha que algunos totales están mal. Encuentra los pedidos en que total no es igual a la suma de las líneas. Muestra el número, el total guardado y el real.
Los pedidos con un total incorrecto:
Solución
SELECT o.id, o.total, SUM(oi.quantity * oi.unit_price) AS items
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id, o.total
HAVING ROUND(o.total, 2) <> ROUND(SUM(oi.quantity * oi.unit_price), 2);Agrupas las líneas por pedido y comparas en HAVING. Dos pedidos: el 59 (13,90 € guardados, mientras las líneas dan 8,90 €) y el 60. ROUND(…, 2) evita que los decimales difieran en la última cifra. Una vez que guardas copias, una consulta de control así se ejecuta con regularidad.
2. Corrige los totales de los dos pedidos calculándolos de nuevo a partir de las líneas.
Actualiza total de los pedidos 59 y 60:
Solución
UPDATE orders SET total = (
SELECT SUM(quantity * unit_price) FROM order_items
WHERE order_items.order_id = orders.id
)
WHERE id IN (59, 60);La misma subconsulta correlacionada que llenó la columna, pero solo para id IN (59, 60). Si actualizas todos los pedidos, el resultado es el mismo, porque los demás ya están bien.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Claves foráneas: vínculos entre tablas · ALTER TABLE: cambiar una tabla · Tipos de relación: 1:1, 1:N, M:N · Normalización
- Continúa en: OLTP y OLAP · Bases de datos documentales · Bases de datos columnares y series temporales · La panadería, etapa 1: la base de datos y las tablas
- Ejercicios sobre el tema: Diseñar una base de datos