Módulo 3: Snapshots And Time Travel

Presentación del módulo: snapshots y time travel

Por qué existe este módulo

El módulo 2 abrió la caja completa de kiosko.fact_orders sin escribir ni una fila nueva. Recorriste la cadena catálogo → metadata → manifest list → manifest files → archivos de datos, y en cada eslabón encontraste un único snapshot: el que creó table.append() en la lección 6 del módulo 1. Ese módulo terminó con una pregunta abierta a propósito, planteada en su propia lección 1: si "nada se sobrescribe nunca" es la promesa central de Iceberg, ¿qué pasa exactamente cuando la tabla cambia? ¿Adónde va el estado anterior?

Este módulo responde esa pregunta con el ejemplo que ya conoces de memoria, porque lo resolviste dos veces antes, con dos técnicas distintas. En data-modeling-for-analytics-guide construiste dim_product_scd a mano: columnas valid_from, valid_to, is_current, y un MERGE INTO escrito con tus propias manos para que la fila vieja de P002 no desapareciera cuando unit_cost cambió de 0.60 a 0.68. En dbt-analytics-engineering-guide automatizaste exactamente esa misma técnica con dbt snapshot: las mismas tres ideas —guardar la versión vieja, marcar cuándo dejó de regir, saber cuál es la vigente—, ahora con menos tecleo, generadas por dbt_valid_from/dbt_valid_to/dbt_scd_id. Las dos soluciones son correctas. Las dos, también, le piden al modelador que diseñe y mantenga columnas de historia.

Este módulo hace lo mismo —recuperar category='snacks', unit_cost=0.60 después de que la tabla ya cambió a category='health-snacks', unit_cost=0.68— sin declarar ni una sola columna nueva. kiosko.dim_product va a tener, de principio a fin, exactamente cuatro columnas: product_id, product_name, category, unit_cost. Ningún valid_from. Ningún is_current. La historia no vive en una columna — vive en el propio mecanismo de snapshots que el módulo 2 ya te mostró que existe, y que este módulo, por primera vez en esta guía, usa de verdad.

El caso que nos acompaña: el cambio de P002, otra vez

El hilo es el mismo de siempre: P002 Energy Bar cambia de category='snacks', unit_cost=0.60 a category='health-snacks', unit_cost=0.68, con fecha de vigencia 2026-08-15 —posterior a las cuarenta órdenes reales de Kiosko, todas entre el 3 y el 9 de agosto—. El resultado correcto para cualquier consulta sobre esas cuarenta órdenes sigue siendo category='snacks', con un margen de 10.8 para esa categoría; el resultado roto —el que aplica el costo nuevo a ventas que ya ocurrieron— sigue dando health-snacks, margen 9.36. Son los mismos dos números que ya viste en data-modeling (módulo 5) y en dbt (módulo 5).

Lo que cambia aquí es la tabla. Este módulo crea kiosko.dim_product desde cero —no existía antes de este módulo—, con una fila por producto y ninguna columna de historia. La carga primero con los valores V1 (P002 todavía snacks/0.60), captura el snapshot_id de ese momento en una variable, y después la sobrescribe por completo con los valores V2 (P002 ya health-snacks/0.68). El estado vigente de la tabla, desde ese momento, es V2 — igual que en la vida real, donde el catálogo de productos de una tienda siempre refleja el precio y la categoría de hoy, nunca un histórico mezclado. La recuperación de V1 no viene de una columna que la tabla conserva a propósito — viene de pedirle a Iceberg, con precisión, "muéstrame cómo se veía esta tabla en el snapshot anterior al cambio".

Una analogía: la foto del estante de supermercado

Piensa en el encargado de reponer un estante de supermercado. Cada vez que reabastece —cada vez que cambia lo que hay ahí—, toma una foto del estante completo, tal como quedó, y la archiva con la fecha. Nunca retoca la foto de ayer. Nunca borra una foto vieja para "corregirla" con la mercadería de hoy. Si le preguntas "¿qué había en el estante el martes pasado?", no te da su opinión ni reconstruye de memoria — va al archivo, busca la foto del martes, y te la muestra tal cual quedó ese día.

Eso es, con precisión, lo que es un snapshot de Iceberg, y lo que es el time travel. Cada escritura —cada append(), cada overwrite()— es una foto nueva del estante completo, archivada junto a todas las anteriores, ninguna retocada. Pedir "muéstrame el estante como estaba en el snapshot X" —lo que este módulo llama, con la sintaxis exacta de PyIceberg, table.scan(snapshot_id=X)— es exactamente pedirle al encargado del archivo "sácame la foto de ese día". El encargado no necesita que nadie le haya pedido, de antemano, que anotara en una libreta aparte "esto es lo que cambió, y desde cuándo" —eso es lo que valid_from/valid_to hacen en data-modeling y dbt—. El archivo de fotos, por sí mismo, ya contiene toda la historia, sin que nadie haya diseñado una columna para eso.

