Módulo 1: From Flat Tables To Dimensional Models
Qué cambia cuando el grano es explícito
Descripción
Hasta ahora, esta guía ha declarado el grano de fact_orders con una consulta y una frase formal. Esta lección responde una pregunta más incómoda: ¿de verdad importa, en la práctica, haberlo hecho así? ¿Qué se habría roto si alguien, apurado, hubiera declarado el grano como "una orden" en vez de "una línea de orden" — la trampa que la lección 5 ya advirtió, pero que hoy no se manifiesta porque ambas lecturas dan el mismo número (40)? Esta lección construye el escenario donde la diferencia deja de ser teórica, con una consulta ejecutada de verdad.
Conexión con el módulo. Esta lección no cambia el fact_orders real de Kiosko —sigue siendo, exactamente, las cuarenta filas de la lección 5—; agrega, temporalmente y de forma controlada, una orden hipotética con dos líneas de producto, para demostrar con números por qué la declaración precisa de la lección 5 importa incluso cuando, hoy, coincide con una declaración más simple.
Una analogía: el seguro que nunca usaste, hasta que lo necesitaste
Alguien que paga un seguro de auto durante diez años sin tener nunca un accidente podría, razonablemente, preguntarse si ese gasto valió la pena. La respuesta no depende de si tuvo un accidente — depende de si, el día que lo tuviera, el seguro habría respondido correctamente. Un seguro mal contratado, con una cobertura mal declarada, puede pasar diez años sin que nadie note el problema — hasta el día del choque, cuando la declaración imprecisa ("cobertura básica" en vez de "cobertura completa, con el detalle exacto de qué cubre") termina costando mucho más de lo esperado.
Declarar el grano como "una línea de orden" en vez de "una orden" es exactamente ese seguro: hoy, con Kiosko vendiendo un solo producto por orden, ambas declaraciones producen el mismo número, y la diferencia se siente académica. Esta lección es el "choque simulado" — el escenario controlado donde, si Kiosko empezara a permitir múltiples productos en una sola orden (algo perfectamente razonable para una tienda de conveniencia real), la declaración imprecisa dejaría de responder correctamente, y la precisa seguiría funcionando sin ningún cambio.
Ejemplo trabajado: una orden hipotética con dos líneas de producto
Parte del fact_orders real de la lección 5, ya cargado en DuckDB con sus cuarenta filas. Ahora, agrega —de forma explícita y controlada, no como parte del dato real de Kiosko— una orden hipotética: un cliente que compra dos productos distintos en una sola visita, algo que el punto de venta actual de Kiosko no genera hoy, pero que es una extensión de negocio perfectamente plausible.
# hypothetical_multiline_order.py -- continua sobre el con y fact_orders de la leccion 5
print("=== Grano ANTES del escenario hipotetico (fact_orders real, 40 filas) ===")
print(con.sql("""
SELECT
COUNT(*) AS total_rows,
COUNT(DISTINCT order_id) AS distinct_order_ids,
COUNT(DISTINCT order_id || '-' || product_id) AS distinct_order_product_lines
FROM fact_orders
"""))
# Escenario hipotetico: un cliente compra P001 y P002 en la MISMA orden (ORD-9001)
# Fijo y deterministico -- no es dato real de Kiosko, es una extension controlada
con.execute("""
INSERT INTO fact_orders VALUES
('ORD-9001', 'S01', 'P001', 2, 0.55, 1.10, '2026-08-10T08:00:00'),
('ORD-9001', 'S01', 'P002', 1, 1.20, 1.20, '2026-08-10T08:00:00')
""")
print("\n=== Grano DESPUES de agregar una orden hipotetica multi-linea ===")
print(con.sql("""
SELECT
COUNT(*) AS total_rows,
COUNT(DISTINCT order_id) AS distinct_order_ids,
COUNT(DISTINCT order_id || '-' || product_id) AS distinct_order_product_lines
FROM fact_orders
"""))
Qué esperar. Al correr python3 hypothetical_multiline_order.py, la salida es exactamente esta:
=== Grano ANTES del escenario hipotetico (fact_orders real, 40 filas) ===
┌────────────┬─────────────────────┬──────────────────────────────┐
│ total_rows │ distinct_order_ids │ distinct_order_product_lines │
│ int64 │ int64 │ int64 │
├────────────┼─────────────────────┼──────────────────────────────┤
│ 40 │ 40 │ 40 │
└────────────┴─────────────────────┴──────────────────────────────┘
=== Grano DESPUES de agregar una orden hipotetica multi-linea ===
┌────────────┬─────────────────────┬──────────────────────────────┐
│ total_rows │ distinct_order_ids │ distinct_order_product_lines │
│ int64 │ int64 │ int64 │
├────────────┼─────────────────────┼──────────────────────────────┤
│ 42 │ 41 │ 42 │
└────────────┴─────────────────────┴──────────────────────────────┘
Ahí está la diferencia, con números reales. Después de agregar ORD-9001 con sus dos líneas, total_rows sube a 42 (40 + 2, correcto: se agregaron dos filas físicas). Pero distinct_order_ids sube solo a 41 (40 + 1, porque ORD-9001 es un solo order_id, repetido en dos filas). Y distinct_order_product_lines sube a 42 — exactamente igual a total_rows.
Esto es la prueba concreta que la lección 5 prometió: si hubieras declarado el grano usando solo order_id ("una fila representa una orden"), esta verificación fallaría —total_rows (42) ya no coincidiría con distinct_order_ids (41)—, y esa discrepancia sería la señal de que el grano real ya no es "una orden", sino algo más fino. La declaración correcta —order_id || '-' || product_id, "una línea de orden"— sigue coincidiendo perfectamente (42 == 42), sin que tuvieras que cambiar ni una palabra de la declaración original de la lección 5. El grano bien declarado sobrevive al cambio de negocio; el grano mal declarado se rompe en el primer caso que no anticipó.
Diagrama: el mismo grano, verificado en dos escenarios
flowchart TD
subgraph Escenario1["Escenario real (40 filas, hoy)"]
A1["total_rows = 40"]
A2["distinct order_id = 40"]
A3["distinct order_id+product_id = 40"]
A1 -.coincide.-> A2
A1 -.coincide.-> A3
end
subgraph Escenario2["Escenario hipotetico (+1 orden multi-linea)"]
B1["total_rows = 42"]
B2["distinct order_id = 41 -- YA NO COINCIDE"]
B3["distinct order_id+product_id = 42 -- SIGUE COINCIDIENDO"]
B1 -.se rompe.-> B2
B1 -.se mantiene.-> B3
end
Escenario1 -->|"agregar ORD-9001\ncon 2 lineas"| Escenario2
Profundización: por qué esto no es un ejercicio artificial
Vale la pena ser honesto sobre algo: hoy, en el fact_orders real de Kiosko, este escenario nunca ocurre — el punto de venta que foundations diseñó genera una orden por cada línea de producto, así que order_id y la combinación order_id-product_id siempre coinciden. Alguien podría preguntar, con razón, si esta lección resuelve un problema que en realidad no existe.
La respuesta honesta es: hoy no existe, pero es exactamente el tipo de cambio que ocurre todo el tiempo en sistemas de producción reales, sin que nadie avise con anticipación. Un punto de venta que se actualiza para permitir "canasta con varios productos" en una sola transacción, un sistema de origen que cambia de proveedor y trae un formato ligeramente distinto, un equipo de producto que decide agrupar compras relacionadas bajo un mismo identificador de sesión — cualquiera de estos cambios, comunes en la vida real de un warehouse, alteraría silenciosamente el grano de una tabla que nunca declaró con precisión qué esperaba. Si la tabla tiene consultas río abajo (dashboards, reportes, otros pipelines) que asumieron, sin verificarlo, que order_id identificaba una fila única, esos consumidores empezarían a dar resultados incorrectos — silenciosamente, sin ningún error visible, exactamente el tipo de bug más caro de encontrar en producción.
Esto conecta directamente con dos módulos que siguen. En el módulo 5, vas a aprender a detectar duplicados reales con ROW_NUMBER()/QUALIFY — una situación distinta a esta (ahí el problema es una fila repetida por error, no una fila nueva y legítima que cambia el grano), pero la misma disciplina de "verificar con una consulta, no asumir" es la que sostiene ambas técnicas. Y en el módulo 7, vas a construir validate_gold_schema(), una función que compara columnas esperadas contra columnas reales antes de publicar cualquier tabla gold — el mismo espíritu de esta lección, llevado a una verificación automatizada y permanente, no manual y de una sola vez.
Errores comunes
Pensar que este escenario "rompió" fact_orders de verdad. Qué pasa: alguien, después de correr el ejemplo trabajado, se preocupa porque su fact_orders ahora tiene 42 filas en vez de 40, y no sabe cómo "revertir" el cambio. Por qué pasa: el INSERT de esta lección modifica la conexión de DuckDB en memoria de la sesión actual, y puede sentirse como un cambio permanente. Cómo detectarlo: si cierras la conexión de DuckDB (con.close()) o simplemente vuelves a correr el script completo de la lección 5 desde cero, fact_orders vuelve a tener exactamente 40 filas — el INSERT de esta lección nunca tocó ningún archivo en disco. Cómo corregirlo: trata este ejemplo como lo que es, un experimento controlado en memoria — si quieres seguir trabajando con el fact_orders real de Kiosko después de esta lección, vuelve a correr declare_grain.py de la lección 5 en una conexión nueva.
Concluir que la lección demuestra que Kiosko "debería" permitir órdenes multi-línea. Qué pasa: alguien interpreta esta lección como una recomendación de negocio —que Kiosko debería cambiar su punto de venta para permitir varios productos por orden—. Por qué pasa: el ejemplo hipotético se siente, por su detalle, como una propuesta real. Cómo detectarlo: si tu conclusión de esta lección es sobre el negocio de Kiosko en vez de sobre el modelado de datos, te desviaste del objetivo. Cómo corregirlo: el punto de esta lección es exclusivamente técnico — mostrar que una declaración de grano precisa sobrevive a un cambio hipotético que una declaración imprecisa no sobreviviría. No es, en ningún sentido, una recomendación sobre cómo debería operar el negocio de Kiosko.
Asumir que declarar el grano con precisión "cuesta más trabajo" y no vale la pena si hoy coincide. Qué pasa: alguien argumenta que, como hoy COUNT(DISTINCT order_id) y COUNT(DISTINCT order_id || '-' || product_id) dan el mismo número, no vale la pena escribir la versión más precisa — es "trabajo extra sin beneficio inmediato". Por qué pasa: el costo de escribir la llave compuesta es real y visible hoy; el beneficio —sobrevivir a un cambio futuro— es invisible hasta que el cambio ocurre. Cómo detectarlo: si tu razonamiento es "total, da el mismo número", estás optimizando para el presente sin considerar el costo de un bug silencioso más adelante. Cómo corregirlo: el costo de escribir order_id || '-' || product_id en vez de order_id es mínimo —una función de concatenación más—; el costo de no hacerlo, el día que el grano real cambie sin que nadie lo note, es mucho mayor —dashboards incorrectos, decisiones de negocio basadas en números mal agregados—. Esta lección existe, precisamente, para hacer ese costo futuro visible hoy.
Ejercicios
Ejercicio 1 — Repite el experimento con una orden de tres líneas. Modifica el ejemplo trabajado para agregar una orden hipotética distinta, ORD-9002, con tres líneas de producto distintas (elige tú los productos y cantidades, manteniéndolos fijos y deterministas). Corre la consulta de verificación de grano y confirma qué números cambian y cuáles no.
Ver solución
con.execute("""
INSERT INTO fact_orders VALUES
('ORD-9002', 'S02', 'P001', 1, 0.55, 0.55, '2026-08-10T09:00:00'),
('ORD-9002', 'S02', 'P002', 1, 1.20, 1.20, '2026-08-10T09:00:00'),
('ORD-9002', 'S02', 'P003', 1, 0.75, 0.75, '2026-08-10T09:00:00')
""")
print(con.sql("""
SELECT
COUNT(*) AS total_rows,
COUNT(DISTINCT order_id) AS distinct_order_ids,
COUNT(DISTINCT order_id || '-' || product_id) AS distinct_order_product_lines
FROM fact_orders
"""))
Salida esperada (continuando sobre la tabla que ya tenía ORD-9001 del ejemplo trabajado, ahora con 42 filas):
┌────────────┬─────────────────────┬──────────────────────────────┐
│ total_rows │ distinct_order_ids │ distinct_order_product_lines │
│ int64 │ int64 │ int64 │
├────────────┼─────────────────────┼──────────────────────────────┤
│ 45 │ 42 │ 45 │
└────────────┴─────────────────────┴──────────────────────────────┘
total_rows sube a 45 (42 + 3), distinct_order_ids sube solo a 42 (41 + 1, porque las tres líneas nuevas comparten el mismo order_id), y distinct_order_product_lines sube a 45 — de nuevo, igual a total_rows. El patrón se confirma con una orden de tres líneas exactamente igual que con una de dos: la declaración de grano por order_id solo se sigue rompiendo más (la brecha entre total_rows y distinct_order_ids crece), mientras que la declaración por línea de orden se sigue sosteniendo sin ningún ajuste.
Ejercicio 2 — Calcula cuánto "se rompería" un reporte mal diseñado. Imagina un reporte (hipotético, no lo construyas) que calculara "revenue promedio por orden" dividiendo SUM(revenue) entre COUNT(DISTINCT order_id). Usando los números del ejemplo trabajado (después de agregar ORD-9001), explica en 2-3 frases si ese reporte seguiría siendo correcto con el escenario multi-línea, y por qué.
Ver solución
COUNT(DISTINCT order_id) sigue siendo la métrica correcta para "número de órdenes" incluso en el escenario multi-línea —de hecho, es exactamente la pregunta que esa columna sí responde bien: cuántas transacciones distintas hubo, sin importar cuántas líneas tenga cada una—. El reporte de "revenue promedio por orden" seguiría siendo correcto, porque divide el revenue total entre el número real de transacciones (41 después de agregar ORD-9001), no entre el número de líneas. El error solo aparecería si alguien usara COUNT(DISTINCT order_id) esperando que representara el número de líneas de producto vendidas —esa es la métrica que COUNT(DISTINCT order_id || '-' || product_id) responde correctamente, y COUNT(DISTINCT order_id) no—. La lección de este ejercicio: cada consulta de conteo debe usarse para la pregunta que realmente responde, y esa pregunta depende, siempre, del grano exacto que se está contando.
Ejercicio 3 — Argumenta por qué el grano de foundations, sin declarar formalmente, "funcionó igual" durante toda esa guía. Foundations nunca declaró el grano con una consulta como la de la lección 5, y aun así su pipeline funcionó correctamente de principio a fin. En 2-3 frases, explica por qué eso fue posible, y por qué no sería una garantía segura para un warehouse que sigue creciendo.
Ver solución
Foundations funcionó sin declarar el grano formalmente porque, durante toda esa guía, el dato real nunca violó la suposición implícita —cada orden siempre tuvo exactamente un producto, así que "una orden" y "una línea de orden" coincidieron todo el tiempo, sin que la ambigüedad se manifestara nunca—. Eso no es una garantía de que seguiría funcionando: fue, en el fondo, buena suerte con los datos, no una propiedad verificada del diseño. Un warehouse real, que recibe datos de sistemas de origen que cambian con el tiempo, no puede depender de que la suerte se mantenga — declarar y verificar el grano con una consulta, como esta guía enseña desde la lección 5, es la diferencia entre confiar en que algo seguirá siendo cierto y saber, con evidencia, que lo es hoy y tener una forma de volver a comprobarlo mañana.
Resumen y siguiente paso
En esta lección demostraste, con una consulta ejecutada sobre un escenario hipotético controlado, por qué la declaración precisa de grano de la lección 5 —"una línea de orden", no "una orden"— importa en la práctica y no solo en el vocabulario. Agregar una orden con dos líneas de producto rompió la coincidencia entre total_rows y distinct_order_ids (42 contra 41), pero no rompió la coincidencia entre total_rows y distinct_order_product_lines (42 contra 42) — la prueba concreta de que un grano bien declarado sobrevive a cambios que un grano mal declarado no sobrevive.
Antes de avanzar deberías poder: explicar, con números propios, la diferencia entre las dos formas de declarar el grano de fact_orders; reproducir el experimento de esta lección con una orden hipotética distinta; y argumentar por qué "hoy da el mismo número" no es una razón suficiente para preferir la declaración menos precisa.
Con el grano declarado, verificado, y puesto a prueba contra un escenario hipotético, la lección 8 —el mini-proyecto de este módulo— reúne los cuatro pasos completos de Kimball en una sola entrega formal: la declaración de grano de Kiosko, documentada y verificada de punta a punta.
Recursos
- Kimball Group — "Four-Step Dimensional Design Process" — la fuente que sostiene por qué declarar el grano con precisión, verificado con evidencia, es el paso que hace robusto todo lo que sigue. kimballgroup.com/.../four-4-step-design-process. En inglés.
- DuckDB — documentación oficial de sentencias
INSERT, usada en esta lección para agregar el escenario hipotético sobre la tabla ya construida. duckdb.org/docs/lts/sql/statements/insert. En inglés. - Joe Reis & Matt Housley, Fundamentals of Data Engineering (O'Reilly, 2022) — el capítulo sobre calidad de datos, relevante aquí para entender por qué los cambios silenciosos en el grano de una fuente son un riesgo real de producción. oreilly.com/library/view/fundamentals-of-data/9781098108298. En inglés.