Módulo 1: From Flat Tables To Dimensional Models

Qué dejó plano foundations, y por qué

Descripción

Antes de agregar una sola idea nueva, esta lección hace inventario: qué tiene fact_orders/dim_store/dim_product hoy, exactamente, y qué le falta frente a un modelo dimensional real. No es una crítica a foundations —la decisión de dejarlo plano fue correcta para el alcance de esa guía, y esta lección explica por qué—, es el punto de partida honesto que necesitas antes de empezar a construir encima.

Conexión con el módulo. Esta lección no ejecuta la consulta central del módulo —eso llega en la lección 5—, pero sí construye, con código real, la lista de verificación que vas a usar como mapa del resto de la guía: qué le falta a Kiosko, y en qué módulo se resuelve cada punto.

Una analogía: la casa habitable, no la casa terminada

Un contratista que entrega una casa "habitable" no es lo mismo que uno que entrega una casa "terminada". Una casa habitable tiene paredes, techo, electricidad y agua funcionando — se puede vivir en ella, y para una familia que necesita mudarse ya, es exactamente lo que pidieron. Pero "habitable" no es "terminada": no tiene closets a medida, no tiene el jardín paisajístico, no tiene el sistema de riego automático. Nadie construyó esas cosas por descuido — el contratista tomó una decisión de alcance explícita: "esto es lo que se entrega en esta fase, esto queda para después", y esa decisión fue la correcta para el presupuesto y el tiempo que había.

fact_orders/dim_store/dim_product de foundations es la casa habitable de Kiosko: tiene lo esencial para vivir en ella —un reporte de ventas confiable, seguro de re-correr—, pero no tiene los "closets a medida" de un warehouse dimensional real: llaves sustitutas, historización, una dimensión de calendario, más de un hecho. Esta lección hace el recorrido por la casa, cuarto por cuarto, señalando qué falta y en qué fase (qué módulo de esta guía) se construye cada pieza.

Ejemplo trabajado: el inventario de lo que existe hoy

Primero, recuerda exactamente lo que foundations dejó — sin agregar ni quitar una sola columna:

# kiosko_today.py
FACT_ORDERS_COLUMNS = ["order_id", "store_id", "product_id", "quantity", "unit_price", "revenue", "order_ts"]

DIM_STORE_COLUMNS = ["store_id", "store_name", "city"]
DIM_PRODUCT_COLUMNS = ["product_id", "product_name", "category", "unit_cost"]

DIM_STORE = [
    {"store_id": "S01", "store_name": "Kiosko Centro", "city": "Bogota"},
    {"store_id": "S02", "store_name": "Kiosko Norte", "city": "Lima"},
    {"store_id": "S03", "store_name": "Kiosko Sur", "city": "Santiago"},
]

DIM_PRODUCT = [
    {"product_id": "P001", "product_name": "Bottled Water 600ml", "category": "beverages", "unit_cost": 0.40},
    {"product_id": "P002", "product_name": "Energy Bar", "category": "snacks", "unit_cost": 0.60},
    {"product_id": "P003", "product_name": "Instant Coffee Sachet", "category": "beverages", "unit_cost": 0.35},
    {"product_id": "P004", "product_name": "Phone Charger Cable", "category": "electronics", "unit_cost": 2.10},
]

print("=== Lo que Kiosko ya tiene, heredado de foundations ===\n")
print(f"fact_orders: {len(FACT_ORDERS_COLUMNS)} columnas -> {FACT_ORDERS_COLUMNS}")
print(f"dim_store:   {len(DIM_STORE_COLUMNS)} columnas -> {DIM_STORE_COLUMNS} ({len(DIM_STORE)} filas)")
print(f"dim_product: {len(DIM_PRODUCT_COLUMNS)} columnas -> {DIM_PRODUCT_COLUMNS} ({len(DIM_PRODUCT)} filas)")

Qué esperar. Al correr python3 kiosko_today.py, la salida es exactamente esta:

=== Lo que Kiosko ya tiene, heredado de foundations ===

fact_orders: 7 columnas -> ['order_id', 'store_id', 'product_id', 'quantity', 'unit_price', 'revenue', 'order_ts']
dim_store:   3 columnas -> ['store_id', 'store_name', 'city'] (3 filas)
dim_product: 4 columnas -> ['product_id', 'product_name', 'category', 'unit_cost'] (4 filas)