Diagrama: dos escrituras, dos fotos, un viaje en el tiempo

flowchart LR
    T0["kiosko.dim_product\nrecien creada, vacia"] -->|"table.append(V1)\nP002 = snacks / 0.60"| S1["snapshot snap_v1\nla foto del estante ANTES"]
    S1 -->|"table.overwrite(V2)\nP002 = health-snacks / 0.68"| S2["snapshot snap_v2\nla foto del estante AHORA\n(= current_snapshot)"]

    S2 -.->|"table.scan()\nsin argumentos"| R2["lee snap_v2\nP002 = health-snacks / 0.68"]
    S1 -.->|"table.scan(snapshot_id=snap_v1)\nTIME TRAVEL"| R1["lee snap_v1\nP002 = snacks / 0.60"]

snap_v1 no desaparece cuando se crea snap_v2 — sigue archivado, disponible, exactamente como la foto vieja del estante sigue en el archivo del supermercado. Lo único que cambia es cuál de las dos fotos te muestra el mostrador por defecto cuando no le pides una fecha específica.

El mapa de este módulo

Leccion   Que resuelve
────────  ──────────────────────────────────────────────────────────────
L1        (esta) El mapa completo: dos escrituras, dos snapshots, un viaje
L2        La primera escritura real de esta guia sobre una segunda tabla:
          kiosko.dim_product, cargada con V1 -- un snapshot nuevo
L3        La segunda escritura: table.overwrite() con V2 -- el cambio de P002
L4        Por que el snapshot-id nunca se hardcodea, y como capturarlo bien
L5        table.scan(snapshot_id=snap_v1) -- el viaje en el tiempo, ejecutado
L6        El pago: el margen correcto de P002 (10.8), sin ninguna columna
L7        El limite honesto: que NO resuelve el time travel
L8        Proyecto: dim_product, historizado por time travel, de punta a punta

Las lecciones 2 y 3 crean las dos escrituras que hacen posible todo lo demás. La lección 4 se detiene, a propósito, en una regla dura de esta guía completa: un snapshot_id no es un número que puedas anotar y reutilizar de una corrida a otra — hay que capturarlo en código, siempre. Las lecciones 5 y 6 son el viaje en el tiempo en sí, y la verificación de que recupera el mismo resultado correcto que ya conoces de dos guías anteriores. La lección 7 es, a propósito, la más importante de las ocho: declara con toda claridad qué no resuelve esta técnica, para que no salgas de este módulo pensando que el time travel reemplaza, en general, a un SCD-2 diseñado a nivel de fila. La lección 8 junta las siete anteriores en un solo proyecto.

La frontera: qué NO entra en este módulo

Este módulo no toca el esquema de ninguna tabla —agregar, renombrar o borrar una columna es, con precisión, el trabajo del módulo 4—. Tampoco toca partición ni volumen —kiosko.fact_orders_at_scale, con sus 10 millones de filas, llega recién en el módulo 5—. Y, la frontera más importante de las tres: este módulo no resuelve el caso general de una dimensión que cambia muchas veces, con hechos repartidos entre varias de esas versiones. Ese caso general sigue necesitando SCD-2 a nivel de fila —a mano, como en data-modeling, o automatizado, como en dbt—. La lección 7 de este módulo lo declara así de explícito, con un ejemplo ejecutado que muestra, con evidencia real, dónde está ese límite.

Errores comunes

Esperar que este módulo agregue columnas de historia "por si acaso". Qué pasa: alguien, familiarizado con valid_from/valid_to de data-modeling o dbt_valid_from/dbt_valid_to de dbt, espera que kiosko.dim_product de este módulo también las tenga, "para estar seguro". Por qué pasa: dos guías anteriores del ecosistema resolvieron este mismo problema exactamente así, y es natural asumir que la tercera hace lo mismo. Cómo detectarlo: si tu propio esquema de kiosko.dim_product tiene más de cuatro columnas, revisa el paso 2 de la lección 2 de este módulo. Cómo corregirlo: el punto entero de este módulo es que esas columnas no son necesarias — la tabla tiene, y debe seguir teniendo, exactamente cuatro columnas: product_id, product_name, category, unit_cost.

Confundir "el time travel resuelve la historia" con "el time travel resuelve TODA historia posible". Qué pasa: alguien termina la lección 6 de este módulo, ve el margen correcto (10.8) recuperado sin ninguna columna, y concluye que Iceberg hace obsoleto todo lo que data-modeling y dbt enseñaron sobre SCD-2. Por qué pasa: el ejemplo de Kiosko, con un único cambio de P002 y todos los hechos ocurridos antes de ese cambio, es el caso más favorable posible para el time travel — y es fácil generalizar de un caso favorable a "siempre funciona así". Cómo detectarlo: si no puedes explicar, con un ejemplo concreto, un escenario donde el time travel no dé la respuesta correcta, todavía no llegaste a la lección 7. Cómo corregirlo: lee la lección 7 completa antes de dar por cerrado este módulo — es, a propósito, la lección con el punto pedagógico más importante de las ocho.

