Módulo 6: Accumulating And Cumulative Patterns
El accumulating snapshot fact table
Descripción
Kimball describe tres tipos de tabla de hechos: transaccional (una fila por evento discreto, fact_orders es este tipo), periodic snapshot (una fila por período de tiempo, por ejemplo el saldo de una cuenta al cierre de cada mes — esta guía no la construye), y accumulating snapshot — el tipo que esta lección introduce. Un accumulating snapshot describe un proceso con principio y fin, compuesto de pasos predecibles (milestones), donde cada fila resume el proceso completo y se actualiza a medida que avanza. Esta lección construye el ejemplo más pequeño posible —una sola sesión de Kiosko, sellada tres veces— antes de que la lección 3 lo escale a las diecisiete sesiones completas.
Conexión con el módulo. Esta lección desarrolla el primer concepto del mapa de la lección 1: qué es, formalmente, un accumulating snapshot fact table, y por qué es un tipo de hecho distinto a fact_orders. La lección 3 escala este mismo mecanismo a todas las sesiones de Kiosko; la lección 4 lo reconstruye evento por evento, en el orden cronológico real en que llegarían en producción.
Una analogía: la guía de envío que se sella, nunca se reimprime
Retoma la guía de envío de la introducción del módulo. El paquete sale del centro de distribución: se sella "despachado", con la fecha y hora exactas. Dos días después llega a la ciudad destino: se sella "en tránsito local", en la misma guía, no en una nueva. El día que se entrega: se sella "entregado". Si alguien consulta la guía a mitad de camino —después del primer sello, antes del segundo— ve una guía con una casilla llena y dos vacías. Eso no es un error ni una fila incompleta que haya que descartar: es, exactamente, el estado real del proceso en ese momento. La guía nunca se duplica, nunca se reimprime — se sella, in-place, tantas veces como pasos tenga el proceso.
fact_sessions, la tabla que este módulo construye, funciona igual: cada sesión de navegación en Kiosko es un "paquete" con hasta tres sellos posibles —vio una página (view_ts), agregó algo al carrito (add_to_cart_ts), compró (purchase_ts)—. La fila nace con el primer sello. Los siguientes sellos, si ocurren, se aplican a la misma fila, nunca a una fila nueva.
Ejemplo trabajado: una sesión, sellada tres veces
Los datos de origen son los tres primeros eventos de SESS-01, exactamente como dbt-analytics-engineering-guide los declaró como source canónico de Kiosko —mismos event_id, session_id, event_type, event_ts, sin ninguna diferencia—:
E5001 page_view SESS-01 2026-08-03T08:00:12
E5002 add_to_cart SESS-01 2026-08-03T08:02:45
E5003 purchase SESS-01 2026-08-03T08:03:10
SESS-01 corresponde a la tienda S01 — el mapeo completo de sesión a tienda, que necesitas antes de construir fact_sessions para las diecisiete sesiones, lo declara la lección 3; por ahora, para este ejemplo de una sola sesión, basta con saber que SESS-01 -> S01.
# accumulating_snapshot_demo.py
import duckdb
con = duckdb.connect()
con.execute("""
CREATE TABLE fact_sessions (
session_id VARCHAR PRIMARY KEY,
store_id VARCHAR,
session_date DATE,
view_ts TIMESTAMP,
add_to_cart_ts TIMESTAMP,
purchase_ts TIMESTAMP,
is_converted BOOLEAN
)
""")
# Milestone 1: E5001 page_view -- la sesion empieza, INSERT de una fila incompleta
con.execute("""
INSERT INTO fact_sessions VALUES
('SESS-01', 'S01', DATE '2026-08-03', TIMESTAMP '2026-08-03 08:00:12', NULL, NULL, false)
""")
print("=== Despues del milestone 1 (page_view) -- INSERT ===")
con.sql("SELECT * FROM fact_sessions").show(max_width=300)
print("filas en fact_sessions:", con.sql("SELECT COUNT(*) FROM fact_sessions").fetchone()[0])
# Milestone 2: E5002 add_to_cart -- la sesion avanza, UPDATE de la MISMA fila
con.execute("""
UPDATE fact_sessions
SET add_to_cart_ts = TIMESTAMP '2026-08-03 08:02:45'
WHERE session_id = 'SESS-01'
""")
print("\n=== Despues del milestone 2 (add_to_cart) -- UPDATE, no INSERT ===")
con.sql("SELECT * FROM fact_sessions").show(max_width=300)
print("filas en fact_sessions:", con.sql("SELECT COUNT(*) FROM fact_sessions").fetchone()[0])
# Milestone 3: E5003 purchase -- la sesion se convierte, UPDATE otra vez
con.execute("""
UPDATE fact_sessions
SET purchase_ts = TIMESTAMP '2026-08-03 08:03:10', is_converted = true
WHERE session_id = 'SESS-01'
""")
print("\n=== Despues del milestone 3 (purchase) -- UPDATE, is_converted -> true ===")
con.sql("SELECT * FROM fact_sessions").show(max_width=300)
print("filas en fact_sessions:", con.sql("SELECT COUNT(*) FROM fact_sessions").fetchone()[0])
Qué esperar. Al correr python3 accumulating_snapshot_demo.py, la salida es exactamente esta:
=== Despues del milestone 1 (page_view) -- INSERT ===
┌────────────┬──────────┬──────────────┬─────────────────────┬────────────────┬─────────────┬──────────────┐
│ session_id │ store_id │ session_date │ view_ts │ add_to_cart_ts │ purchase_ts │ is_converted │
│ varchar │ varchar │ date │ timestamp │ timestamp │ timestamp │ boolean │
├────────────┼──────────┼──────────────┼─────────────────────┼────────────────┼─────────────┼──────────────┤
│ SESS-01 │ S01 │ 2026-08-03 │ 2026-08-03 08:00:12 │ NULL │ NULL │ false │
└────────────┴──────────┴──────────────┴─────────────────────┴────────────────┴─────────────┴──────────────┘
filas en fact_sessions: 1
=== Despues del milestone 2 (add_to_cart) -- UPDATE, no INSERT ===
┌────────────┬──────────┬──────────────┬─────────────────────┬─────────────────────┬─────────────┬──────────────┐
│ session_id │ store_id │ session_date │ view_ts │ add_to_cart_ts │ purchase_ts │ is_converted │
│ varchar │ varchar │ date │ timestamp │ timestamp │ timestamp │ boolean │
├────────────┼──────────┼──────────────┼─────────────────────┼─────────────────────┼─────────────┼──────────────┤
│ SESS-01 │ S01 │ 2026-08-03 │ 2026-08-03 08:00:12 │ 2026-08-03 08:02:45 │ NULL │ false │
└────────────┴──────────┴──────────────┴─────────────────────┴─────────────────────┴─────────────┴──────────────┘
filas en fact_sessions: 1
=== Despues del milestone 3 (purchase) -- UPDATE, is_converted -> true ===
┌────────────┬──────────┬──────────────┬─────────────────────┬─────────────────────┬─────────────────────┬──────────────┐
│ session_id │ store_id │ session_date │ view_ts │ add_to_cart_ts │ purchase_ts │ is_converted │
│ varchar │ varchar │ date │ timestamp │ timestamp │ timestamp │ boolean │
├────────────┼──────────┼──────────────┼─────────────────────┼─────────────────────┼─────────────────────┼──────────────┤
│ SESS-01 │ S01 │ 2026-08-03 │ 2026-08-03 08:00:12 │ 2026-08-03 08:02:45 │ 2026-08-03 08:03:10 │ true │
└────────────┴──────────┴──────────────┴─────────────────────┴─────────────────────┴─────────────────────┴──────────────┘
filas en fact_sessions: 1
Detente en el número que se repite tres veces: filas en fact_sessions: 1. Tres eventos distintos, tres momentos distintos en el tiempo, y la tabla nunca tuvo más de una fila para SESS-01. Eso es el accumulating snapshot en su forma más pura — el mismo session_id, la misma fila física, cada vez con más columnas rellenas, hasta que el proceso (la sesión) llega a su fin natural (compra, o simplemente el usuario deja de interactuar).
Diagrama: la fila que se sella tres veces
Momento 1 (08:00:12) Momento 2 (08:02:45) Momento 3 (08:03:10)
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ SESS-01 | S01 │ │ SESS-01 | S01 │ │ SESS-01 | S01 │
│ view: 08:00:12 │ ---> │ view: 08:00:12 │ ---> │ view: 08:00:12 │
│ cart: NULL │ UPDATE │ cart: 08:02:45 │ UPDATE │ cart: 08:02:45 │
│ purchase: NULL │ │ purchase: NULL │ │ purchase: 08:03:10│
│ converted: false │ │ converted: false │ │ converted: true │
└─────────────────┘ └─────────────────┘ └─────────────────┘
INSERT la MISMA fila,
(1 fila nueva) actualizada 2 veces
(0 filas nuevas)
Profundización: por qué el orden de los milestones importa, y qué pasa si uno se salta
El accumulating snapshot de Kimball asume que los milestones ocurren en un orden predecible — en el funnel de Kiosko, nadie compra (purchase) sin haber visto una página (view) primero, y agregar al carrito (add_to_cart) siempre ocurre entre esos dos. Esta suposición no es una regla universal de SQL, es una regla de negocio: el modelo la asume porque así funciona, de hecho, la app de delivery de Kiosko. Si el proceso de negocio real permitiera saltarse un paso —por ejemplo, una compra directa sin pasar por el carrito—, el modelo seguiría funcionando exactamente igual: la fila nacería con view_ts lleno, add_to_cart_ts se quedaría NULL para siempre, y purchase_ts se llenaría de todas formas. Nada en el UPDATE exige que las columnas se llenen en un orden fijo — cada milestone es una columna independiente que se llena cuando su evento correspondiente llega, sin importar qué otras columnas ya estén llenas o vacías.
Esto tiene una consecuencia práctica importante para la lección 3 y 4: una fila de fact_sessions con add_to_cart_ts lleno y purchase_ts vacío (como vas a ver en varias sesiones de Kiosko) no es una fila "rota" ni "incompleta en el sentido de faltarle datos que deberían existir" — es una fila que describe, con exactitud, una sesión que llegó hasta el carrito y ahí se detuvo. Un NULL en fact_sessions tiene un significado de negocio preciso: "este milestone todavía no ocurrió", nunca "faltó capturar este dato".
Errores comunes
Esperar que un accumulating snapshot tenga una columna "estado actual" en vez de columnas de fecha por milestone. Qué pasa: alguien, familiarizado con otros sistemas, espera que fact_sessions tenga una sola columna status ('viewed', 'added_to_cart', 'purchased') en vez de tres columnas de timestamp independientes. Por qué pasa: una columna de estado único es un patrón común en sistemas transaccionales (una orden "está" en un estado a la vez). Cómo detectarlo: si intentas escribir fact_sessions con una sola columna de estado, vas a perder información — no podrías responder "¿cuánto tiempo pasó entre que vio la página y agregó al carrito?" sin guardar ambos timestamps por separado. Cómo corregirlo: el patrón de Kimball guarda una columna de fecha/hora por milestone, no un estado único — eso es lo que permite calcular duraciones entre pasos y saber exactamente cuándo ocurrió cada uno, no solo cuál fue el último.
Pensar que is_converted se puede calcular sin volver a mirar purchase_ts. Qué pasa: alguien trata is_converted como una columna independiente que hay que mantener sincronizada manualmente, en vez de una que se deriva directamente de si purchase_ts está lleno o no. Por qué pasa: tener dos columnas —una de fecha, una booleana— que "dicen lo mismo" se siente redundante, y es fácil actualizar una sin la otra. Cómo detectarlo: si en algún punto de tu código actualizas purchase_ts sin actualizar is_converted en la misma sentencia, tu tabla puede terminar con una sesión que tiene purchase_ts lleno pero is_converted = false — una contradicción interna. Cómo corregirlo: en el ejemplo de esta lección, el mismo UPDATE que llena purchase_ts también pone is_converted = true, en una sola sentencia — nunca en dos pasos separados que puedan desincronizarse.
Asumir que el accumulating snapshot necesita saber, de antemano, cuántos milestones va a tener el proceso. Qué pasa: alguien piensa que este patrón solo funciona si el número de pasos es fijo y conocido para todos los casos (como los tres milestones del funnel de Kiosko), y no sabe cómo aplicarlo a un proceso con un número variable de pasos. Por qué pasa: el ejemplo de esta lección tiene exactamente tres milestones, siempre los mismos tres, lo que puede sugerir que el patrón depende de esa regularidad. Cómo detectarlo: si te preguntas "¿qué pasaría si un proceso tuviera a veces 3 pasos y a veces 5?", es una señal de que estás generalizando de más a partir de un solo ejemplo. Cómo corregirlo: el patrón sigue funcionando con un número variable de milestones — Kimball documenta ejemplos con hasta una docena de columnas de fecha (por ejemplo, el ciclo de vida completo de un pedido: ordenado, pagado, empacado, despachado, en tránsito, entregado). Lo único que cambia es cuántas columnas tiene la fila, no el mecanismo de INSERT una vez y UPDATE por cada milestone que sí ocurre.
Ejercicios
Ejercicio 1 — Repite el ejemplo con una sesión que no compra. Usando SESS-02 (page_view a las 2026-08-03T08:05:00, sin ningún otro evento en events), escribe el INSERT correspondiente y confirma qué valores quedan en las columnas de milestone que nunca ocurrieron.
Ver solución
con.execute("""
INSERT INTO fact_sessions VALUES
('SESS-02', 'S02', DATE '2026-08-03', TIMESTAMP '2026-08-03 08:05:00', NULL, NULL, false)
""")
con.sql("SELECT * FROM fact_sessions WHERE session_id = 'SESS-02'").show(max_width=300)
Salida esperada:
┌────────────┬──────────┬──────────────┬─────────────────────┬────────────────┬─────────────┬──────────────┐
│ session_id │ store_id │ session_date │ view_ts │ add_to_cart_ts │ purchase_ts │ is_converted │
│ varchar │ varchar │ date │ timestamp │ timestamp │ timestamp │ boolean │
├────────────┼──────────┼──────────────┼─────────────────────┼────────────────┼─────────────┼──────────────┤
│ SESS-02 │ S02 │ 2026-08-03 │ 2026-08-03 08:05:00 │ NULL │ NULL │ false │
└────────────┴──────────┴──────────────┴─────────────────────┴────────────────┴─────────────┴──────────────┘
add_to_cart_ts y purchase_ts quedan NULL para siempre — no porque falten datos, sino porque esos milestones, con la evidencia de events disponible, nunca ocurrieron para SESS-02. Esta fila es tan completa y correcta como la de SESS-01; describe, con exactitud, una sesión que se quedó en la primera etapa del funnel.
Ejercicio 2 — Verifica que dos UPDATE seguidos sobre la misma sesión nunca crean una fila duplicada. Corre el UPDATE del milestone 2 (add_to_cart_ts) sobre SESS-01 dos veces seguidas, con el mismo valor, y confirma con COUNT(*) que la tabla sigue teniendo exactamente una fila para esa sesión.
Ver solución
con.execute("UPDATE fact_sessions SET add_to_cart_ts = TIMESTAMP '2026-08-03 08:02:45' WHERE session_id = 'SESS-01'")
con.execute("UPDATE fact_sessions SET add_to_cart_ts = TIMESTAMP '2026-08-03 08:02:45' WHERE session_id = 'SESS-01'")
print(con.sql("SELECT COUNT(*) AS filas_para_sess_01 FROM fact_sessions WHERE session_id = 'SESS-01'"))
Salida esperada:
┌────────────────────┐
│ filas_para_sess_01 │
│ int64 │
├────────────────────┤
│ 1 │
└────────────────────┘
Un UPDATE repetido con el mismo valor no cambia nada — sigue siendo una sola fila, con el mismo contenido. Esta es una propiedad importante del patrón: es idempotente respecto al número de filas, sin importar cuántas veces llegue el mismo evento (algo que vas a relacionar directamente con la deduplicación del módulo 5 si un mismo evento se reenvía por error).
Ejercicio 3 — Explica por qué un INSERT duplicado sí sería un problema, aunque un UPDATE duplicado no lo sea. En 2-3 frases, explica qué pasaría si el mecanismo de esta lección usara INSERT en cada milestone en vez de INSERT solo en el primero y UPDATE en los siguientes.
Ver solución
Si cada milestone insertara una fila nueva en vez de actualizar la existente, fact_sessions terminaría con hasta tres filas por sesión completa —una por cada evento— en vez de una sola fila acumulada. Eso rompería el grano del hecho: en vez de "una fila por sesión", tendrías "una fila por evento de sesión", que es exactamente el grano de la tabla events original, no un accumulating snapshot. El propósito entero del patrón —resumir un proceso completo en una sola fila consultable— desaparecería, y cualquier consulta que cuente sesiones (COUNT(*)) contaría eventos en su lugar, infladas por sesiones con más de un milestone.
Resumen y siguiente paso
Esta lección construyó el ejemplo más pequeño posible de un accumulating snapshot fact table: una sola sesión de Kiosko (SESS-01), sellada tres veces —INSERT en el primer milestone, UPDATE en cada uno de los siguientes dos—, terminando siempre con exactamente una fila. Viste, con evidencia ejecutada, la propiedad central del patrón de Kimball: el número de eventos que ocurren no determina el número de filas de la tabla — lo determina el número de procesos distintos (sesiones), sin importar cuántos milestones alcance cada uno.
Antes de avanzar deberías poder: explicar la diferencia entre INSERT (nace el proceso) y UPDATE (avanza el proceso) en este patrón; interpretar un NULL en una columna de milestone como "todavía no ocurrió", no como un dato faltante; y predecir qué pasaría si el mecanismo usara INSERT en cada milestone en vez de UPDATE.
La lección 3 escala este mismo mecanismo a las diecisiete sesiones completas de Kiosko, usando una consulta agregada (GROUP BY + MAX(CASE WHEN ...)) que construye la tabla completa de una sola vez — el equivalente de un "recálculo completo" (full refresh), útil para entender el resultado final antes de que la lección 4 lo reconstruya evento por evento, como ocurriría de verdad en producción.
Recursos
- Kimball Group — "Accumulating Snapshot Fact Table" — la definición formal de este patrón: "A row in an accumulating snapshot fact table summarizes the measurement events occurring at predictable steps between the beginning and the end of a process", la fuente exacta de esta lección. kimballgroup.com/data-warehouse-business-intelligence-resources/kimball-techniques/dimensional-modeling-techniques/accumulating-snapshot-fact-table. En inglés.
- Kimball Group — "Star Schema / OLAP Cube" — el vocabulario general que distingue transactional, periodic snapshot y accumulating snapshot fact tables. kimballgroup.com/data-warehouse-business-intelligence-resources/kimball-techniques/dimensional-modeling-techniques/star-schema-olap-cube. En inglés.
- DuckDB — documentación oficial del cliente Python, la interfaz que ejecutó cada
INSERT/UPDATEde esta lección. duckdb.org/docs/current/clients/python/overview. En inglés.