lección 2
Hechos y dimensiones: distinguir lo que mides de lo que describe
Las tablas del almacén se organizan en dos tipos: hechos (lo que pasó) y dimensiones (el contexto). Aprende a distinguirlas para saber dónde buscar cada dato.
⏱ 50 min
Ya sabes que tu terreno es el almacén analítico, no la base de la app. Ahora necesitas entender cómo está organizado por dentro. Porque si abres tu cliente SQL y ves treinta tablas con nombres como fact_sales, dim_product, dim_customer, dim_date... no es evidente qué significan esos prefijos ni por qué las tablas están separadas así. No es capricho: es un patrón de organización que lleva funcionando desde los años 90. Ralph Kimball no inventó la idea de separar hechos de dimensiones, pero fue quien la convirtió en un método con nombres, reglas y vocabulario propio — y ese método sigue siendo el estándar en casi todos los almacenes de datos del mundo.
El concepto central es brutalmente simple: separa lo que PASÓ (los hechos, las cosas que mides) de lo que DESCRIBE ese evento (las dimensiones, el contexto). Un periódico funciona igual. El titular dice: "En España se matricularon 949.359 turismos en 2023". El hecho es el número: 949.359. Las dimensiones son el contexto: España (dónde), turismos (qué), 2023 (cuándo). Sin el número no hay noticia. Sin el contexto, el número no significa nada. Cada tabla del almacén es una de esas dos cosas.
### Tablas de hechos: dónde están los números
Una tabla de hechos (fact table) registra eventos del negocio. Cada fila es algo que pasó: una venta, un clic, un envío, una llamada al servicio de atención. Las columnas se dividen en dos tipos: números que puedes sumar o promediar (importe, cantidad, duración) y claves que apuntan a las tablas de dimensiones (id_cliente, id_producto, id_fecha). Es la tabla más grande del almacén — puede tener cientos de millones de filas — y es de donde salen todos tus KPIs.
La analogía perfecta: una tabla de hechos es como un tique de compra. El tique registra que algo pasó (una venta), tiene números (29,90 euros, 2 unidades) y tiene referencias al contexto (qué producto, qué tienda, qué fecha). Si solo tuvieras los tiques sin saber qué es cada producto ni dónde está cada tienda, tendrías números sueltos. Si solo tuvieras el catálogo de productos sin tiques, no tendrías nada que contar. Necesitas los dos.
- Cada fila = un evento de negocio (una venta, un clic, un envío, una llamada)
- Columnas de métricas: importe, cantidad, descuento, coste — números que puedes sumar o promediar
- Columnas de claves: id_cliente, id_producto, id_fecha — enlaces a las dimensiones
- Es la tabla MÁS GRANDE del almacén (millones o miles de millones de filas)
- Convención de nombres habitual: fact_ventas, fact_pedidos, fct_clics, f_envios
- Tus GROUP BY siempre empiezan aquí: SUM(importe), COUNT(*), AVG(duracion)
### Tablas de dimensiones: dónde está el contexto
Una tabla de dimensiones describe las entidades del negocio: clientes, productos, tiendas, fechas, empleados. Son tablas relativamente pequeñas (miles a pocos millones de filas) pero con MUCHAS columnas descriptivas. Es donde están los atributos por los que filtras y agrupas: nombre del producto, categoría, marca, país del cliente, segmento, día de la semana, si es festivo...
Volviendo a la analogía del tique: si el tique dice "producto 789", la dimensión de producto es el catálogo que te dice que el 789 es una "Camiseta Nike Dri-FIT, categoría Ropa Deportiva, marca Nike, talla M". Sin esa tabla, tu informe diría "el producto 789 vendió 450 unidades" — inútil para cualquier decisión. Con ella, dice "la Camiseta Nike Dri-FIT vendió 450 unidades en la categoría Ropa Deportiva" — eso ya lo entiende un director comercial.
- Cada fila = una entidad de negocio (un cliente, un producto, una tienda, una fecha)
- Columnas descriptivas: nombre, categoría, marca, país, segmento — texto para filtrar y agrupar
- Es una tabla PEQUEÑA comparada con los hechos (miles a pocos millones de filas)
- Tiene MUCHAS columnas (20-50 atributos es normal en una dimensión madura)
- Convención de nombres habitual: dim_cliente, dim_producto, dim_fecha, d_tienda
- Tus WHERE y GROUP BY las usan constantemente: WHERE dim_producto.categoria = 'Ropa'
### Cómo se conectan: el JOIN que harás mil veces
La tabla de hechos tiene una columna id_producto con valores como 789, 234, 567. La tabla de dimensiones tiene una fila para cada producto con todas sus propiedades. El JOIN entre ambas es lo que le da significado a los números. Este patrón es el que vas a repetir decenas de veces al día: empiezas por la tabla de hechos (que tiene los números), haces JOIN con una o varias dimensiones (que tienen el contexto), filtras por atributos de la dimensión (WHERE categoria = 'Ropa'), y agrupas por otros atributos (GROUP BY marca).
1-- El patrón básico: hecho + dimensión2-- "¿Cuánto vendimos por categoría de producto este mes?"34SELECT5 d.categoria,6 SUM(f.importe) AS total_ventas,7 COUNT(*) AS num_transacciones8FROM fact_ventas f9JOIN dim_producto d ON f.id_producto = d.id_producto10WHERE f.id_fecha BETWEEN 20240301 AND 2024033111GROUP BY d.categoria12ORDER BY total_ventas DESC;
El JOIN más común: tabla de hechos + dimensión. Siempre empiezas por el hecho.
Consejo de senior: cuando escribas una query, empieza SIEMPRE por la tabla de hechos en el FROM. Es tu ancla. Desde ahí, haz JOIN a las dimensiones que necesites para filtrar o agrupar. Nunca empieces por una dimensión y hagas JOIN al hecho — funciona igual en SQL, pero es más fácil perderte y acabar con resultados inesperados si la dimensión tiene duplicados (algo que ocurre en dimensiones con historial, como verás más adelante).
### La dimensión de fecha: tu mejor amiga
Toda tabla de hechos tiene una dimensión de fecha. SIEMPRE. Es la dimensión más consultada porque el negocio siempre quiere ver tendencias: "ventas este mes vs el anterior", "comparativa interanual", "acumulado del trimestre". La dimensión de fecha no es un simple campo DATE — es una tabla completa con columnas precalculadas que hacen tus GROUP BY triviales: día de la semana, número de mes, trimestre, si es festivo, si es fin de semana, año fiscal...
Esto te facilita la vida enormemente. Sin dimensión de fecha, para agrupar por trimestre tendrías que escribir EXTRACT(QUARTER FROM fecha_pedido) en cada query. Con ella, haces GROUP BY dim_fecha.trimestre y listo. Parece un detalle menor, pero cuando llevas 40 queries al día, la diferencia se nota. Además, la dimensión de fecha incluye cosas que no puedes calcular con funciones: festivos locales, año fiscal de la empresa (que puede empezar en julio), semanas comerciales...
1-- dim_fecha: lo que suele tener2-- (no la construyes tú, pero necesitas saber qué hay dentro)34-- date_key: 20240315 (la clave, formato YYYYMMDD)5-- fecha: 2024-03-156-- dia_semana: 'Viernes'7-- num_mes: 38-- nombre_mes: 'Marzo'9-- trimestre: 110-- anio: 202411-- es_fin_de_semana: true12-- es_festivo: false13-- anio_fiscal: 2024 (puede diferir del natural)14-- semana_iso: 111516-- Ejemplo: ventas por trimestre, facilísimo17SELECT18 d.anio,19 d.trimestre,20 SUM(f.importe) AS total21FROM fact_ventas f22JOIN dim_fecha d ON f.id_fecha = d.date_key23GROUP BY d.anio, d.trimestre24ORDER BY d.anio, d.trimestre;
Con dim_fecha precalculada, cualquier agrupación temporal es un GROUP BY simple.
### Cómo distinguir un hecho de una dimensión al vuelo
Cuando llegas a un almacén nuevo y ves una tabla que no conoces, hay un truco rápido para saber si es un hecho o una dimensión: mira los tipos de las columnas. Si la mayoría son números (INTEGER, DECIMAL, FLOAT) y claves foráneas, es un hecho. Si la mayoría son texto (VARCHAR, TEXT) con pocos números, es una dimensión. Otro indicador: el tamaño. Si tiene millones de filas, casi seguro es un hecho. Si tiene miles, probablemente es una dimensión.
- Muchas filas + columnas numéricas + claves foráneas = HECHO
- Pocas filas + columnas de texto + muchos atributos descriptivos = DIMENSIÓN
- El nombre ayuda: fact_, fct_, f_ = hecho. dim_, d_ = dimensión
- La pregunta clave: ¿puedo sumar esta columna? Si sí → hecho. ¿Puedo filtrar/agrupar por ella? Si sí → dimensión
- Una tabla puede tener ambos (importe Y nombre_producto en la misma fila), pero eso suele ser una tabla desnormalizada, no un modelo dimensional limpio
Cuidado con las columnas que parecen métricas pero son dimensiones. El ejemplo clásico: el precio de un producto. ¿Es un hecho o una dimensión? Depende. El precio AL QUE SE VENDIÓ (lo que pagó el cliente en esa transacción) es un hecho — puedes sumarlo. El precio DE LISTA del producto (lo que cuesta en el catálogo) es un atributo de la dimensión — no tiene sentido sumarlo. La misma palabra "precio" puede ser una cosa u otra según dónde esté.
### Por qué importa esto para tus queries
No es teoría académica. Saber si una tabla es un hecho o una dimensión cambia cómo escribes la query y, sobre todo, cómo interpretas el resultado. Si haces SUM(importe) GROUP BY categoria, el SUM va contra el hecho y el GROUP BY contra la dimensión. Si te equivocas y haces SUM sobre algo que no es un hecho (como sumar los id_producto), obtienes un número que no significa nada. Si agrupas por algo que debería ser un hecho (como GROUP BY importe), obtienes tantas filas como importes distintos existan — probablemente miles — y tu informe es ilegible.
Además, la estructura hechos-dimensiones te dice cómo crecer una query. Si el director te pide "ventas por categoría" y luego añade "pero solo de clientes VIP del último trimestre", sabes exactamente dónde tocar: añadir un JOIN a dim_cliente para filtrar por segmento, y otro JOIN a dim_fecha para filtrar por trimestre. No necesitas reescribir nada: solo apilas más dimensiones al mismo patrón.
Consejo de senior: cuando tengas una query que "casi" funciona pero da números raros, lo primero que compruebo es si el JOIN está multiplicando filas. Si una dimensión tiene duplicados (por ejemplo, dos filas para el mismo producto porque cambió de categoría), tu SUM se duplica sin que te des cuenta. Más adelante verás cómo manejar eso — por ahora, cuando un número te parezca demasiado alto, cuenta las filas antes y después del JOIN. Si aumentan, algo está duplicando.
### Un ejemplo completo: la tienda de bicicletas
Imagina que llegas a una empresa que vende bicicletas y accesorios online. El almacén tiene estas tablas:
1-- HECHOS2-- fact_ventas: una fila por cada línea de pedido3-- venta_id, id_producto, id_cliente, id_fecha, id_tienda4-- cantidad, precio_unitario, descuento, importe_neto56-- DIMENSIONES7-- dim_producto: nombre, categoria, marca, modelo, color, precio_lista8-- dim_cliente: nombre, email, pais, ciudad, segmento, canal_adquisicion9-- dim_fecha: fecha, dia_semana, mes, trimestre, anio, es_festivo10-- dim_tienda: nombre_tienda, ciudad, region, responsable, m21112-- Pregunta de negocio:13-- "¿Cuáles son las 5 marcas que más vendieron14-- a clientes del segmento Premium en Q4 2023?"1516SELECT17 p.marca,18 SUM(f.importe_neto) AS ingresos,19 COUNT(DISTINCT f.id_cliente) AS clientes_unicos20FROM fact_ventas f21JOIN dim_producto p ON f.id_producto = p.id_producto22JOIN dim_cliente c ON f.id_cliente = c.id_cliente23JOIN dim_fecha d ON f.id_fecha = d.date_key24WHERE c.segmento = 'Premium'25 AND d.trimestre = 426 AND d.anio = 202327GROUP BY p.marca28ORDER BY ingresos DESC29LIMIT 5;
Tres JOINs a dimensiones: producto (para agrupar por marca), cliente (para filtrar por segmento), fecha (para filtrar por trimestre).
### Surrogate keys: por qué las claves son números raros
Si miras la tabla de hechos, verás que la columna id_producto no tiene un código legible como "BIKE-MTB-001". Tiene un número como 789. Ese número es una surrogate key (clave subrogada): un identificador inventado por el equipo que construyó el almacén, que no tiene significado de negocio. ¿Por qué? Porque los códigos "reales" del sistema fuente (la app) pueden cambiar, reciclarse o tener formatos distintos entre sistemas. El 789 es estable para siempre.
Para ti como analista esto significa una cosa práctica: nunca filtres por la surrogate key directamente (WHERE id_producto = 789) a menos que sepas EXACTAMENTE qué producto es. Lo normal es hacer JOIN a la dimensión y filtrar por el atributo legible: WHERE dim_producto.nombre = 'Mountain Bike Pro' o WHERE dim_producto.categoria = 'Bicicletas de montaña'. La surrogate key es la llave para abrir la puerta de la dimensión, no el dato que enseñas en un informe.
### Resumen: el mapa mental
Cuando abras el almacén de cualquier empresa, vas a ver este patrón: una o varias tablas de hechos grandes (con millones de filas y números) rodeadas de tablas de dimensiones pequeñas (con miles de filas y texto descriptivo). Los hechos te dan QUÉ PASÓ y CUÁNTO. Las dimensiones te dan QUIÉN, DÓNDE, CUÁNDO y QUÉ. El JOIN entre ambos te da la respuesta completa a cualquier pregunta de negocio.
En la siguiente lección vas a ver cómo estas tablas se organizan en un patrón visual concreto — el esquema estrella — y por qué ese patrón te facilita escribir queries correctas sin multiplicar filas ni perder datos.
Regístrate para guardar tu progreso.
## comentarios
Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.
Inicia sesión para comentar y responder.
cargando comentarios...