Ejercicios

Ejercicio 1 — Antes de empezar, predice: ¿cuántos snapshots va a tener kiosko.dim_product al final de la lección 3? Sin haber leído todavía las lecciones 2 y 3, y basándote solo en la analogía de esta lección (una foto por escritura), predice cuántos snapshots vas a ver en kiosko.dim_product después de cargar V1 y sobrescribir con V2 — dos operaciones de escritura, tal como las describe el mapa de esta lección.

Ver solución

La predicción más razonable, con la información de esta lección, es dos: uno por cada operación de escritura (append(V1) y overwrite(V2)). Vale la pena adelantar que la lección 3 de este módulo va a mostrar que la respuesta real es más matizada —table.overwrite() puede, por dentro, generar más de un snapshot en una sola llamada—, pero la intuición de "una escritura, un snapshot como mínimo" que esta lección enseña es correcta y es la base sobre la que la lección 3 construye el matiz completo.

Ejercicio 2 — Explica, sin mirar atrás, la diferencia entre el estado "vigente" y el estado "recuperable". En 2-3 frases, explica qué significa que snap_v1 siga siendo recuperable después de que snap_v2 se convierte en el snapshot vigente — y por qué esto es distinto de "haber perdido" el estado anterior.

Ver solución

"Vigente" es lo que table.scan() te muestra por defecto, sin que le pidas nada más específico —es la foto más reciente del archivo, la que el catálogo apunta ahora mismo—. "Recuperable" significa que la foto anterior sigue físicamente archivada, disponible para cualquiera que la pida explícitamente con table.scan(snapshot_id=snap_v1) — no se perdió, ni se movió a un lugar distinto, ni requiere ningún proceso especial de restauración. La diferencia clave es que Iceberg nunca tuvo que "guardar una copia de seguridad" de snap_v1 antes de escribir snap_v2snap_v1 nunca se tocó en absoluto; la escritura de snap_v2 simplemente archivó una foto nueva al lado de la vieja, sin sobrescribir nada.

Ejercicio 3 — Nombra, de memoria, las dos técnicas anteriores que este módulo reemplaza para el caso favorable de Kiosko. Sin mirar atrás, nombra las dos guías del ecosistema que ya resolvieron el cambio de P002, y la técnica exacta que usó cada una.

Ver solución

data-modeling-for-analytics-guide (módulo 4-5) resolvió el cambio de P002 a mano, con columnas valid_from/valid_to/is_current en dim_product_scd, mantenidas con un MERGE INTO escrito explícitamente en SQL. dbt-analytics-engineering-guide (módulo 5) automatizó la misma idea con dbt snapshot, generando dbt_valid_from/dbt_valid_to/dbt_scd_id sin que nadie escribiera el MERGE INTO a mano. Las dos técnicas dependen de columnas de historia diseñadas por una persona; este módulo llega al mismo resultado correcto sin declarar ninguna.

Resumen y siguiente paso

En esta lección conociste el mapa completo del módulo 3: dos escrituras reales sobre una tabla nueva, kiosko.dim_product, sin ninguna columna de historia, y un viaje en el tiempo que recupera el estado anterior al cambio de P002. Viste la analogía de la foto del estante, el diagrama de las dos escrituras, y la frontera explícita de lo que este módulo no resuelve —el caso general de una dimensión con muchos cambios, que sigue necesitando SCD-2 a nivel de fila—.

Antes de avanzar deberías poder: explicar, en tus propias palabras, qué es un snapshot y qué es time travel, usando la analogía de esta lección; y nombrar las dos técnicas anteriores del ecosistema que este módulo reproduce con una tabla más simple.

La lección 2 crea kiosko.dim_product y le da su primera escritura real: los valores V1 de los cuatro productos de Kiosko, con P002 todavía como snacks/0.60.

Recursos

  • Apache Iceberg — documentación oficial, "Table Spec", la definición formal de snapshot como el estado completo de una tabla en un instante dado. iceberg.apache.org/spec. En inglés.
  • PyIceberg — referencia de API, table.scan(snapshot_id=...) y table.overwrite(), el par de operaciones centrales de este módulo. py.iceberg.apache.org/api. En inglés.
  • DISEÑO de data-modeling-for-analytics-guide — fuente del dim_product_scd con valid_from/valid_to/is_current y el MERGE INTO que este módulo reproduce sin columnas. src/guides/data-modeling-for-analytics-guide/DISENO.md. En español.
  • DISEÑO de dbt-analytics-engineering-guide — fuente de dbt snapshot y dbt_valid_from/dbt_valid_to/dbt_scd_id, la versión automatizada de la misma técnica. src/guides/dbt-analytics-engineering-guide/DISENO.md. En español.
  • DISEÑO de esta guía — el mapa completo de los ocho módulos, incluida la frontera exacta de este módulo. src/guides/lakehouse-and-iceberg-guide/DISENO.md. En español.