Módulo 6: Accumulating And Cumulative Patterns

Presentación del módulo: dos formas de hecho que no son una línea de orden

Por qué existe este módulo

El módulo 5 cerró con una frase que anticipaba, literalmente, el tema de este módulo: "el módulo 6 —accumulating-and-cumulative-patterns— cambia de tema por completo: en vez de unir hechos contra dimensiones, construye dos patrones de tabla de hechos distintos". Vale la pena detenerse en esa frase, porque marca una ruptura real con todo lo que hiciste hasta ahora. Los cinco módulos anteriores giraron alrededor de una sola tabla de hechos —fact_orders— y de cómo unirla correctamente contra sus dimensiones: primero declarando su grano (módulo 1), luego dándole llaves sustitutas y un calendario conformado (módulo 2), luego decidiendo su forma —star, snowflake, OBT— (módulo 3), luego historizando la dimensión que cambia (módulo 4), y finalmente uniendo hechos contra esa historia sin corromperla (módulo 5). En ningún momento cambió qué tipo de hecho era fact_orders: siempre fue una tabla de hechos transaccional, en el vocabulario de Kimball —una fila por evento de negocio discreto, que nunca se modifica después de insertada—.

Este módulo introduce dos tipos de tabla de hechos que no son transaccionales, y que resuelven preguntas que fact_orders no puede responder sin importar cuántas dimensiones le agregues. La primera pregunta: "¿en qué etapa del proceso de compra se quedó cada sesión de navegación de un cliente de Kiosko?" no tiene una respuesta razonable en una tabla que solo registra transacciones completadas —una sesión que nunca compró no genera ninguna fila en fact_orders—. La segunda pregunta: "¿cuántos días activos tuvo cada tienda en los últimos 7 y 30 días?" tampoco se responde bien con una tabla transaccional, porque cada vez que alguien la consulta tendría que releer semanas o meses completos de historia para sumar algo que ya sumó ayer.

Kimball resuelve la primera pregunta con el accumulating snapshot fact table: una fila por proceso completo (una sesión, un pedido, una postulación), que se inserta incompleta cuando el proceso empieza y se actualiza in-place cada vez que el proceso avanza un paso —nunca se inserta una fila nueva por avance—. Zach Wilson, en el patrón que popularizó como cumulative table design (documentado en el repositorio cumulative-table-design de DataExpert-io, la fuente que cita este módulo), resuelve la segunda con columnas tipo arreglo: cada fila de hoy se construye tomando la fila de ayer y agregándole solo el dato de hoy, sin releer ningún día anterior a eso. Este módulo construye ambos patrones sobre datos reales de Kiosko: fact_sessions sobre los events que ya conoces, y fact_store_activity sobre el revenue que ya conoces de fact_orders.

Conexión con el módulo. Este módulo no modifica fact_orders, dim_store, dim_product ni dim_product_scd —las cuatro tablas que los módulos 1 a 5 dejaron completas—. Lo que construye son dos tablas de hechos nuevas, de un tipo distinto al que viste hasta ahora, sobre los mismos datos fijos de Kiosko (events, fact_orders) que esta guía ya generó.

Una analogía: la guía de envío que se sella, y el resumen que crece un día a la vez

Piensa en una guía de envío de un paquete: cuando el paquete sale del centro de distribución, alguien sella la casilla "despachado" con la fecha de hoy. Cuando llega a la ciudad destino, alguien sella "en tránsito local". Cuando el repartidor lo entrega, se sella "entregado". En ningún momento de ese proceso se imprime una guía nueva — es la misma guía, sellada cada vez que el paquete avanza. Si alguien la consulta a mitad de camino, ve una guía con dos sellos y una casilla vacía: no significa que el paquete "no existe", significa que todavía no llegó a esa etapa. Eso es, exactamente, un accumulating snapshot fact table: una fila que nace con algunas casillas vacías y se va sellando —actualizando— sin multiplicarse.

Ahora piensa en cómo un almacén lleva la cuenta de cuántas cajas movió en los últimos 7 días, sin releer siete días de recibos cada vez que alguien pregunta: al cierre de cada día, alguien toma el resumen de ayer —la lista de los últimos 7 números—, le agrega el número de hoy al frente, y descarta el número más viejo si ya son ocho. El resumen de hoy nunca necesitó releer el recibo del día 1; solo necesitó el resumen de ayer y el dato de hoy. Eso es, exactamente, el cumulative table design: revenue_array_7d de hoy se construye con revenue_array_7d de ayer más el revenue de hoy, nunca con un SUM sobre siete días completos de fact_orders.

Ejemplo trabajado: el mapa de este módulo, antes de construirlo

Antes de tocar código ejecutable de verdad, el mismo tipo de mapa que abrieron los módulos 3, 4 y 5 antes de construir sus patrones centrales:

