Módulo 3: Star Vs Snowflake Vs One Big Table
Mini-proyecto: las tres formas de Kiosko comparadas
Descripción
Este proyecto cierra el módulo integrando las seis piezas anteriores: normalizar una dimensión (lección 2), medir el costo de un JOIN extra con EXPLAIN (lección 3), el argumento moderno de la tabla ancha (lección 4), construir la OBT de Kiosko (lección 5), el costo real de mantener el snowflake (lección 6), y la ganancia real de consultar la OBT (lección 7). Lo que falta es reunir las tres formas en una sola entrega formal: construidas a la vez, en la misma conexión de DuckDB, verificadas número por número contra el mismo revenue que conoces desde foundations, y documentadas en una declaración —SHAPE_COMPARISON— que el resto de esta guía puede consultar sin repetir la comparación.
El proyecto tiene cinco partes. Primero, reconstruyes el star heredado del módulo 2, sin cambios. Segundo, construyes la versión snowflake — dim_category y dim_product_normalized. Tercero, construyes la versión OBT — mart_daily_sales_obt. Cuarto, verificas cruzado: confirmas que las tres formas, a pesar de sus estructuras distintas, dan exactamente el mismo revenue total. Quinto, documentas la comparación como una estructura formal, SHAPE_COMPARISON, con los números concretos que las lecciones 2 a 7 ya midieron.
Conexión con el módulo. Este proyecto no introduce ningún concepto nuevo — es la integración final de las siete lecciones anteriores, empaquetada como SHAPE_COMPARISON, la estructura que los módulos 4 a 8 de esta guía pueden citar sin volver a construir las tres formas desde cero.
Una analogía: los tres prototipos, presentados juntos al comité
Cada lección de este módulo construyó y probó un prototipo distinto por separado: el snowflake, medido con EXPLAIN y con el costo de un UPDATE; la OBT, medida con conteo de filas, columnas repetidas y tamaño en disco. Este proyecto es la reunión final: los tres prototipos, construidos uno al lado del otro, sobre la misma mesa, presentados juntos a un comité que necesita decidir con criterio — no "cuál es la única forma correcta", sino "qué forma usar para qué propósito, con la evidencia de las siete lecciones anteriores ya en la mano".
El material: todo lo que este módulo construyó, en un solo flujo
Necesitas, en la misma carpeta: kiosko.py y raw_orders.py (idénticos a los módulos 1 y 2). No necesitas ningún archivo adicional — generate_date_dim() se define directamente en el script de este proyecto, igual que en los proyectos anteriores.
La solución de referencia, verificada
Parte 1 — Reconstruir el star, sin cambios
# three_shapes_project.py -- Kiosko's star vs snowflake vs OBT, mini-proyecto de cierre del modulo 3
from datetime import date, timedelta, datetime
import duckdb
from kiosko import DIM_PRODUCT, DIM_STORE, Order, transform_fact_orders
from raw_orders import RAW_ORDERS
DAY_NAMES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
def generate_date_dim(start_date: str, end_date: str) -> list[dict]:
start = date.fromisoformat(start_date)
end = date.fromisoformat(end_date)
rows = []
current = start
while current <= end:
weekday_index = current.weekday()
rows.append({
"date_key": int(current.strftime("%Y%m%d")), "calendar_date": current,
"day_of_week": DAY_NAMES[weekday_index], "month": current.month,
"quarter": (current.month - 1) // 3 + 1, "year": current.year,
"is_weekend": weekday_index >= 5,
})
current += timedelta(days=1)
return rows
print("=== Kiosko: star vs snowflake vs OBT, entrega final del modulo 3 ===\n")
orders = [
Order(order_id=r[0], store_id=r[1], product_id=r[2], quantity=r[3],
unit_price=r[4], order_ts=datetime.fromisoformat(r[5]))
for r in RAW_ORDERS
]
fact_orders = transform_fact_orders(orders, DIM_STORE, DIM_PRODUCT)
con = duckdb.connect()
con.execute("""
CREATE TABLE fact_orders (
order_id VARCHAR, store_id VARCHAR, product_id VARCHAR,
quantity INTEGER, unit_price DOUBLE, revenue DOUBLE, order_ts TIMESTAMP
)
""")
con.executemany("INSERT INTO fact_orders VALUES (?, ?, ?, ?, ?, ?, ?)",
[(r["order_id"], r["store_id"], r["product_id"], r["quantity"],
r["unit_price"], r["revenue"], r["order_ts"]) for r in fact_orders])
con.execute("CREATE TABLE dim_store_natural (store_id VARCHAR, store_name VARCHAR, city VARCHAR)")
con.executemany("INSERT INTO dim_store_natural VALUES (?, ?, ?)",
[(s["store_id"], s["store_name"], s["city"]) for s in DIM_STORE])
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")
con.execute("CREATE TABLE dim_product_natural (product_id VARCHAR, product_name VARCHAR, category VARCHAR, unit_cost DOUBLE)")
con.executemany("INSERT INTO dim_product_natural VALUES (?, ?, ?, ?)",
[(p["product_id"], p["product_name"], p["category"], p["unit_cost"]) for p in DIM_PRODUCT])
con.execute("CREATE TABLE dim_product AS SELECT ROW_NUMBER() OVER (ORDER BY product_id) AS product_key, product_id, product_name, category, unit_cost FROM dim_product_natural")
dim_date_rows = generate_date_dim("2026-08-01", "2026-08-31")
con.execute("""
CREATE TABLE dim_date (
date_key INTEGER, calendar_date DATE, day_of_week VARCHAR,
month INTEGER, quarter INTEGER, year INTEGER, is_weekend BOOLEAN
)
""")
con.executemany("INSERT INTO dim_date VALUES (?, ?, ?, ?, ?, ?, ?)",
[(r["date_key"], r["calendar_date"], r["day_of_week"], r["month"],
r["quarter"], r["year"], r["is_weekend"]) for r in dim_date_rows])
print("Parte 1 -- el star heredado del modulo 2, sin cambios")
for table in ["fact_orders", "dim_store", "dim_product", "dim_date"]:
count = con.sql(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
print(f" {table:12} {count:3} filas")
Esta primera parte no construye nada nuevo — reconstruye, exactamente como en la lección 8 del módulo 2, el star schema completo que sirve de línea base para las dos comparaciones que siguen.
Parte 2 — Construir la versión snowflake
con.execute("""
CREATE TABLE dim_category AS
SELECT ROW_NUMBER() OVER (ORDER BY category) AS category_id, category AS category_name
FROM (SELECT DISTINCT category FROM dim_product_natural) t
""")
con.execute("""
CREATE TABLE dim_product_normalized AS
SELECT ROW_NUMBER() OVER (ORDER BY n.product_id) AS product_key, n.product_id, n.product_name, c.category_id, n.unit_cost
FROM dim_product_natural n JOIN dim_category c ON n.category = c.category_name
""")
print("\nParte 2 -- la version snowflake: dim_category + dim_product_normalized")
print(f" dim_category {con.sql('SELECT COUNT(*) FROM dim_category').fetchone()[0]:3} filas")
print(f" dim_product_normalized {con.sql('SELECT COUNT(*) FROM dim_product_normalized').fetchone()[0]:3} filas")
Exactamente la construcción de la lección 2: dim_category normaliza las categorías distintas de dim_product_natural, y dim_product_normalized reemplaza category (texto) por category_id (llave sustituta).
Parte 3 — Construir la versión OBT
con.execute("""
CREATE TABLE mart_daily_sales_obt AS
SELECT
CAST(f.order_ts AS DATE) AS sale_date, d.day_of_week, d.is_weekend, d.month, d.quarter, d.year,
s.store_id, s.store_name, s.city, p.product_id, p.product_name, p.category, p.unit_cost,
SUM(f.quantity) AS quantity, ROUND(SUM(f.revenue), 2) AS revenue
FROM fact_orders f
JOIN dim_store s ON f.store_id = s.store_id
JOIN dim_product p ON f.product_id = p.product_id
JOIN dim_date d ON CAST(strftime(f.order_ts, '%Y%m%d') AS INTEGER) = d.date_key
GROUP BY 1,2,3,4,5,6,7,8,9,10,11,12,13
""")
print("\nParte 3 -- la version OBT: mart_daily_sales_obt")
print(f" mart_daily_sales_obt {con.sql('SELECT COUNT(*) FROM mart_daily_sales_obt').fetchone()[0]:3} filas")
Exactamente la construcción de la lección 5: un CREATE TABLE ... AS SELECT que une las cuatro tablas del star y agrupa por día, tienda y producto — el grano más grueso que colapsa cuarenta líneas de orden en treinta y nueve filas.
Parte 4 — Verificar cruzado: las tres formas, el mismo revenue
star_rev = con.sql("""
SELECT ROUND(SUM(f.revenue), 2) FROM fact_orders f
JOIN dim_product p ON f.product_id = p.product_id
""").fetchone()[0]
snowflake_rev = con.sql("""
SELECT ROUND(SUM(f.revenue), 2) FROM fact_orders f
JOIN dim_product_normalized p ON f.product_id = p.product_id
JOIN dim_category c ON p.category_id = c.category_id
""").fetchone()[0]
obt_rev = con.sql("SELECT ROUND(SUM(revenue), 2) FROM mart_daily_sales_obt").fetchone()[0]
print("\nParte 4 -- verificacion cruzada: las tres formas, el mismo revenue")
print(f" star (1 salto de JOIN): {star_rev}")
print(f" snowflake (2 saltos de JOIN): {snowflake_rev}")
print(f" OBT (0 saltos de JOIN): {obt_rev}")
assert star_rev == snowflake_rev == obt_rev == 106.15, "las tres formas no coinciden"
print(" Verificacion: las tres formas coinciden en 106.15 -- OK")
Esta es la parte que le da confianza a todo el proyecto: sin importar cuántos JOIN necesite cada forma para llegar a category, ni si el grano es "una línea de orden" (star, snowflake) o "un producto vendido en una tienda en un día" (OBT), el revenue total de negocio —el número que le importa a la gerente de Kiosko— es idéntico en las tres.
Parte 5 — Documentar la comparación como una estructura formal
SHAPE_COMPARISON = {
"star": {
"tables": ["fact_orders", "dim_store", "dim_product", "dim_date"],
"joins_to_category": 1,
"columns_widest_table": 5,
"rows_widest_table": 4,
"update_cost_category_rename": 2,
},
"snowflake": {
"tables": ["fact_orders", "dim_store", "dim_product_normalized", "dim_category", "dim_date"],
"joins_to_category": 2,
"columns_widest_table": 5,
"rows_widest_table": 4,
"update_cost_category_rename": 1,
},
"obt": {
"tables": ["mart_daily_sales_obt"],
"joins_to_category": 0,
"columns_widest_table": 15,
"rows_widest_table": 39,
"update_cost_category_rename": 22,
},
"verified_revenue_all_shapes": obt_rev,
}
print("\nParte 5 -- la declaracion formal: SHAPE_COMPARISON")
for shape, data in SHAPE_COMPARISON.items():
print(f" {shape}: {data}")
Qué esperar. Al correr python3 three_shapes_project.py completo (las cinco partes juntas), la salida es exactamente esta:
=== Kiosko: star vs snowflake vs OBT, entrega final del modulo 3 ===
Parte 1 -- el star heredado del modulo 2, sin cambios
fact_orders 40 filas
dim_store 3 filas
dim_product 4 filas
dim_date 31 filas
Parte 2 -- la version snowflake: dim_category + dim_product_normalized
dim_category 3 filas
dim_product_normalized 4 filas
Parte 3 -- la version OBT: mart_daily_sales_obt
mart_daily_sales_obt 39 filas
Parte 4 -- verificacion cruzada: las tres formas, el mismo revenue
star (1 salto de JOIN): 106.15
snowflake (2 saltos de JOIN): 106.15
OBT (0 saltos de JOIN): 106.15
Verificacion: las tres formas coinciden en 106.15 -- OK
Parte 5 -- la declaracion formal: SHAPE_COMPARISON
star: {'tables': ['fact_orders', 'dim_store', 'dim_product', 'dim_date'], 'joins_to_category': 1, 'columns_widest_table': 5, 'rows_widest_table': 4, 'update_cost_category_rename': 2}
snowflake: {'tables': ['fact_orders', 'dim_store', 'dim_product_normalized', 'dim_category', 'dim_date'], 'joins_to_category': 2, 'columns_widest_table': 5, 'rows_widest_table': 4, 'update_cost_category_rename': 1}
obt: {'tables': ['mart_daily_sales_obt'], 'joins_to_category': 0, 'columns_widest_table': 15, 'rows_widest_table': 39, 'update_cost_category_rename': 22}
verified_revenue_all_shapes: 106.15
Detente en la Parte 4 y en la Parte 5 juntas, porque son las que resumen todo el módulo en una sola imagen. 106.15 aparece tres veces, una por cada forma — el hecho de negocio no cambia con la estructura del modelo. Y SHAPE_COMPARISON reúne, en una sola estructura, cada número que las siete lecciones anteriores midieron por separado: joins_to_category viene de la lección 3 (EXPLAIN), update_cost_category_rename viene de la lección 6 (el UPDATE real), rows_widest_table y columns_widest_table vienen de la lección 5 (la construcción de la OBT). Cada campo de esta estructura tiene, detrás, una lección que lo verificó con evidencia — no es una tabla de opiniones, es un resumen de mediciones.
Diagrama: las siete piezas del módulo, cerradas con evidencia
flowchart TD
A["L2: dim_category\nVERIFICADO -- 4 productos, 3 categorias"] --> B
B["L3: EXPLAIN\nVERIFICADO -- 1 HASH_JOIN vs 2"] --> C
C["L4: El argumento OBT\nFivetran + dataarchitect.studio"] --> D
D["L5: mart_daily_sales_obt\nVERIFICADO -- 39 filas, 106.15"] --> E
E["L6-L7: Cuando gana cada forma\nVERIFICADO -- costo 1/2/22, resultado identico"] --> F
F["SHAPE_COMPARISON\nel contrato formal que este proyecto entrega"]
F --> G["Modulos 4-8: pueden citar\neste contrato sin repetir la comparacion"]
Cerrando el checklist de la lección 2 del módulo 1, pieza por pieza
| Pieza del checklist (lección 2, módulo 1) | Estado al cerrar este módulo |
|---|---|
Grano de fact_orders declarado y verificado | Resuelto — módulo 1 |
Llaves sustitutas, dim_date, dimensiones conformadas | Resuelto — módulo 2 |
| Snowflake vs tabla ancha | Resuelto — ESTE MÓDULO, SHAPE_COMPARISON verificado con 106.15 en las tres formas |
| Historización (SCD) | Pendiente — módulo 4 |
| Join punto-en-el-tiempo, deduplicación | Pendiente — módulo 5 |
| Accumulating snapshot, cumulative design | Pendiente — módulo 6 |
| Dimensión junk, más de un hecho | Pendiente — módulo 7 |
Tres filas de las siete del checklist ya quedaron resueltas. El módulo 4, el siguiente en la lista, necesita específicamente el star schema que este módulo dejó intacto —no la versión snowflake ni la OBT, que fueron comparaciones paralelas dentro de este módulo—: dim_product, con category como columna de texto, es la tabla que el módulo 4 va a historizar con SCD tipo 1 y tipo 2, precisamente porque, tal como aprendiste en la lección 6, dim_product es una dimensión —fuente de verdad, no derivada— y por lo tanto el lugar correcto para registrar cómo cambia con el tiempo.
Errores comunes
Entregar SHAPE_COMPARISON sin la verificación cruzada de la Parte 4. Qué pasa: alguien, apurado por mostrar la estructura de comparación como resultado final, construye SHAPE_COMPARISON directamente después de la Parte 3, sin correr primero el assert star_rev == snowflake_rev == obt_rev == 106.15 de la Parte 4. Por qué pasa: la estructura de comparación se ve más presentable como "el entregable", y la verificación de revenue se siente como un paso preliminar descartable. Cómo detectarlo: si tu entrega final no incluye ninguna evidencia ejecutada de que las tres formas coinciden en revenue, estás documentando una comparación de estructura sin haber confirmado que las estructuras realmente representan el mismo negocio — exactamente la misma trampa que el módulo 2 ya advirtió sobre el JOIN sin verificar. Cómo corregirlo: la Parte 4 de este proyecto no es opcional — es la garantía que hace confiable todo lo que SHAPE_COMPARISON documenta en la Parte 5.
Confundir "comparé las tres formas" con "decidí cuál usar para siempre". Qué pasa: alguien termina este proyecto y, buscando una respuesta única, decide que Kiosko debería usar la OBT (o el snowflake) como su modelo permanente de aquí en adelante, descartando las otras dos formas por completo. Por qué pasa: después de un módulo entero comparando alternativas, es natural buscar un veredicto final y definitivo. Cómo detectarlo: si tu conclusión de este proyecto es una sola forma "ganadora", sin mencionar el contexto —quién consulta, con qué frecuencia cambia el dato, qué tan repetido es el patrón de consulta— perdiste el argumento central de las lecciones 6 y 7. Cómo corregirlo: recuerda SHAPE_COMPARISON no declara una forma ganadora — documenta tres estructuras válidas, cada una con su propio costo y su propio beneficio, medidos con evidencia. La decisión de cuál usar depende del caso de uso específico, no de una preferencia fija.
Asumir que dim_product_normalized o mart_daily_sales_obt van a seguir existiendo, sin cambios, en los módulos siguientes. Qué pasa: alguien, al ver que este proyecto documenta ambas tablas en SHAPE_COMPARISON, espera que el módulo 4 las use como base para historización, o que el módulo 6 construya fact_sessions uniéndose contra dim_product_normalized. Por qué pasa: SHAPE_COMPARISON presenta las tres formas con el mismo nivel de detalle, lo que puede sugerir que las tres tienen el mismo estatus permanente en la guía. Cómo detectarlo: si en un ejercicio de un módulo posterior escribes JOIN dim_product_normalized o JOIN mart_daily_sales_obt esperando que sigan sincronizadas con los cambios que vas a introducir, mezclaste el propósito de este módulo con el resto de la guía. Cómo corregirlo: como ya se advirtió en las lecciones 2 y 4 de este módulo, dim_product (star, category como texto) sigue siendo la dimensión canónica del resto de esta guía. dim_category, dim_product_normalized y mart_daily_sales_obt existen para la comparación de este módulo, documentada aquí, y no se actualizan en los módulos siguientes.
Ejercicios
Ejercicio 1 — Verifica que la OBT también reproduce el revenue por tienda y por producto. Usando mart_daily_sales_obt, escribe dos consultas que agrupen por store_name y por product_name respectivamente, y compara los resultados contra los números que ya conoces desde el módulo 1 (38.3/38.8/29.05 por tienda; 33.55/21.6/10.5/40.5 por producto).
Ver solución
print(con.sql("""
SELECT store_name, ROUND(SUM(revenue), 2) AS revenue
FROM mart_daily_sales_obt
GROUP BY store_name
ORDER BY store_name
"""))
print(con.sql("""
SELECT product_name, ROUND(SUM(revenue), 2) AS revenue
FROM mart_daily_sales_obt
GROUP BY product_name
ORDER BY product_name
"""))
Salida esperada:
┌───────────────┬─────────┐
│ store_name │ revenue │
│ varchar │ double │
├───────────────┼─────────┤
│ Kiosko Centro │ 38.3 │
│ Kiosko Norte │ 38.8 │
│ Kiosko Sur │ 29.05 │
└───────────────┴─────────┘
┌───────────────────────┬─────────┐
│ product_name │ revenue │
│ varchar │ double │
├───────────────────────┼─────────┤
│ Bottled Water 600ml │ 33.55 │
│ Instant Coffee Sachet │ 10.5 │
│ Energy Bar │ 21.6 │
│ Phone Charger Cable │ 40.5 │
└───────────────────────┴─────────┘
Exactamente los mismos números que conoces desde el módulo 1 —38.3/38.8/29.05 por tienda, 33.55/21.6/10.5/40.5 por producto—, ahora calculados sobre una tabla con un grano completamente distinto al de fact_orders (día + tienda + producto, en vez de línea de orden). Esta es la confirmación más fuerte de todo el módulo: cambiar radicalmente la forma física del modelo —de cuatro tablas normalizadas a una sola tabla ancha con un grano más grueso— no cambia ni un centavo del negocio que ese modelo representa, siempre que la agregación sea correcta.
Ejercicio 2 — Extiende SHAPE_COMPARISON con un campo de recomendación. Sin usar datetime.now(), agrega a SHAPE_COMPARISON un campo recommended_default con el valor "star", y un campo recommendation_reason que explique, en una frase, por qué el star sigue siendo la forma recomendada por defecto para el resto de esta guía.
Ver solución
SHAPE_COMPARISON["recommended_default"] = "star"
SHAPE_COMPARISON["recommendation_reason"] = (
"El star balancea flexibilidad de consulta y costo de mantenimiento; "
"snowflake y OBT se construyen encima cuando un caso de uso especifico lo justifica."
)
print(f"recommended_default: {SHAPE_COMPARISON['recommended_default']}")
print(f"recommendation_reason: {SHAPE_COMPARISON['recommendation_reason']}")
Salida esperada:
recommended_default: star
recommendation_reason: El star balancea flexibilidad de consulta y costo de mantenimiento; snowflake y OBT se construyen encima cuando un caso de uso especifico lo justifica.
Esta extensión documenta explícitamente lo que el módulo completo argumentó: el star no ganó la comparación de este módulo por ser "el más rápido" ni "el más barato de mantener" en ningún eje individual —el snowflake gana en costo de actualización, la OBT gana en velocidad de consulta—, sino por ser el punto de partida más equilibrado, sobre el cual las otras dos formas se construyen cuando un caso de uso concreto las justifica. Es, en una frase, el mismo argumento en capas de dataarchitect.studio que las lecciones 4 y 7 citaron.
Ejercicio 3 — Explica, de memoria, qué necesita el módulo 4 de este proyecto para poder empezar. Sin mirar el diseño de la guía, describe en un párrafo de 4-6 frases qué piezas de SHAPE_COMPARISON —y de las tablas construidas en este proyecto— va a necesitar el módulo 4 para historizar dim_product con SCD tipo 1 y tipo 2.
Ver solución
El módulo 4 necesita, como punto de partida, la tabla dim_product en su forma star —con category como columna de texto, product_key como llave sustituta— exactamente como quedó en el módulo 2 y sin cambios en este módulo. No necesita dim_product_normalized ni dim_category: la lección 6 de este módulo ya estableció por qué dim_product es la dimensión canónica —fuente de verdad, no derivada— y por lo tanto el lugar correcto para historizar cambios de unit_cost y category a lo largo del tiempo. Tampoco necesita mart_daily_sales_obt: esa tabla es una vista materializada derivada, y el módulo 7 (contratos Medallion) va a explicar por qué las tablas derivadas se regeneran desde la fuente después de una historización, no se historizan directamente. Lo único que el módulo 4 hereda de este proyecto es la confirmación de que dim_product sigue siendo una dimensión pequeña y simple —cuatro productos, sin ninguna versión histórica todavía— lista para que la primera fila cambie de precio o de categoría, y el modelo la registre sin perder la anterior.
Resumen y siguiente paso: el final del módulo 3
Con este mini-proyecto cierras el módulo 3 completo. Construiste las tres formas de Kiosko en la misma conexión de DuckDB: el star heredado del módulo 2 (sin cambios), la versión snowflake (dim_category + dim_product_normalized), y la tabla ancha (mart_daily_sales_obt, con su grano más grueso de día + tienda + producto). Verificaste, con un assert literal, que las tres formas coinciden en el mismo revenue total —106.15—, y documentaste toda la comparación del módulo en SHAPE_COMPARISON: un JOIN de distancia en el star, dos en el snowflake, cero en la OBT; una fila de costo de actualización en la dimensión normalizada, veintidós en la tabla ancha.
Diste el tercer paso de un camino de ocho módulos: fact_orders y dim_product (en su forma star) siguen siendo, columna por columna, las mismas tablas que ya conocías — lo que cambió es que ahora sabes, con evidencia propia, por qué esta guía las eligió como el cimiento sobre el que construir, en vez de saltar directo a la forma más normalizada o a la más aplanada.
Hacia dónde sigues. El módulo 4 —slowly-changing-dimensions— toma dim_product, tal como la dejó este proyecto, y le hace la pregunta que ninguna lección de esta guía respondió todavía: ¿qué pasa cuando el precio o la categoría de un producto cambia de verdad? SCD tipo 1 sobrescribe y pierde historia; SCD tipo 2 historiza con valid_from/valid_to/is_current — y vas a implementarlo, de verdad, con MERGE INTO sobre dos snapshots reales de products.
Recursos
- Kimball Group — "Star Schema / OLAP Cube" — la fuente completa del vocabulario dimensional que este proyecto integra: star, snowflake, y la disciplina de decidir con criterio entre ellos. kimballgroup.com/data-warehouse-business-intelligence-resources/kimball-techniques/dimensional-modeling-techniques/star-schema-olap-cube. En inglés.
- Fivetran — "Star Schema vs. OBT for Data Warehouse Performance" — el benchmark de producción que sostiene, con datos reales, el argumento cuantitativo completo de este módulo. fivetran.com/blog/star-schema-vs-obt. En inglés.
- dataarchitect.studio — "One Big Table vs the Star Schema: The Real Trade-off" — la recomendación en capas (star como base, OBT como servicio) que
SHAPE_COMPARISONadopta como principio de cierre. dataarchitect.studio/essays/one-big-table-vs-star-schema. En inglés. - DuckDB — "EXPLAIN: Inspect Query Plans" — la herramienta que la lección 3 de este módulo usó para medir, no suponer, el costo de un
JOINextra. duckdb.org/docs/current/guides/meta/explain. En inglés. - DuckDB — documentación oficial del cliente Python, la herramienta que ejecutó cada verificación de este módulo. duckdb.org/docs/current/clients/python/overview. En inglés.