Módulo 2: The Star Schema And Conformed Dimensions
Dimensiones conformadas a través de procesos
Descripción
Hasta esta lección, Kiosko ha tenido un solo proceso de negocio: la venta (fact_orders). Con un solo proceso, la pregunta "¿esta dimensión sirve a más de un hecho?" nunca se hizo relevante — dim_store y dim_product solo tenían un consumidor. Esta lección adelanta, de forma controlada, lo que va a pasar en el módulo 6, cuando Kiosko gane un segundo proceso de negocio (fact_sessions, el funnel de sesiones de la app): y muestra por qué dim_store y dim_date, tal como quedaron construidas en este módulo, ya están listas para ese momento, sin necesitar ningún cambio.
Conexión con el módulo. Esta lección no construye ninguna tabla nueva — reutiliza dim_store de la lección 3, sin ninguna modificación. Lo que construye es el vocabulario preciso de dimensión conformada, el concepto que la lección 6 (bus matrix) va a generalizar en un mapa completo de todos los procesos de Kiosko.
Una analogía: el directorio único de la empresa
Piensa en una empresa mediana, con varios departamentos: ventas, logística, contabilidad. Cada uno de esos departamentos, en algún momento, necesita saber "¿en qué sucursal ocurrió esto?" — ventas para saber dónde se cerró un contrato, logística para saber a dónde despachar un pedido, contabilidad para saber a qué centro de costos asignar un gasto. Una empresa mal organizada tendría tres directorios de sucursales, uno por departamento, cada uno mantenido por separado — y el día que una sucursal cambie de dirección, alguien tendría que actualizar los tres directorios, con el riesgo real de que uno se actualice y los otros dos queden desactualizados, silenciosamente, hasta que alguien note la discrepancia.
Una empresa bien organizada tiene un solo directorio de sucursales, compartido por los tres departamentos — se actualiza una vez, en un solo lugar, y los tres departamentos ven siempre la misma verdad. Eso es, exactamente, lo que Kimball llama una dimensión conformada: la misma tabla de dimensión —con la misma llave, el mismo significado, los mismos valores— reutilizada por más de un proceso de negocio, sin duplicarse. dim_store, tal como la construiste en la lección 3, es ese directorio único: cuando el módulo 6 construya fact_sessions, va a unirse contra la misma dim_store que fact_orders ya usa hoy — no una copia, no una versión nueva con otro nombre.
Ejemplo trabajado: dim_store, lista para un segundo proceso que todavía no existe
Reconstruye dim_store exactamente como en la lección 3, y verifica algo que todavía no puedes probar con una tabla real —porque fact_sessions no existe hasta el módulo 6—, pero que sí puedes razonar con precisión usando una estructura de datos que documenta qué dimensiones necesita cada proceso.
# conformed_dimensions.py
import duckdb
con = duckdb.connect()
con.execute("CREATE TABLE dim_store_natural (store_id VARCHAR, store_name VARCHAR, city VARCHAR)")
con.executemany("INSERT INTO dim_store_natural VALUES (?, ?, ?)", [
("S01", "Kiosko Centro", "Bogota"),
("S02", "Kiosko Norte", "Lima"),
("S03", "Kiosko Sur", "Santiago"),
])
con.execute("""
CREATE TABLE dim_store AS
SELECT ROW_NUMBER() OVER (ORDER BY store_id) AS store_key, store_id, store_name, city
FROM dim_store_natural
""")
print("=== dim_store, tal como la construyo la leccion 3 -- una sola tabla, una sola vez ===")
print(con.sql("SELECT * FROM dim_store ORDER BY store_key"))
# Dos procesos de negocio distintos que, en algun momento de esta guia, van a necesitar
# "en que tienda ocurrio esto". orders ya existe (fact_orders). sessions es el funnel de
# clickstream que el modulo 6 va a construir como fact_sessions -- no se construye aqui,
# solo se nombra para ilustrar el punto de esta leccion.
BUS_MATRIX_PROCESSES = {
"orders": {"dimensions": ["dim_store", "dim_product", "dim_date"], "fact_table": "fact_orders", "status": "construido (M1-M2)"},
"sessions": {"dimensions": ["dim_store", "dim_date"], "fact_table": "fact_sessions", "status": "planeado (M6)"},
}
print("\n=== Que dimensiones necesita cada proceso de negocio de Kiosko ===")
for process, info in BUS_MATRIX_PROCESSES.items():
print(f"{process:10} -> {info['fact_table']:14} usa {info['dimensions']} [{info['status']}]")
conformed = set(BUS_MATRIX_PROCESSES["orders"]["dimensions"]) & set(BUS_MATRIX_PROCESSES["sessions"]["dimensions"])
print(f"\nDimensiones CONFORMADAS entre orders y sessions: {sorted(conformed)}")
print("Estas son las dimensiones que fact_sessions, cuando el modulo 6 la construya, va a unir")
print("contra la MISMA tabla dim_store que ya existe -- no una copia, no una version nueva.")
# Ilustracion concreta: aunque fact_sessions no existe todavia, dim_store YA tiene todo
# lo que necesitaria para unirse con ella, sin agregar ni una fila nueva.
hypothetical_session_store_ids = {"S01", "S02"}
placeholders = ", ".join(f"'{s}'" for s in sorted(hypothetical_session_store_ids))
print(f"\n=== dim_store ya cubre las tiendas que un fact_sessions hipotetico usaria hoy ===")
print(con.sql(f"SELECT store_key, store_id, store_name FROM dim_store WHERE store_id IN ({placeholders}) ORDER BY store_key"))
Qué esperar. Al correr python3 conformed_dimensions.py, la salida es exactamente esta:
=== dim_store, tal como la construyo la leccion 3 -- una sola tabla, una sola vez ===
┌───────────┬──────────┬───────────────┬──────────┐
│ store_key │ store_id │ store_name │ city │
│ int64 │ varchar │ varchar │ varchar │
├───────────┼──────────┼───────────────┼──────────┤
│ 1 │ S01 │ Kiosko Centro │ Bogota │
│ 2 │ S02 │ Kiosko Norte │ Lima │
│ 3 │ S03 │ Kiosko Sur │ Santiago │
└───────────┴──────────┴───────────────┴──────────┘
=== Que dimensiones necesita cada proceso de negocio de Kiosko ===
orders -> fact_orders usa ['dim_store', 'dim_product', 'dim_date'] [construido (M1-M2)]
sessions -> fact_sessions usa ['dim_store', 'dim_date'] [planeado (M6)]
Dimensiones CONFORMADAS entre orders y sessions: ['dim_date', 'dim_store']
Estas son las dimensiones que fact_sessions, cuando el modulo 6 la construya, va a unir
contra la MISMA tabla dim_store que ya existe -- no una copia, no una version nueva.
=== dim_store ya cubre las tiendas que un fact_sessions hipotetico usaria hoy ===
┌───────────┬──────────┬───────────────┐
│ store_key │ store_id │ store_name │
│ int64 │ varchar │ varchar │
├───────────┼──────────┼───────────────┤
│ 1 │ S01 │ Kiosko Centro │
│ 2 │ S02 │ Kiosko Norte │
└───────────┴──────────┴───────────────┘
La intersección de conjuntos (set & set) confirma, con código, lo que la analogía del directorio único ya adelantó: dim_store y dim_date son las dos dimensiones que ambos procesos —el que ya existe (orders) y el que va a existir (sessions)— necesitan. dim_product, en cambio, no aparece en la intersección: hoy solo orders la usa, porque el grano de fact_sessions (una fila por sesión de navegación, no por producto visto) no incluye ningún producto específico como parte de su estructura. Eso no significa que dim_product sea menos importante — significa, simplemente, que todavía no es una dimensión conformada, porque solo tiene un consumidor.
Diagrama: la misma dimensión, dos procesos
flowchart TD
DS["dim_store\n(UNA sola tabla,\nstore_key 1, 2, 3)"]
DD["dim_date\n(UNA sola tabla,\ndate_key por dia de agosto)"]
DP["dim_product\n(una sola tabla,\nsolo un consumidor hoy)"]
FO["fact_orders\n(construido, M1-M2)"]
FS["fact_sessions\n(planeado, M6)"]
DS --> FO
DS -.->|"conformada:\nel mismo store_key"| FS
DD --> FO
DD -.->|"conformada:\nel mismo date_key"| FS
DP --> FO
Profundización: la definición precisa de Kimball, y por qué "conformada" no significa "idéntica"
Kimball define una dimensión conformada con dos condiciones, ambas necesarias: las llaves deben ser las mismas (el mismo store_key = 2 significa "Kiosko Norte, Lima" en cualquier hecho que lo use), y el significado de los atributos debe ser idéntico (city significa lo mismo, con los mismos valores posibles, sin importar desde qué hecho se consulte). No es suficiente que dos tablas se parezcan — tienen que ser, literalmente, la misma tabla, o dos tablas garantizadas de mantenerse sincronizadas por el mismo proceso de carga.
Vale la pena aclarar un malentendido común: "dimensión conformada" no significa que todos los hechos que la usan tengan que verse iguales entre sí, ni que compartan el mismo grano. fact_orders tiene grano de "una línea de orden"; fact_sessions, cuando exista, va a tener grano de "una sesión completa de navegación" — dos grano completamente distintos, dos procesos de negocio distintos, dos tablas de hechos con columnas totalmente diferentes. Lo único que se conforma es la dimensión compartida —dim_store, con su store_key— no los hechos que la consumen. Esta distinción importa porque es fácil, al empezar a modelar un warehouse con varios procesos, asumir que "conformar" implica unificar todo — y no es así: cada hecho conserva su propio grano y su propia forma; solo las dimensiones que comparten se mantienen como una sola fuente de verdad.
Esta es también la razón práctica por la que agregar la llave sustituta en la lección 3, antes de que existiera ningún segundo proceso, resultó ser la decisión correcta: store_key ya es estable, ya está generada de forma determinista, y cuando el módulo 6 construya fact_sessions, esa tabla nueva simplemente va a apuntar al mismo store_key que fact_orders ya usa — ningún trabajo de "sincronizar" dos dimensiones distintas, porque nunca hubo dos dimensiones para empezar.
Errores comunes
Crear una copia de dim_store específica para un proceso nuevo, "para no tocar la original". Qué pasa: alguien, al construir un hecho nuevo que necesita información de tienda, crea dim_store_sessions en vez de reutilizar dim_store — con la idea de que así evita el riesgo de romper algo que ya funciona. Por qué pasa: tocar (o depender de) una tabla que otro proceso ya usa se siente arriesgado, y duplicarla parece una forma segura de aislar el trabajo nuevo. Cómo detectarlo: si tu warehouse tiene dos tablas con la misma información de tienda, bajo nombres distintos, tienes exactamente el problema de "tres directorios de sucursales" de la analogía de esta lección — el día que una tienda cambie de nombre, alguien tiene que recordar actualizar ambas copias, y el riesgo de que se desincronicen es real, no teórico. Cómo corregirlo: cualquier hecho nuevo que necesite "en qué tienda" reutiliza la dim_store que ya existe, uniéndose por store_key — nunca duplicarla.
Pensar que dos dimensiones son conformadas solo porque tienen las mismas columnas. Qué pasa: alguien ve dos tablas con columnas llamadas igual (store_id, store_name, city) y las declara conformadas, sin verificar que ambas contengan exactamente los mismos valores para las mismas llaves. Por qué pasa: los nombres de columna coincidiendo se sienten como evidencia suficiente. Cómo detectarlo: si dos tablas con columnas del mismo nombre pueden, en algún momento, tener valores distintos para la misma llave —por ejemplo, una dim_store "vieja" que no se actualizó cuando la otra sí—, no son conformadas, son dos tablas parecidas que coincidentemente comparten esquema. Cómo corregirlo: la prueba real de conformidad no es el nombre de las columnas — es que ambos procesos consulten, literalmente, la misma tabla física (o una vista sobre ella), garantizando que nunca puedan divergir.
Asumir que dim_product también es conformada, porque "eventualmente todo se conecta". Qué pasa: alguien, al ver que dim_store y dim_date son conformadas entre orders y sessions, asume por extensión que dim_product también lo es, porque intuitivamente "los productos también importan para las sesiones" (alguien navega páginas de productos, después de todo). Por qué pasa: parece razonable que, si dos dimensiones se comparten, la tercera también debería. Cómo detectarlo: revisa el grano exacto que el diseño de esta guía declara para fact_sessions —una fila por sesión completa (session_id, store_id, session_date, milestones de vista/carrito/compra)—; ningún producto específico forma parte de ese grano. Cómo corregirlo: una dimensión es conformada solo si el grano del hecho realmente la necesita como parte de su estructura — no por intuición de negocio. dim_product sigue siendo, por ahora, una dimensión de un solo proceso; eso podría cambiar en el futuro si Kiosko decidiera modelar el grano de sesión a nivel de producto visto, pero esa es una decisión de diseño distinta, fuera del alcance de esta guía.
Ejercicios
Ejercicio 1 — Agrega un tercer proceso hipotético a BUS_MATRIX_PROCESSES. El módulo 6 también va a construir fact_store_activity (actividad diaria por tienda, con arreglos de revenue de 7 y 30 días). Agrega esa entrada al diccionario BUS_MATRIX_PROCESSES del ejemplo trabajado, con las dimensiones dim_store y dim_date (sin dim_product), y vuelve a calcular la intersección de dimensiones conformadas entre los tres procesos.
Ver solución
BUS_MATRIX_PROCESSES["store_activity"] = {
"dimensions": ["dim_store", "dim_date"],
"fact_table": "fact_store_activity",
"status": "planeado (M6)",
}
conformed_all = (
set(BUS_MATRIX_PROCESSES["orders"]["dimensions"])
& set(BUS_MATRIX_PROCESSES["sessions"]["dimensions"])
& set(BUS_MATRIX_PROCESSES["store_activity"]["dimensions"])
)
print(f"Dimensiones conformadas entre los 3 procesos: {sorted(conformed_all)}")
Salida esperada:
Dimensiones conformadas entre los 3 procesos: ['dim_date', 'dim_store']
El resultado no cambia frente a la comparación de solo dos procesos — dim_store y dim_date siguen siendo las únicas dos dimensiones compartidas por los tres. Esto es exactamente lo que la lección 6 (bus matrix) va a formalizar en un mapa completo: dim_store y dim_date son las dimensiones "universales" de Kiosko —las que casi cualquier proceso de negocio termina necesitando—, mientras que dim_product sigue siendo específica de la venta.
Ejercicio 2 — Verifica que dim_store no cambió al "prepararse" para un segundo proceso. Usando dim_store del ejemplo trabajado, escribe una consulta que confirme que sigue teniendo exactamente 3 filas y las mismas llaves (store_key 1, 2, 3) que tenía en la lección 3 — una verificación de que "hacerla conformada" no significó ningún cambio de estructura ni de datos.
Ver solución
print(con.sql("""
SELECT COUNT(*) AS total_rows, MIN(store_key) AS min_key, MAX(store_key) AS max_key
FROM dim_store
"""))
Salida esperada:
┌────────────┬─────────┬─────────┐
│ total_rows │ min_key │ max_key │
│ int64 │ int64 │ int64 │
├────────────┼─────────┼─────────┤
│ 3 │ 1 │ 3 │
└────────────┴─────────┴─────────┘
Los mismos números exactos de la lección 3 — porque, precisamente, no hubo ningún cambio. La dimensión ya estaba lista para ser conformada desde el momento en que se construyó bien, en la lección 3 — "conformar" no es una operación que se le hace a una dimensión, es una propiedad que una dimensión bien construida ya tiene, disponible para cuando un segundo proceso la necesite.
Ejercicio 3 — Explica, sin código, qué pasaría si dim_store tuviera hoy dos llaves distintas para "Kiosko Norte". Imagina, hipotéticamente, que por un error de carga dim_store tuviera dos filas para la misma tienda de Lima —store_key = 2 y store_key = 5, ambas con store_id = "S02"—. En 2-3 frases, explica por qué esto rompería la propiedad de dimensión conformada, incluso si ambos procesos siguen usando "la misma tabla física".
Ver solución
Aunque orders y sessions seguirían consultando la misma tabla física dim_store, la propiedad de conformidad no depende solo de "ser la misma tabla" — depende de que la llave identifique de forma única y consistente a cada entidad. Si fact_orders se unió, en algún momento, usando store_key = 2 para las ventas de Lima, y fact_sessions se uniera usando store_key = 5 para las sesiones de la misma tienda, ambos procesos estarían "de acuerdo" en que existe una tienda en Lima, pero en completo desacuerdo sobre qué llave la representa — cualquier reporte que intentara comparar ventas y sesiones de Lima por store_key fallaría en silencio, sin ningún error visible. Esto es exactamente el tipo de problema que la verificación de integridad de llaves (como la del ejercicio 2 de la lección 3) existe para prevenir — una dimensión conformada exige, además de ser una sola tabla, que su llave nunca tenga ambigüedad ni duplicados sobre la misma entidad de negocio.
Resumen y siguiente paso
En esta lección aprendiste el vocabulario preciso de dimensión conformada: la misma tabla, con la misma llave y el mismo significado, reutilizada por más de un proceso de negocio, en vez de duplicarse. Con BUS_MATRIX_PROCESSES y una intersección de conjuntos, confirmaste que dim_store y dim_date —tal como quedaron construidas en las lecciones 3 y 4— ya están listas para servir a fact_sessions, el proceso que el módulo 6 va a construir, sin necesitar ningún cambio hoy. dim_product, en cambio, sigue siendo específica de un solo proceso, porque el grano de fact_sessions no la necesita.
Antes de avanzar deberías poder: definir "dimensión conformada" con las dos condiciones precisas de Kimball (misma llave, mismo significado); explicar por qué "conformada" no implica que los hechos que la usan compartan grano o estructura; y nombrar, de memoria, cuáles de las tres dimensiones de Kiosko son conformadas hoy y cuál no.
La lección 6 generaliza esta idea en una herramienta de planeación completa: el bus matrix, el mapa que muestra, de un vistazo, qué dimensión sirve a qué proceso — para todos los procesos de Kiosko a la vez, no solo el par que comparaste en esta lección.
Recursos
- Kimball Group — "Star Schema / OLAP Cube" — la fuente que define formalmente las dimensiones conformadas y su papel en la arquitectura de bus de un warehouse. kimballgroup.com/data-warehouse-business-intelligence-resources/kimball-techniques/dimensional-modeling-techniques/star-schema-olap-cube. En inglés.
- "The Data Warehouse Toolkit", 3ra edición (Kimball & Ross, Wiley) — el capítulo sobre arquitectura de bus desarrolla en profundidad el concepto de dimensiones conformadas usado en esta lección. wiley.com/en-jp/The+Data+Warehouse+Toolkit. En inglés.
- Python — documentación oficial de operaciones de conjuntos (
set), usada en esta lección para calcular la intersección de dimensiones entre procesos de negocio. docs.python.org/3/library/stdtypes.html#set-types-set-frozenset. En inglés.