Este es, exactamente, el mismo esquema que foundations dejó en su módulo 8 —ni una columna de más, ni una de menos—. A partir de aquí, el resto de la lección no cambia ninguna de estas columnas: las usa tal cual, y compara lo que falta alrededor de ellas.

Ahora, la lista de verificación — un checklist de las piezas que un modelo dimensional real necesita, con lo que Kiosko tiene hoy marcado explícitamente:

# dimensional_checklist.py
CHECKLIST = [
    ("Grano de fact_orders declarado y verificado con una consulta", False, "M1 (esta guia)"),
    ("Llaves sustitutas (surrogate keys) en las dimensiones", False, "M2"),
    ("dim_date: una dimension de calendario reutilizable", False, "M2"),
    ("Dimensiones conformadas (compartidas entre mas de un hecho)", False, "M2"),
    ("Snowflake: dimensiones normalizadas cuando conviene", False, "M3"),
    ("Historizacion de una dimension que cambia (SCD)", False, "M4"),
    ("Join punto-en-el-tiempo contra una dimension historizada", False, "M5"),
    ("Deduplicacion explicita de filas repetidas", False, "M5"),
    ("Accumulating snapshot fact table (funnel de sesiones)", False, "M6"),
    ("Cumulative table design (actividad con ventanas moviles)", False, "M6"),
    ("Dimension junk (banderas de baja cardinalidad agrupadas)", False, "M7"),
    ("Mas de un hecho conviviendo con dimensiones compartidas", False, "M7"),
]

print("=== Lo que le falta a Kiosko para ser un warehouse dimensional real ===\n")
for item, exists_today, resolved_in in CHECKLIST:
    mark = "[x]" if exists_today else "[ ]"
    print(f"{mark} {item:62} -> se resuelve en {resolved_in}")

pending = sum(1 for _, exists_today, _ in CHECKLIST if not exists_today)
print(f"\nTotal de piezas pendientes hoy: {pending} de {len(CHECKLIST)}")

Qué esperar. Al correr python3 dimensional_checklist.py, la salida es exactamente esta:

=== Lo que le falta a Kiosko para ser un warehouse dimensional real ===

[ ] Grano de fact_orders declarado y verificado con una consulta   -> se resuelve en M1 (esta guia)
[ ] Llaves sustitutas (surrogate keys) en las dimensiones          -> se resuelve en M2
[ ] dim_date: una dimension de calendario reutilizable             -> se resuelve en M2
[ ] Dimensiones conformadas (compartidas entre mas de un hecho)    -> se resuelve en M2
[ ] Snowflake: dimensiones normalizadas cuando conviene            -> se resuelve en M3
[ ] Historizacion de una dimension que cambia (SCD)                -> se resuelve en M4
[ ] Join punto-en-el-tiempo contra una dimension historizada       -> se resuelve en M5
[ ] Deduplicacion explicita de filas repetidas                     -> se resuelve en M5
[ ] Accumulating snapshot fact table (funnel de sesiones)          -> se resuelve en M6
[ ] Cumulative table design (actividad con ventanas moviles)       -> se resuelve en M6
[ ] Dimension junk (banderas de baja cardinalidad agrupadas)       -> se resuelve en M7
[ ] Mas de un hecho conviviendo con dimensiones compartidas        -> se resuelve en M7

Total de piezas pendientes hoy: 12 de 12

Doce piezas pendientes, cero resueltas — y eso es exactamente lo esperado al abrir este módulo, no una señal de alarma. Fíjate en algo importante: la primera línea de la lista —"grano declarado y verificado"— es la que este módulo va a marcar como resuelta al final de la lección 8. Las otras once quedan, a propósito, para los módulos que siguen. Ningún módulo de esta guía intenta resolver más de lo que le corresponde.

Diagrama: la casa habitable de Kiosko, cuarto por cuarto

┌─────────────────────────────────────────────────────────────┐
│  KIOSKO HOY (heredado de foundations)                        │
│                                                                 │
│  fact_orders (7 cols)      dim_store (3 cols)                 │
│  order_id, store_id,       store_id (PK natural)               │
│  product_id, quantity,     store_name, city                    │
│  unit_price, revenue,                                          │
│  order_ts                  dim_product (4 cols)                │
│                             product_id (PK natural)             │
│                             product_name, category, unit_cost   │
│                                                                 │
│  Un hecho. Dos dimensiones. Llaves naturales. Sin dim_date.     │
│  Sin historizacion. Sin grano declarado formalmente.            │
└─────────────────────────────────────────────────────────────┘
                              │
                              │  este modulo declara el grano
                              v