# module_map.py
CONCEPTS = [
    ("Accumulating snapshot fact table", "Una fila por proceso (Kimball). Los milestones se llenan con UPDATE, nunca con un INSERT nuevo."),
    ("INSERT al inicio, UPDATE en cada milestone", "El mecanismo exacto: la fila nace incompleta y se completa in-place."),
    ("fact_sessions sobre events", "El funnel de Kiosko: view_ts, add_to_cart_ts, purchase_ts, por sesion."),
    ("Cumulative table design (Zach Wilson/DataExpert)", "Arreglo con ventana movil: el resumen de ayer + el valor de hoy, sin releer la historia completa."),
    ("Columnas tipo LIST y funciones de arreglo de DuckDB", "list_prepend, list_slice, list_sum, slicing [1:7]."),
    ("Ventanas fijas de 7 y 30 dias", "active_days_7d / active_days_30d, contando valores > 0 en el arreglo."),
]

LESSONS = [
    ("El accumulating snapshot fact table", "Kimball, EJECUTADO: una sesion, 3 milestones, 1 fila"),
    ("Modelando el funnel de sesiones de Kiosko", "fact_sessions completo, EJECUTADO: 17 sesiones"),
    ("Actualizando milestones in-place", "32 eventos -> 17 INSERT + 15 UPDATE, EJECUTADO"),
    ("Cumulative table design: el patron de Zach Wilson", "El patron ayer+hoy, EJECUTADO en 3 dias"),
    ("Ventanas moviles con columnas tipo arreglo", "fact_store_activity completo, EJECUTADO: 21 filas"),
    ("Calculando activos de 7 y 30 dias por tienda", "list_sum, active_days, EJECUTADO"),
    ("Proyecto: funnel y actividad acumulativa de Kiosko", "Las 6 lecciones anteriores integradas, EJECUTADO"),
]

print("=== Los seis conceptos centrales de este modulo ===\n")
for name, description in CONCEPTS:
    print(f"- {name}")
    print(f"  {description}\n")

print("=== Las siete lecciones que construyen sobre ellos ===\n")
for i, (name, description) in enumerate(LESSONS, start=2):
    print(f"L{i}. {name}")
    print(f"    {description}\n")

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

=== Los seis conceptos centrales de este modulo ===

- Accumulating snapshot fact table
  Una fila por proceso (Kimball). Los milestones se llenan con UPDATE, nunca con un INSERT nuevo.

- INSERT al inicio, UPDATE en cada milestone
  El mecanismo exacto: la fila nace incompleta y se completa in-place.

- fact_sessions sobre events
  El funnel de Kiosko: view_ts, add_to_cart_ts, purchase_ts, por sesion.

- Cumulative table design (Zach Wilson/DataExpert)
  Arreglo con ventana movil: el resumen de ayer + el valor de hoy, sin releer la historia completa.

- Columnas tipo LIST y funciones de arreglo de DuckDB
  list_prepend, list_slice, list_sum, slicing [1:7].

- Ventanas fijas de 7 y 30 dias
  active_days_7d / active_days_30d, contando valores > 0 en el arreglo.

=== Las siete lecciones que construyen sobre ellos ===

L2. El accumulating snapshot fact table
    Kimball, EJECUTADO: una sesion, 3 milestones, 1 fila

L3. Modelando el funnel de sesiones de Kiosko
    fact_sessions completo, EJECUTADO: 17 sesiones

L4. Actualizando milestones in-place
    32 eventos -> 17 INSERT + 15 UPDATE, EJECUTADO

L5. Cumulative table design: el patron de Zach Wilson
    El patron ayer+hoy, EJECUTADO en 3 dias

L6. Ventanas moviles con columnas tipo arreglo
    fact_store_activity completo, EJECUTADO: 21 filas

L7. Calculando activos de 7 y 30 dias por tienda
    list_sum, active_days, EJECUTADO

L8. Proyecto: funnel y actividad acumulativa de Kiosko
    Las 6 lecciones anteriores integradas, EJECUTADO

Fíjate en el orden: las lecciones 2, 3 y 4 desarrollan el primer patrón de punta a punta —el concepto de Kimball, el hecho completo construido de una sola vez, y después el mismo hecho construido evento por evento para probar que el mecanismo real (INSERT + UPDATE) produce el mismo resultado que el cálculo agregado—. Las lecciones 5, 6 y 7 hacen el mismo recorrido con el segundo patrón —el concepto de Zach Wilson, el arreglo construido día a día, y después el análisis de esos arreglos para responder la pregunta de negocio real—. La lección 8 integra ambos hechos en un solo proyecto.

Diagrama: dónde estabas, dónde vas a estar

