lección 2
Hechos y dimensiones: separar "qué pasó" de "quién/dónde/cuándo"
Aprende la distinción fundamental del modelado dimensional: tablas de hechos (métricas) y tablas de dimensiones (contexto).
⏱ 55 min
En la lección anterior descubriste que necesitas una base de datos separada para análisis. Ahora la pregunta es: ¿cómo organizamos los datos dentro de esa base analítica? La respuesta es el modelado dimensional, inventado por Ralph Kimball, y su concepto central es brutalmente simple: separa los HECHOS (lo que pasó, lo que mides) de las DIMENSIONES (el contexto de lo que pasó).
Piensa en un periódico. Un titular dice: "España vendió 2.3 millones de coches en 2023". El hecho es el número: 2.3 millones. Las dimensiones son el contexto: España (dónde), coches (qué), 2023 (cuándo). Sin el número, no hay noticia. Sin el contexto, el número no significa nada. El modelado dimensional replica esta estructura natural del lenguaje humano para describir eventos.
### Tabla de hechos (Fact Table): lo que mides
Una tabla de hechos registra EVENTOS o MEDICIONES del negocio. Cada fila es algo que pasó: una venta, un clic, un envío, una llamada al call center. Contiene dos tipos de columnas: las métricas numéricas (amount, quantity, duration) y las claves foráneas que apuntan a las dimensiones (customer_id, product_id, date_id). Es la tabla más grande del warehouse — puede tener miles de millones de filas. Por eso se diseña ESTRECHA: cada columna que le añades se multiplica por miles de millones de filas.
- Cada fila = un evento de negocio (una venta, un clic, un envío)
- Columnas de métricas: amount, quantity, discount, cost — números que puedes sumar/promediar
- Columnas de FK: claves que conectan con las dimensiones (quién, qué, cuándo, dónde)
- Es la tabla MÁS GRANDE del warehouse (millones/billones de filas)
- Naming convention: fact_sales, fact_shipments, fact_page_views
- Normalmente tiene una granularidad definida (una fila por transacción, por día, etc.)
Las métricas de la fact table se clasifican en tres tipos según cómo se agregan. Aditivas: se suman por cualquier dimensión (importe, cantidad). Semi-aditivas: se suman por ALGUNAS dimensiones pero NO por el tiempo — el ejemplo canónico es el saldo de una cuenta o el nivel de inventario. No aditivas: no se suman nunca — ratios, porcentajes y puntuaciones como customer_rating. Para las no aditivas, la práctica recomendada es guardar los COMPONENTES (suma_puntuaciones y num_valoraciones) y calcular el ratio al consultar.
### Tabla de dimensiones (Dimension Table): el contexto
Una tabla de dimensiones describe las ENTIDADES del negocio: clientes, productos, tiendas, fechas, empleados. Son tablas relativamente pequeñas (miles a 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, fecha, día de la semana, es festivo, trimestre fiscal... A diferencia de la fact table, la dimensión se puede permitir ser ANCHA: 20-50 columnas × un millón de filas no es nada.
La analogía perfecta: si la tabla de hechos es una factura (tiene números), las dimensiones son los catálogos de referencia (quién es cada cliente, qué es cada producto). La factura dice "producto_id=789, cantidad=3, precio=29.99". La dimensión de producto te dice que el 789 es "Camiseta Nike Dri-FIT, categoría: Ropa Deportiva, marca: Nike, talla: M".
- Cada fila = una entidad de negocio (un cliente, un producto, una tienda)
- Columnas descriptivas: name, category, brand, country, segment — texto para filtrar/agrupar
- Es una tabla PEQUEÑA comparada con hechos (miles a pocos millones de filas)
- Tiene MUCHAS columnas (20-50 atributos es normal)
- Naming convention: dim_customer, dim_product, dim_date, dim_store
- Incluye una surrogate key (PK sintética) y opcionalmente la natural key del sistema fuente
1-- Tabla de hechos: fact_sales (millones de filas)2CREATE TABLE fact_sales (3 sale_id BIGINT PRIMARY KEY,4 -- Foreign keys a dimensiones5 date_key INTEGER REFERENCES dim_date(date_key),6 customer_key INTEGER REFERENCES dim_customer(customer_key),7 product_key INTEGER REFERENCES dim_product(product_key),8 store_key INTEGER REFERENCES dim_store(store_key),9 -- Métricas (lo que medimos)10 quantity INTEGER,11 unit_price DECIMAL(10,2),12 discount_amount DECIMAL(10,2),13 net_amount DECIMAL(10,2),14 cost DECIMAL(10,2)15);1617-- Tabla de dimensiones: dim_product (miles de filas)18CREATE TABLE dim_product (19 product_key INTEGER PRIMARY KEY, -- surrogate key20 product_id VARCHAR(20), -- natural key del sistema fuente21 product_name VARCHAR(200),22 category VARCHAR(50),23 subcategory VARCHAR(50),24 brand VARCHAR(100),25 supplier VARCHAR(100),26 unit_cost DECIMAL(10,2),27 is_active BOOLEAN,28 launch_date DATE29);
Estructura básica de un hecho y una dimensión. Nota la surrogate key en la dimensión.
### La dimensión de fecha: la más importante
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 solo un campo DATE — es una tabla completa con columnas precalculadas que hacen los GROUP BY triviales.
1-- La dimensión de fecha: tu mejor amiga en el warehouse2CREATE TABLE dim_date (3 date_key INTEGER PRIMARY KEY, -- formato YYYYMMDD (20240315)4 full_date DATE NOT NULL,5 day_of_week VARCHAR(10), -- 'Lunes', 'Martes'...6 day_of_month INTEGER, -- 1-317 day_of_year INTEGER, -- 1-3668 week_of_year INTEGER, -- 1-539 month_number INTEGER, -- 1-1210 month_name VARCHAR(15), -- 'Enero', 'Febrero'...11 quarter INTEGER, -- 1-412 year INTEGER,13 is_weekend BOOLEAN,14 is_holiday BOOLEAN,15 holiday_name VARCHAR(50),16 fiscal_quarter INTEGER, -- puede diferir del calendario17 fiscal_year INTEGER18);1920-- Ahora "ventas por trimestre" es trivial:21SELECT d.fiscal_year, d.quarter, SUM(f.net_amount)22FROM fact_sales f JOIN dim_date d ON f.date_key = d.date_key23GROUP BY 1, 2;
Con dim_date precargada, cualquier agrupación temporal es un simple GROUP BY sin funciones de fecha.
Consejo de alguien que ha generado dim_date 20 veces: genera la tabla de fecha una sola vez con un script, cargando desde el año 2000 hasta el 2030. Incluye festivos de tu país. Incluye semana ISO. Incluye fiscal_year si tu empresa cierra en junio. Es una inversión de 30 minutos que ahorra horas de funciones DATE_TRUNC en cada query. En el ejercicio 4 construyes una versión mínima de 7 columnas; la completa es la de arriba (15). Y una nota: is_holiday y holiday_name no se pueden generar con generate_series — hay que cargarlos de una fuente de festivos. Es de lo primero que aprende quien monta esta tabla por primera vez.
### Surrogate keys vs natural keys
Nota que en la dimensión usamos product_key (un entero secuencial inventado por nosotros) en vez de product_id (el ID real del sistema fuente). ¿Por qué? Porque las natural keys del sistema fuente pueden cambiar, reciclarse o tener formatos inconsistentes entre sistemas. Un cliente puede tener ID "C-1234" en el CRM y "1234" en el ERP. La surrogate key nos aísla de esos problemas.
Además, las surrogate keys son INTEGER — ocupan 4 bytes. Las natural keys suelen ser VARCHAR — ocupan 10-50 bytes. En una tabla de hechos con mil millones de filas, esa diferencia importa, pero menos de lo que parece: medido en PostgreSQL con un millón de filas, el índice pasa de 30 MB (clave de 15 caracteres) a 21 MB (entero), un 30% menos. La razón de peso no es el tamaño: es que la clave natural no la controlas tú. Cambia cuando el sistema fuente decide cambiarla, se recicla cuando se borra un cliente y se vuelve a usar el código, y llega en tres formatos distintos si integras tres sistemas. Una clave subrogada es tuya y no cambia nunca.
Error que he visto en producción: usar la natural key del sistema fuente como PK de la dimensión. Todo funciona hasta que otro sistema fuente tiene un ID diferente para la misma entidad, o hasta que necesitas trackear cambios históricos (SCD tipo 2). Con surrogate keys, ambos problemas se resuelven limpiamente. Sin ellas, necesitas migraciones dolorosas.
### Dimensiones degeneradas y junk dimensions
No todo encaja limpiamente en "es un hecho o es una dimensión". Hay casos especiales. Una dimensión degenerada es un atributo dimensional que vive DENTRO de la tabla de hechos sin su propia tabla de dimensión. El ejemplo clásico: el número de pedido (order_number). No es una métrica, pero tampoco merece su propia tabla. Se queda como columna en la fact table.
Una junk dimension es una tabla que agrupa flags y atributos de baja cardinalidad que no pertenecen a ninguna otra dimensión. Por ejemplo: is_online, payment_type, is_gift_wrap, shipping_priority. En vez de tener 4 columnas booleanas/varchar sueltas en la fact table, creas una dim_transaction_profile con todas las combinaciones posibles (256 como máximo) y la fact table apunta a ella con un solo FK.
### Resumen visual: anatomía de un modelo dimensional
## ejercicios
Clasificar columnas en hechos y dimensiones
Una startup de delivery tiene estos datos por cada pedido: rider_name, restaurant_name, delivery_fee, tip_amount, order_time, distance_km, customer_rating, weather, city. Clasifica cada columna como HECHO (métrica) o DIMENSIÓN (contexto) usando comentarios SQL.
Diseñar una dimensión de cliente completa
El equipo de marketing quiere analizar ventas por segmento, país, antigüedad y canal de adquisición. Diseña la tabla dim_customer con todos los atributos necesarios, incluyendo surrogate key y natural key.
Escribir queries dimensionales
Dado el modelo fact_sales + dim_date + dim_product + dim_customer, escribe una query que responda: "¿Cuáles son los 5 productos más vendidos a clientes VIP durante los fines de semana del Q4 2023?"
Generar la dimensión de fecha
Escribe una query que genere todos los registros de dim_date para 2023 y 2024 usando generate_series. Incluye: date_key, full_date, day_of_week, month_number, quarter, year, is_weekend. Este ejercicio es autocontenido: crea la tabla, la puebla y la consulta. Ejecútalo en DuckDB.
💡 Resultado esperado
date_key | full_date | day_of_week | month_number | quarter | year | is_weekend 20230101 | 2023-01-01 | Sunday | 1 | 1 | 2023 | true 20230102 | 2023-01-02 | Monday | 1 | 1 | 2023 | false 20230103 | 2023-01-03 | Tuesday | 1 | 1 | 2023 | false
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...