┌─────────────────────────────────────────────────────────────┐
│  KIOSKO AL CERRAR ESTA GUIA (modulos 2-8)                      │
│  Star schema + dim_date + llaves sustitutas + SCD-2 +           │
│  snowflake/OBT + accumulating snapshot + cumulative design +    │
│  dimension junk + contratos Medallion formales                  │
└─────────────────────────────────────────────────────────────┘

Profundización: por qué "plano" fue la decisión correcta para foundations

Vale la pena ser justos con foundations, porque es fácil, en retrospectiva, ver una lista de doce piezas faltantes y pensar que esa guía "hizo las cosas mal". No es así. El alcance de foundations era enseñar el ciclo de vida completo del dato —generación, almacenamiento, ingestión, transformación, servido— con un pipeline real, de punta a punta, corrible en una laptop. Agregar llaves sustitutas, dim_date, y SCD-2 a esa guía habría significado enseñar dos disciplinas distintas a la vez —el pipeline batch y el modelado dimensional a fondo— diluyendo ambas.

La disciplina que mejor separa a alguien que diseña software con criterio de alguien que solo agrega funcionalidades es saber decir, con precisión, "esto no es parte de este alcance, y aquí está la razón" — exactamente lo que foundations hizo, y exactamente lo que la sección "NO entra en" del diseño de esta guía sigue haciendo en cada módulo. Un dim_store con llave natural, sin historización, es una decisión de diseño correcta cuando las tiendas no cambian de nombre durante el alcance de la guía — y se vuelve una decisión incorrecta el día que sí cambian, sin que el modelo lo pueda representar. Esta guía existe porque ese día ya llegó: vas a ver, en el módulo 4, exactamente ese escenario con dim_product cuando el precio de un producto cambia de verdad.

Errores comunes

Tratar la lista de doce piezas como "todo lo que falta arreglar ya". Qué pasa: alguien, al ver el checklist de esta lección, quiere agregar dim_date, llaves sustitutas y SCD-2 todos a la vez, en esta misma lección, sin seguir el orden de los módulos. Por qué pasa: ver una lista completa de carencias genera el impulso de resolverlas todas de inmediato. Cómo detectarlo: si terminas esta lección con código que ya intenta construir dim_date o una llave sustituta, te adelantaste — esa es, literalmente, la lección 4 del módulo 2. Cómo corregirlo: cada pieza de la lista tiene su módulo específico por una razón pedagógica —cada una depende de conceptos que los módulos anteriores todavía no enseñaron—; resuélvelas en el orden que la guía propone, no en el orden que te parezca más urgente.

Pensar que "plano" significa "mal diseñado" en cualquier contexto. Qué pasa: alguien generaliza la lección y concluye que cualquier tabla sin llave sustituta o sin historización está mal diseñada, sin importar el contexto. Por qué pasa: es fácil convertir "esto le falta a Kiosko para este propósito" en una regla universal ("siempre hay que tener llaves sustitutas"). Cómo detectarlo: si tu razonamiento no menciona el contexto específico —volumen de datos, si la dimensión cambia en el tiempo, quién consume el modelo—, estás aplicando una regla sin criterio. Cómo corregirlo: la profundización de esta lección lo dice explícitamente — una decisión de diseño es correcta o incorrecta según el contexto, nunca en abstracto. Vas a ver esta misma idea repetirse en el módulo 3, cuando compares star schema contra tabla ancha: ninguna de las dos formas es "la correcta" sin conocer el caso de uso.

Saltarse el checklist porque "ya se sabe intuitivamente qué falta". Qué pasa: alguien con experiencia previa en SQL o en herramientas de BI asume que ya conoce todas las carencias de un modelo plano, y no ejecuta el script de esta lección. Por qué pasa: conceptos como "llave sustituta" o "dimensión conformada" pueden sonar familiares de nombre, aunque no se hayan aplicado nunca con precisión sobre un caso concreto. Cómo detectarlo: si no puedes nombrar, sin mirar, las doce piezas exactas del checklist y en qué módulo se resuelve cada una, tu intuición todavía no es tan completa como crees. Cómo corregirlo: corre el script, lee la lista completa una vez, y guárdala como referencia — vas a volver a ella al cerrar cada módulo de esta guía, marcando cada pieza como resuelta.