flowchart LR
    subgraph M5["Modulo 5 (ya escrito)"]
        A["fact_orders + dim_product_scd\nJoin punto-en-el-tiempo\nverificado"]
    end

    subgraph M6["Este modulo (6 de 8)"]
        B["L2-L4: accumulating snapshot\nfact_sessions, EJECUTADO"]
        C["L5-L7: cumulative table design\nfact_store_activity, EJECUTADO"]
        D["L8: Proyecto integrado\nEJECUTADO"]
    end

    subgraph M7["Modulo 7 (siguiente)"]
        E["Dimension junk,\nmas de un hecho conviviendo"]
    end

    A --> B --> C --> D --> E

El mapa de este módulo

Leccion   Que construye
────────  ──────────────────────────────────────────────────────────────
L1        (esta) El mapa: los seis conceptos, antes de construirlos
L2        El accumulating snapshot fact table (Kimball), EJECUTADO
L3        Modelando el funnel de sesiones de Kiosko, EJECUTADO
L4        Actualizando milestones in-place, EJECUTADO
L5        Cumulative table design (Zach Wilson), EJECUTADO
L6        Ventanas moviles con columnas tipo arreglo, EJECUTADO
L7        Activos de 7 y 30 dias por tienda, EJECUTADO
L8        Proyecto: funnel y actividad acumulativa de Kiosko, EJECUTADO

Profundización: por qué estos dos patrones necesitan datos que este módulo no inventa

Este módulo, a propósito, no genera ningún dato nuevo. fact_sessions se construye sobre los mismos 32 events que dbt-analytics-engineering-guide ya declaró como source canónico —los mismos event_id, session_id, event_type, event_ts, sin ninguna diferencia—, porque el objetivo de este módulo es enseñar el patrón de modelado, no inventar un dataset de clickstream distinto para cada guía hermana que lo toca. La única pieza que este módulo sí declara —porque events nunca trae store_id— es un mapeo fijo de sesión a tienda, que la lección 3 declara explícitamente antes de usarlo.

fact_store_activity se construye sobre fact_orders, la misma tabla de 40 filas y 106.15 de revenue total que conoces desde el módulo 1. No hay ningún dato nuevo que generar para ese hecho tampoco —solo una forma nueva de resumirlo, agrupando por tienda y día, y llevando ese resumen hacia adelante en un arreglo en vez de recalcularlo desde cero en cada consulta. Esta decisión —reusar datos ya conocidos en vez de inventar un dataset nuevo— es deliberada: te deja comparar, número por número, el revenue semanal por tienda que ya calculaste en el módulo 1 (S01: 38.3, S02: 38.8, S03: 29.05) contra la suma del arreglo de 7 días que este módulo construye — y si esos números no coinciden exactamente, algo en el patrón nuevo está mal, no en el dato.

Errores comunes

Pensar que fact_sessions y fact_store_activity reemplazan a fact_orders. Qué pasa: alguien, al ver dos tablas de hechos nuevas en un solo módulo, asume que el warehouse de Kiosko va a tener una sola tabla de hechos "definitiva" a partir de aquí, y que las otras quedan obsoletas. Por qué pasa: los módulos anteriores giraron tanto alrededor de fact_orders que es fácil asumir que cualquier hecho nuevo la reemplaza. Cómo detectarlo: si esperas que fact_orders desaparezca o deje de actualizarse después de este módulo, tienes esta confusión. Cómo corregirlo: un warehouse dimensional real casi siempre tiene varias tablas de hechos, cada una describiendo un proceso de negocio distinto, a menudo con distinto grano y distinto tipo (transaccional, accumulating snapshot, cumulative). fact_orders sigue existiendo, sin cambios, describiendo ventas; fact_sessions describe sesiones de navegación; fact_store_activity describe actividad diaria por tienda. Las tres conviven — el módulo 7 nombra esto explícitamente como "un dominio con más de un hecho".

Confundir accumulating snapshot con "una tabla que se actualiza mucho". Qué pasa: alguien generaliza el patrón a "cualquier tabla donde hago UPDATE en vez de INSERT", incluyendo, por ejemplo, dim_product_scd del módulo 4 (que también usa MERGE/UPDATE). Por qué pasa: ambos patrones usan UPDATE en algún momento, y es tentador agruparlos por esa similitud superficial. Cómo detectarlo: si no puedes explicar la diferencia entre "actualizar una dimensión para cerrar una versión vieja y abrir una nueva" (SCD-2) y "actualizar una fila de hecho para rellenar un milestone que faltaba" (accumulating snapshot), te falta esta distinción. Cómo corregirlo: SCD-2 nunca sobrescribe una fila existente — cierra la vieja (valid_to, is_current = false) e inserta una fila nueva. El accumulating snapshot sí sobrescribe columnas de la misma fila, sin cerrar nada ni insertar nada nuevo. Son mecanismos casi opuestos que, por coincidencia, ambos usan la palabra UPDATE en algún punto.

