La idea
Una base de datos relacional es un conjunto de tablas. Cada tabla guarda un tipo de cosa: clientes, pedidos, productos. Cada fila es una de esas cosas, y cada columna es un dato sobre ella: la ciudad de un cliente, la fecha de un pedido, el precio de un producto.
Suena a hoja de cálculo, y casi lo es. La diferencia es la regla que hay detrás: cada dato se guarda una sola vez, en la tabla a la que pertenece, y las tablas se apuntan entre sí con claves. Luego una consulta reúne los datos que necesita desde donde estén.
Por qué existe
Imagina que Harbor & Pine funciona con una gran hoja: una fila por línea de pedido, con el nombre y la ciudad del cliente escritos a mano, más el producto y su categoría. Funciona durante una semana.
Figura 1
De una gran hoja a tablas separadas
| order_id | first_name | city | product_id | category |
|---|---|---|---|---|
| 1231 | Gary | Chicago | 165 | Textiles |
| 1231 | Gary | Chicago | 129 | Lighting |
| 1734 | Charles | Las Vegas | 166 | Textiles |
| 4230 | Gary | Chicago | 135 | Lighting |
| 4230 | Gary | Chicago | 191 | Decor |
| 4230 | Gary | Chicago | 177 | Textiles |
| customer_id | first_name | city |
|---|---|---|
| 69 | Gary | Chicago |
| 101 | Charles | Las Vegas |
| order_id | customer_id |
|---|---|
| 1231 | 69 |
| 1734 | 101 |
| 4230 | 69 |
| order_id | product_id |
|---|---|
| 1231 | 165 |
| 1231 | 129 |
| 1734 | 166 |
| 4230 | 135 |
| 4230 | 191 |
| 4230 | 177 |
- Seis líneas de pedido guardadas al estilo hoja de cálculo: una fila por línea, con todo lo del pedido y del cliente escrito en ella.
- El nombre y la ciudad de Gary están guardados cinco veces. El pedido 4230 está guardado tres veces. La hoja repite un dato cada vez que lo necesita.
- Gary se muda a Denver. Hay que cambiar cinco celdas. Si se te escapa una, la hoja dice que vive en dos ciudades a la vez, y cada informe que agrupa por ciudad queda mal sin que nadie lo note.
- Divídela. Cada cliente se convierte en una fila de
customers, con uncustomer_idque nunca cambia. - Cada pedido se convierte en una fila de
orders, que guarda solo elcustomer_idde quien lo hizo. - Las líneas van a
order_items, una fila por producto de un pedido. Ahora cada dato vive en un solo lugar, y la mudanza a Denver es una única actualización.
El problema de la hoja no es su tamaño. Es que el mismo dato está en muchas celdas, y las copias se van separando. Alguien corrige una errata en una fila y no en las demás. Un cliente cambia su correo y la mitad de su historial se queda con el viejo. Borrar el último pedido de un cliente borra el único registro de que ese cliente existe. Esas tres fallas tienen nombre (anomalías de actualización, de inserción y de borrado), y todas vienen de guardar un dato dos veces.
Dividir los datos en tablas arregla las tres de una vez, porque solo hay una copia que mantener
correcta. El precio es que la respuesta a una pregunta ahora está repartida en varias tablas. Para eso
sirve JOIN, y por eso tiene un capítulo entero.
Cómo funciona por dentro
Tablas, filas y columnas
Una tabla tiene una lista fija de columnas, cada una con un tipo: números enteros para quantity,
decimales para unit_price, fechas para order_date, texto para city. Cada fila tiene un valor (o
NULL, "sin valor") en cada columna. Las filas no tienen un orden propio. "La primera fila" no
significa nada hasta que una consulta dice ORDER BY.
La granularidad (grain) de una tabla es lo que representa una fila. En orders es un pedido; en
order_items, una línea de un pedido; en web_sessions, una visita al sitio. Conocer la granularidad
te dice qué cuenta COUNT(*) y qué números se pueden sumar sin problema.
Claves
Una clave primaria es la columna que distingue las filas entre sí. No hay dos filas que la
compartan y nunca cambia: customer_id en customers, order_id en orders. Los nombres son malas
claves (hay tres clientes distintos que se llaman Michael Chavez); los ids están hechos para ser claves.
Una clave foránea es una columna que guarda la clave primaria de otra tabla. orders.customer_id
dice qué cliente hizo el pedido. Es el hilo que sigue un JOIN.
Figura 2
Claves primarias y claves foráneas
| order_item_id | order_id | product_id | unit_price |
|---|---|---|---|
| 601 | 1231 | 165 | 25.99 |
| 602 | 1231 | 129 | 109.00 |
| 1828 | 1734 | 166 | 26.99 |
| 8230 | 4230 | 135 | 149.00 |
| product_id | category | unit_price |
|---|---|---|
| 129 | Lighting | 109.00 |
| 135 | Lighting | 159.00 |
| 165 | Textiles | 25.99 |
| 166 | Textiles | 28.99 |
- Toda tabla tiene una clave primaria: una columna cuyo valor es distinto en cada fila.
order_item_ididentifica exactamente una línea,product_idexactamente un producto. order_items.product_ides una clave foránea: guarda la clave primaria de un producto. Si la sigues desde una línea, siempre llegas a un solo producto.- Si la sigues en sentido contrario, un producto puede tener muchas líneas. Eso es una relación uno a muchos, la forma más común en cualquier base de datos.
- No toda columna repetida es un error.
unit_priceen la línea es lo que el cliente pagó ese día;products.unit_pricees el precio de lista de hoy. La línea 8230 pagó 149.00 por una lámpara que ahora cuesta 159.00.
La base de datos puede hacer cumplir las dos. Una restricción de clave primaria rechaza un id
duplicado; una restricción de clave foránea rechaza un pedido de un cliente que no existe. Los
analistas casi nunca las crean, pero saber que existen te dice en qué puedes confiar. Si order_id es
la clave primaria de orders, un join con orders por order_id nunca puede devolver más de un
pedido por línea.
NULL: el valor que no está
NULL no es cero ni un texto vacío. Significa "sin valor": un cliente que no dijo cómo conoció la
tienda tiene referral_source en NULL, y un producto cuyo costo se desconoce tiene unit_cost en
NULL. Como el valor es desconocido, cualquier comparación con él también lo es. NULL = NULL no es
verdadero, y WHERE referral_source = 'search' se salta las filas con NULL sin avisar. Para pedirlas
usas IS NULL. Las agregaciones también se las saltan: AVG(unit_cost) promedia solo los productos
que tienen costo, y COUNT(unit_cost) cuenta solo esos, mientras que COUNT(*) cuenta todas las
filas. Cuando un número parece demasiado pequeño o demasiado grande, los NULL son el primer lugar
donde mirar.
La normalización, en palabras sencillas
Normalización es el nombre que recibe diseñar las tablas para que cada dato se guarde una sola vez. Los libros definen "formas normales"; en la práctica se reducen a tres preguntas sobre cada columna:
- Un valor por celda. Una columna guarda una sola cosa, no una lista. Tres productos en una celda
("165, 129, 177") son señal de que esos datos necesitan su propia tabla, aquí
order_items. - Sobre la clave completa. Una columna describe la clave de la fila y nada más. La categoría de
un producto depende del producto, no de la línea de pedido, así que vive en
products. - Nada más que la clave. Si una columna se puede deducir de otra columna que no es clave (una ciudad a partir del cliente), pertenece a esa otra tabla.
Sigue las tres y obtienes la estructura de la tienda.
Figura 3
La tienda Harbor & Pine, tabla por tabla
customers
- customer_id
- first_name
- city
- state
orders
- order_id
- customer_id
- order_date
- status
order_items
- order_item_id
- order_id
- product_id
- quantity
products
- product_id
- name
- category
returns
- return_id
- order_item_id
- reason
- Cinco de las tablas de la tienda. Cada línea es una clave: el extremo 1 guarda la clave primaria y el extremo * la guarda como clave foránea.
- Un cliente, muchos pedidos:
orders.customer_idapunta acustomers. - Un pedido, muchas líneas; un producto, muchas líneas.
order_itemsestá en el medio y conecta los pedidos con los productos. - Una devolución pertenece a una línea de pedido. Para ir de una devolución a un cliente recorres las claves:
returns→order_items→orders→customers. Ese recorrido es una cadena de JOINs.
Las bases de datos reales rompen las reglas a propósito. order_items.unit_price repite un precio
porque el precio del día de la venta es un dato distinto del precio de lista de hoy. Las herramientas
de informes van más allá y reconstruyen tablas anchas (un "esquema en estrella", que se ve en la ruta
de Power BI) porque leer es más fácil cuando hacen falta menos joins. Son decisiones tomadas teniendo
en cuenta las reglas, no en lugar de ellas.
Errores comunes
"Una base de datos es una gran hoja de cálculo"
Una hoja de cálculo es una cuadrícula de celdas en la que puedes poner cualquier cosa. Una tabla es un conjunto de filas que tienen todas las mismas columnas, con tipos y claves. Eso es lo que permite a una base de datos responder preguntas sobre millones de filas y mantenerlas coherentes mientras mucha gente escribe en ella.
"Dividir los datos hace perder información"
No se pierde nada: las claves conservan cada conexión. Cualquier versión de la gran hoja se puede reconstruir con joins cuando una pregunta lo necesite, y siempre estará al día, porque se construye a partir de la única copia de cada dato.
"Las filas se guardan en el orden en que las ves"
Las tablas no tienen orden. Dos ejecuciones de la misma consulta sin ORDER BY pueden devolver las
filas en otro orden, y las dos son correctas. Cuando el orden importa para una respuesta, la consulta
tiene que pedirlo.
Ponte a prueba
Una pregunta. Al responderla, la lección queda marcada en el mapa; nunca bloquea un nivel.
No del todo. Eso es lo que necesitaría la versión de una gran hoja. order_items guarda un product_id, no la categoría, así que las líneas no cambian.Prueba otra respuesta.
Correcto. La categoría es un dato sobre un producto, así que vive en products. Cada línea y cada pedido llegan a ella a través de product_id, y ven el nombre nuevo al instante.
No del todo. orders no guarda productos en absoluto: las líneas de un pedido están en order_items, y esas solo guardan product_id.Prueba otra respuesta.
No del todo. La categoría es un dato guardado en products[category]. Simplemente no se copia en ningún otro lugar, así que el cambio ocurre en una sola tabla.Prueba otra respuesta.
Lección hecha: ya está marcada en el mapa.