Ejercicios

Ejercicio 1 — Explica por qué "grano declarado" es la primera línea de la lista, no cualquiera de las otras once. Sin repetir literalmente la sección de profundización, explica en 2-3 frases por qué declarar el grano tiene que resolverse antes que, por ejemplo, agregar llaves sustitutas o construir dim_date.

Ver solución

Llaves sustitutas, dim_date, historización y el resto de la lista son decisiones que dependen de saber, con precisión, qué representa una fila del hecho al que se van a conectar — no tiene sentido diseñar una dimensión de calendario para unir con fact_orders si todavía no sabes con certeza si el grano es "una orden" o "una línea de orden" (la pregunta exacta que resuelve la lección 5). Declarar el grano primero es lo que le da al resto de las decisiones un cimiento verificado en vez de una suposición — exactamente la misma razón por la que el proceso de Kimball pone "declare the grain" como el paso 2, antes de "identify the dimensions" (paso 3).

Ejercicio 2 — Cuenta las piezas por módulo. Usando el checklist del ejemplo trabajado, cuenta cuántas piezas pendientes le corresponden a cada módulo (M2, M3, M4, M5, M6, M7), y confirma que la suma total, más la pieza de M1, da doce.

Ver solución
from collections import Counter

CHECKLIST = [
    ("M1 (esta guia)"), ("M2"), ("M2"), ("M2"), ("M3"), ("M4"),
    ("M5"), ("M5"), ("M6"), ("M6"), ("M7"), ("M7"),
]

counts = Counter(CHECKLIST)
for module in ["M1 (esta guia)", "M2", "M3", "M4", "M5", "M6", "M7"]:
    print(f"{module}: {counts[module]} pieza(s)")

print(f"\nTotal: {sum(counts.values())}")

Salida esperada:

M1 (esta guia): 1 pieza(s)
M2: 3 pieza(s)
M3: 1 pieza(s)
M4: 1 pieza(s)
M5: 2 pieza(s)
M6: 2 pieza(s)
M7: 2 pieza(s)

Total: 12

M2 concentra la mayor carga (llaves sustitutas, dim_date, dimensiones conformadas) porque es, literalmente, el módulo que construye el star schema completo — el resto de los módulos profundiza en una técnica específica sobre ese star schema ya construido.

Ejercicio 3 — Argumenta si foundations debió incluir SCD-2 desde el principio. En 2-3 frases, usando la profundización de esta lección, argumenta por qué habría sido un error que foundations incluyera SCD-2 (historización con valid_from/valid_to) en su propio módulo 4, en vez de dejarlo para esta guía.

Ver solución

Foundations estaba enseñando, por primera vez, la diferencia entre un hecho y una dimensión —el módulo 4 de esa guía es, literalmente, la primera vez que Kiosko separa fact_orders de dim_store/dim_product—. Introducir SCD-2 en ese mismo momento habría significado enseñar dos ideas nuevas y complejas a la vez: qué es una dimensión, y cómo esa dimensión sobrevive a un cambio en el tiempo — cuando la segunda idea depende por completo de haber entendido bien la primera. Separarlas en guías distintas, con foundations construyendo dimensiones estables primero y esta guía historizándolas después, respeta el mismo principio de una responsabilidad a la vez que ya usaste al escribir funciones de Python en foundations.

Resumen y siguiente paso

En esta lección hiciste inventario, sin agregar ni quitar nada: fact_orders (7 columnas), dim_store (3 columnas) y dim_product (4 columnas), exactamente como los dejó foundations, y una lista de doce piezas que un modelo dimensional real necesita y que Kiosko todavía no tiene — cada una asignada a un módulo específico de esta guía. Entendiste, además, por qué dejar el modelo plano fue la decisión correcta para el alcance de foundations, no un error a corregir con vergüenza.

Antes de avanzar deberías poder: recitar de memoria las siete columnas de fact_orders; nombrar al menos cuatro de las doce piezas pendientes del checklist; y explicar por qué "plano" no es sinónimo de "mal diseñado" sin conocer el contexto.

La lección 3 deja el inventario atrás y entra al método: el proceso de cuatro pasos de Ralph Kimball, explicado primero con un ejemplo pequeño y completo —no todavía Kiosko—, para que veas el patrón entero antes de aplicarlo sobre fact_orders en las lecciones 4 y 5.

Recursos