Esperar que fact_store_activity necesite 30 días de datos reales para funcionar. Qué pasa: alguien, al ver que Kiosko solo tiene una semana de órdenes, asume que el patrón de ventana de 30 días "no se puede demostrar" con estos datos. Por qué pasa: 30 es un número mayor a 7, los días de datos disponibles, así que parece que falta información. Cómo detectarlo: si esperas que la lección 6 o 7 fallen o simulen datos falsos para llegar a 30 días, vas a sorprenderte cuando el arreglo de 30 días simplemente tenga, honestamente, los mismos 7 valores que el de 7 días — porque eso es exactamente lo que hay. Cómo corregirlo: el patrón de ventana móvil no necesita que la ventana esté "llena" para funcionar — un negocio que abrió hace 3 días tiene, correctamente, un arreglo de 30 días con 3 elementos, no un error. Este módulo declara ese comportamiento de forma explícita en vez de esconderlo.

Ejercicios

Ejercicio 1 — Nombra, de memoria, la diferencia mecánica entre un accumulating snapshot y una tabla transaccional como fact_orders. Sin releer la introducción, escribe 2-3 frases explicando qué hace que fact_orders sea "transaccional" y qué va a hacer que fact_sessions no lo sea.

Ver solución

fact_orders es transaccional porque cada fila se inserta completa, una sola vez, en el momento exacto en que ocurre la venta — ninguna fila de fact_orders se modifica después de insertada, ni falta ninguna columna por rellenar. fact_sessions, en cambio, va a nacer incompleta: la fila de una sesión se inserta en el momento del primer evento (page_view), con add_to_cart_ts y purchase_ts todavía vacíos, y esas columnas se van a rellenar con UPDATE a medida que la sesión avanza — la misma fila, actualizada varias veces, nunca una fila nueva por evento adicional.

Ejercicio 2 — Explica con tus propias palabras por qué el cumulative table design evita releer toda la historia. Usando la analogía del resumen del almacén, describe en 2-3 frases qué información necesita la fila de "hoy" de fact_store_activity para construirse, y qué información no necesita.

Ver solución

La fila de hoy solo necesita dos cosas: la fila de ayer (que ya trae el arreglo de los últimos días, calculado previamente) y el dato de hoy (el revenue del día). No necesita releer fact_orders completo ni recalcular la suma de ningún día anterior a hoy — esa suma ya vive, calculada, dentro del arreglo que trajo la fila de ayer. Es exactamente la diferencia entre sumar de nuevo siete recibos cada día, y simplemente agregarle el recibo de hoy a un resumen que ya tenía los seis anteriores.

Ejercicio 3 — Predice cuántas filas va a tener fact_sessions al final del módulo, y por qué ese número no es 32. Sabes que events tiene 32 filas (17 page_view, 9 add_to_cart, 6 purchase) y que cada sesión empieza con un page_view. Sin mirar la lección 3, predice cuántas filas tendrá fact_sessions y explica el razonamiento.

Ver solución

17 filas — una por sesión distinta, no una por evento. fact_sessions tiene el grano de "una sesión completa", así que sus filas se cuentan por session_id distinto, no por evento: los 32 eventos se distribuyen entre 17 sesiones (algunas con 1 evento, otras con hasta 3), y el patrón accumulating snapshot garantiza que cada sesión, sin importar cuántos eventos tenga, produce exactamente una fila. Esta es la diferencia central entre el patrón de este módulo y una tabla transaccional: en una tabla transaccional, más eventos significa más filas; en un accumulating snapshot, más eventos de la misma sesión significa más UPDATE sobre la misma fila.

Resumen y siguiente paso

Este módulo introduce dos tipos de tabla de hechos que no viste en los cinco módulos anteriores: el accumulating snapshot fact table de Kimball —una fila por proceso, que nace incompleta y se completa con UPDATE a medida que el proceso avanza, sin insertar filas nuevas— y el cumulative table design de Zach Wilson/DataExpert —columnas tipo arreglo que se construyen día a día, tomando el resumen de ayer y agregándole solo el dato de hoy—. Ambos se construyen sobre datos que ya conoces: fact_sessions sobre los events canónicos de Kiosko, fact_store_activity sobre el revenue de fact_orders.

Antes de avanzar deberías poder: nombrar los seis conceptos centrales de este módulo; explicar la diferencia mecánica entre una tabla transaccional, un accumulating snapshot y una dimensión historizada con SCD-2; y predecir por qué fact_sessions va a tener 17 filas, no 32.

La lección 2 empieza por el primero de los dos patrones: qué es, formalmente, un accumulating snapshot fact table según Kimball, con un primer ejemplo pequeño y ejecutado —una sola sesión, sellada tres veces, sin multiplicarse nunca.

Recursos