Módulo 5: Snapshots And Scd Type 2

`timestamp` contra `check`: las dos estrategias de un snapshot

Descripción

La lección 2 te mostró la pregunta booleana que un snapshot le hace al warehouse en cada corrida —¿cambió esta fila?—, pero dejó abierta una pregunta previa, más importante: ¿cómo decide dbt, exactamente, si algo cambió? dbt-core trae dos formas distintas de contestar esa pregunta, y se declaran con la clave strategy. Esta lección ejecuta ambas —de verdad, con datos reales— para que la elección de esta guía (strategy: timestamp, la que vas a configurar en la lección 4) no sea un valor copiado sin entender, sino una decisión informada por evidencia.

Conexión con el módulo. La lección 2 te dio el mecanismo general de "comparar y archivar". Esta lección responde la pregunta que ese mecanismo necesita antes de poder funcionar: comparar, ¿cómo, exactamente? La lección 4 va a declarar dim_product_snapshot con la estrategia que esta lección justifica con evidencia — no antes.

Una analogía: el guardia que mira el reloj de fichaje, contra el que revisa la cara

Vuelve al guardia de la lección 2, comparando la foto nueva contra la del legajo. Hay dos formas completamente distintas de que ese guardia decida "esta persona cambió algo relevante":

La primera es mirar un sello de fecha que la propia persona trae consigo — un carné con un campo "última actualización", que la oficina de recursos humanos actualiza cada vez que corrige algo del perfil de esa persona. El guardia no necesita comparar cara contra cara, pelo contra pelo: solo compara la fecha del carné nuevo contra la fecha que ya tenía archivada. Si es más reciente, algo cambió — sin necesidad de saber qué. Esa es la estrategia timestamp: confía en que la fuente trae, ella misma, una columna que dice cuándo fue la última vez que algo cambió, y compara solo esa fecha.

La segunda es no confiar en ningún carné, y en cambio, revisar directamente las características que le interesan al guardia: ¿el color de pelo es el mismo que la última vez? ¿La altura registrada coincide? El guardia compara, campo por campo, el legajo viejo contra la persona que tiene enfrente — sin necesitar ningún sello de fecha, porque la comparación misma es la fuente de verdad. Esa es la estrategia check: compara, columna por columna, el valor archivado contra el valor actual, y si cualquiera de las columnas que le pediste vigilar difiere, considera que la fila cambió.

Ejemplo trabajado: la misma pregunta, dos formas de contestarla

Retoma el escenario de la lección 2 — P002 ya archivado como snacks/0.60/2026-08-01, y la fuente trayendo ahora health-snacks/0.68/2026-08-15. Así es como cada estrategia contesta "¿cambió?":

-- lo que ya esta archivado
create table snapshotted as
select 'P002' as product_id, 'snacks' as category, 0.60 as unit_cost,
       date '2026-08-01' as product_updated_at, date '2026-08-01' as dbt_updated_at;

-- lo que trae la fuente en esta corrida
create table source_now as
select 'P002' as product_id, 'health-snacks' as category, 0.68 as unit_cost,
       date '2026-08-15' as product_updated_at;

-- pregunta de la estrategia TIMESTAMP: la fecha es mas reciente?
select
    s.product_id,
    src.product_updated_at as source_updated_at,
    s.dbt_updated_at        as snapshotted_updated_at,
    (src.product_updated_at > s.dbt_updated_at) as row_has_changed
from snapshotted s join source_now src on s.product_id = src.product_id;

-- pregunta de la estrategia CHECK: alguna de estas columnas es distinta?
select
    s.product_id,
    s.category   as old_category,   src.category   as new_category,
    s.unit_cost  as old_unit_cost,  src.unit_cost  as new_unit_cost,
    (src.category  is distinct from s.category
     or src.unit_cost is distinct from s.unit_cost) as row_has_changed
from snapshotted s join source_now src on s.product_id = src.product_id;

Qué esperar.

-- TIMESTAMP
┌────────────┬────────────────────┬─────────────────────────┬──────────────────┐
│ product_id │ source_updated_at  │ snapshotted_updated_at  │ row_has_changed  │
├────────────┼────────────────────┼─────────────────────────┼──────────────────┤
│ P002       │ 2026-08-15         │ 2026-08-01              │ true             │
└────────────┴────────────────────┴─────────────────────────┴──────────────────┘

-- CHECK
┌────────────┬──────────────┬───────────────┬────────────────┬────────────────┬──────────────────┐
│ product_id │ old_category │ new_category  │ old_unit_cost  │ new_unit_cost  │ row_has_changed  │
├────────────┼──────────────┼───────────────┼────────────────┼────────────────┼──────────────────┤
│ P002       │ snacks       │ health-snacks │           0.60 │           0.68 │ true             │
└────────────┴──────────────┴───────────────┴────────────────┴────────────────┴──────────────────┘

Las dos estrategias llegan a la misma conclusión —row_has_changed = true—, pero por caminos completamente distintos: timestamp nunca mira category ni unit_cost, solo la fecha; check nunca mira ninguna fecha, solo los valores de las columnas que le dijiste que vigilara. Esta coincidencia no es casualidad —Kiosko actualiza product_updated_at exactamente cuando cambia algo real—, pero es exactamente el tipo de suposición que vale la pena examinar antes de confiar en ella ciegamente, y es lo que hace el resto de esta lección.

Verificación real: strategy: check corrido de verdad, sobre un snapshot aislado

Las dos consultas de arriba son una simulación en SQL puro — útil para entender la lógica, pero no es lo mismo que ver a dbt tomar esa decisión de verdad. Un snapshot aislado, separado de dim_product_snapshot (que vas a declarar recién en la lección 4), con strategy: check sobre las mismas dos versiones de P002, corrido dos veces:

# snapshot de demostracion, NO forma parte del proyecto final de este modulo
snapshots:
  - name: dim_product_check_demo
    relation: source('demo_raw', 'products')
    config:
      unique_key: product_id
      strategy: check
      check_cols:
        - category
        - unit_cost

Qué esperar (primera corrida, sobre la versión snacks/0.60).

1 of 1 OK snapshotted main.dim_product_check_demo .............................. [OK in 0.07s]
Done. PASS=1 WARN=0 ERROR=0 SKIP=0 NO-OP=0 REUSED=0 TOTAL=1
┌────────────┬──────────┬───────────┬────────────────────────────┬──────────────┐
│ product_id │ category │ unit_cost │       dbt_valid_from       │ dbt_valid_to │
├────────────┼──────────┼───────────┼────────────────────────────┼──────────────┤
│ P002       │ snacks   │       0.6 │ 2026-08-12 18:58:14.713882 │ NULL         │
└────────────┴──────────┴───────────┴────────────────────────────┴──────────────┘

Detente aquí, porque este es el hallazgo central de la lección: dbt_valid_from no dice 2026-08-01 (la fecha real del dato) — dice 2026-08-12 18:58:14.713882, el instante exacto en que se corrió este comando en esta máquina, con hora, minutos y microsegundos incluidos. Esto no es un error — es el comportamiento documentado de strategy: check cuando no le das explícitamente una columna updated_at: sin una fecha de la fuente a la cual aferrarse, dbt usa el momento de la corrida (run_started_at internamente) para marcar cuándo abrió cada fila.

Qué esperar (segunda corrida, sobre la versión health-snacks/0.68).

1 of 1 OK snapshotted main.dim_product_check_demo .............................. [OK in 0.12s]
Done. PASS=1 WARN=0 ERROR=0 SKIP=0 NO-OP=0 REUSED=0 TOTAL=1
┌────────────┬───────────────┬───────────┬────────────────────────────┬────────────────────────────┐
│ product_id │   category    │ unit_cost │       dbt_valid_from       │        dbt_valid_to        │
├────────────┼───────────────┼───────────┼────────────────────────────┼────────────────────────────┤
│ P002       │ snacks        │       0.6 │ 2026-08-12 18:58:14.713882 │ 2026-08-12 18:58:29.053896 │
│ P002       │ health-snacks │      0.68 │ 2026-08-12 18:58:29.053896 │ NULL                       │
└────────────┴───────────────┴───────────┴────────────────────────────┴────────────────────────────┘

strategy: check sí detecta el cambio correctamente —dos filas, row_has_changed funcionó exactamente como en la simulación de arriba—, pero mira las columnas de fecha: 2026-08-12 18:58:14.713882 y 2026-08-12 18:58:29.053896 son, con precisión, el instante en que este demo específico corrió en esta máquina — quince segundos de diferencia entre una corrida y la siguiente, porque eso fue lo que tardé en escribir el comando entre medio. Si corrieras exactamente esta misma demostración en tu propia máquina, en otro momento, verías números completamente distintos.

Por qué esta guía elige timestamp, con evidencia y no por costumbre

Ahí está la razón concreta, no abstracta, de por qué dim_product_snapshot (lección 4 en adelante) usa strategy: timestamp y no strategy: check: esta guía promete, en cada lección, un bloque "Qué esperar" con salida literal, reproducible byte a byte en cualquier máquina. strategy: check sin una columna updated_at explícita rompe esa promesa de raíz —cada corrida estampa la hora real del sistema, distinta cada vez—, exactamente el tipo de función de reloj (CURRENT_TIMESTAMP implícito) que el diseño de esta guía prohíbe en cualquier bloque que alimente un "Qué esperar".

strategy: timestamp, en cambio, nunca consulta el reloj del sistema para decidir dbt_valid_from: usa, siempre, el valor de la columna que configures con updated_atproduct_updated_at, en el caso de Kiosko—. Como esa columna es un dato fijo de Kiosko (2026-08-01, 2026-08-15), el resultado es exactamente el mismo sin importar en qué máquina, ni en qué momento del día, corras dbt snapshot. Vas a confirmar esto con evidencia real en la lección 5: cuando declares dim_product_snapshot con strategy: timestamp, dbt_valid_from va a mostrar 2026-08-01 — la fecha del dato, no la fecha en que tú corriste el comando, aunque las corras semanas después de escribir esta guía.

Vale la pena decir, para que quede completo: strategy: check también acepta un updated_at opcional, y si lo configuraras, dejaría de depender del reloj del sistema igual que timestamp. Pero hacerlo sería redundante en el caso de Kiosko —si ya tienes una columna de fecha confiable como product_updated_at, usar strategy: timestamp directamente es la forma más simple de aprovecharla— y check demuestra su verdadero valor en el caso contrario: cuando la fuente no trae ninguna columna de fecha confiable, y la única forma de detectar un cambio es comparar los valores mismos, columna por columna. Kiosko no está en ese caso —por eso esta guía usa timestamp—, pero es importante que reconozcas cuándo check sería la elección correcta en un proyecto distinto.

Tabla de decisión: cuándo usar cada estrategia

Preguntatimestampcheck
¿La fuente trae una columna de fecha confiable, que cambia solo cuando algo real cambia?Sí — úsalaNo, o no confías en ella
¿Qué compara dbt para decidir "cambió"?Una sola columna de fecha (updated_at)Las columnas que declares en check_cols
¿Necesita conocer cada columna de negocio de la tabla?No — le basta una fechaSí — tiene que enumerarlas (o usar check_cols: 'all')
¿Es reproducible sin configuración extra?Sí, siempre —usa el dato, no el relojSolo si además configuras updated_at
Elección de esta guía para dim_product_snapshot (updated_at: product_updated_at)No (pero se demuestra en esta lección)

Errores comunes

Asumir que check es "más seguro" porque compara más cosas. Qué pasa: alguien, al ver que check examina columnas de negocio directamente, concluye que es la opción más rigurosa, y que timestamp es una versión "recortada" o menos confiable. Por qué pasa: comparar más columnas se siente, intuitivamente, más exhaustivo que comparar una sola fecha. Cómo detectarlo: si la fuente actualiza product_updated_at de forma confiable cada vez que algo cambia —el caso de Kiosko—, timestamp detecta exactamente los mismos cambios que check detectaría comparando columna por columna, con menos configuración y sin tener que enumerar cada columna de negocio a mano. Cómo corregirlo: ninguna estrategia es "más segura" en abstracto — la pregunta correcta es si la fuente trae, o no, una columna de fecha confiable. Si la trae, timestamp es más simple y igual de correcta; si no la trae, check es la única opción viable.

Copiar el demo de check de esta lección dentro del proyecto real de Kiosko. Qué pasa: alguien, después de ejecutar el demo de esta lección, deja snapshots/dim_product_check_demo.yml dentro de kiosko_analytics/, pensando que forma parte del proyecto. Por qué pasa: el demo usa una sintaxis casi idéntica a la que vas a usar en la lección 4 para el snapshot real, y es fácil confundir "ejemplo ilustrativo" con "pieza del proyecto". Cómo detectarlo: si en la lección 8 corres dbt ls --select snapshot:* y ves más de un snapshot, algo quedó de más. Cómo corregirlo: el proyecto final de este módulo tiene un solo snapshot, dim_product_snapshot, con strategy: timestamp — el demo de esta lección es, a propósito, un experimento aislado, para que veas la diferencia con tus propios ojos, no una pieza permanente de kiosko_analytics/.

Pensar que el WARNING de tipos que vas a ver en la lección 5 tiene algo que ver con la elección de estrategia. Qué pasa: alguien, al ver un WARNING sobre tipos de datos en la lección 5, sospecha que eligió mal la estrategia en esta lección. Por qué pasa: cualquier WARNING cerca de la palabra "timestamp" invita a sospechar de la estrategia con el mismo nombre. Cómo detectarlo: la lección 5 explica ese WARNING en detalle —tiene que ver con el tipo de dato de product_updated_at (DATE, no TIMESTAMP), no con si elegiste timestamp o check como estrategia—. Cómo corregirlo: separa mentalmente estos dos conceptos —strategy: timestamp es el criterio de comparación (esta lección); el tipo de dato TIMESTAMP de SQL es un tipo de columna (lección 5). Comparten nombre, no comparten significado.

Ejercicios

Ejercicio 1 — Predice el resultado de check sin updated_at, corrido dos veces sobre datos SIN cambios. Sin ejecutar nada, predice: si corrieras el demo dim_product_check_demo de esta lección dos veces seguidas, sin cambiar products_v1.csv entre medio, ¿cuántas filas tendría la tabla al final? ¿Cambiaría algo dbt_valid_from de la única fila?

Ver solución

Seguiría teniendo una sola fila, y dbt_valid_from no cambiaría en la segunda corrida — seguiría mostrando el instante exacto de la primera corrida. La razón: check_cols: [category, unit_cost] compara esos dos valores contra lo ya archivado; si ninguno cambió, row_has_changed da false, y dbt no ejecuta ninguna acción —ni cierre, ni apertura—, sin importar cuánto tiempo real haya pasado entre una corrida y la siguiente. El reloj del sistema solo se consulta quando dbt SÍ detecta un cambio y necesita estampar dbt_valid_from de la fila nueva — nunca "por costumbre" en cada corrida.

Ejercicio 2 — Diseña check_cols para vigilar también product_name. Si Kiosko decidiera que un cambio de nombre de producto (por ejemplo, renombrar "Energy Bar" a "Energy Bar Max") también debería historizarse, ¿qué cambiarías en la configuración check del demo de esta lección?

Ver solución
config:
  unique_key: product_id
  strategy: check
  check_cols:
    - category
    - unit_cost
    - product_name

Agregar product_name a la lista de check_cols — nada más. La estrategia check compara, columna por columna, exactamente las que le declares en esa lista; agregar una nueva columna a vigilar no requiere ningún otro cambio de configuración. (La estrategia timestamp, en cambio, no tiene ningún concepto de "columnas vigiladas" — confía por completo en que product_updated_at ya refleja cualquier cambio relevante, sin importar cuál columna cambió.)

Ejercicio 3 — Argumenta por qué el WARNING de reproducibilidad de esta lección es más grave que un WARNING cualquiera. En 2-3 frases, y usando lo que ya sabes del módulo 1 sobre analytics engineering como disciplina, explica por qué que dbt_valid_from dependa del reloj del sistema —como viste con check sin updated_at— es un problema más serio que un simple detalle cosmético.

Ver solución

Un valor que depende del reloj del sistema rompe la propiedad central que hace confiable un proyecto de datos versionado: que el mismo código, corrido sobre los mismos datos de entrada, produzca siempre el mismo resultado. Si dbt_valid_from cambiara cada vez que alguien corre dbt snapshot, dos personas del mismo equipo, corriendo el mismo comando sobre el mismo products_v2.csv en momentos distintos, terminarían con historiales de P002 con fechas de vigencia distintas — haciendo imposible comparar resultados entre ellas, escribir un test que verifique una fecha exacta, o depurar un problema con confianza. Es exactamente el mismo principio que ya defendió el módulo 1 al prohibir random/CURRENT_TIMESTAMP en cualquier bloque "Qué esperar": la reproducibilidad no es un capricho pedagógico, es lo que separa un pipeline confiable de uno que "parece funcionar" hasta que alguien intenta reproducirlo.

Resumen y siguiente paso

Esta lección ejecutó ambas estrategias sobre el mismo cambio real de P002 y encontró la diferencia que importa: strategy: timestamp usa siempre la fecha de la fuente (product_updated_at), completamente reproducible; strategy: check sin un updated_at explícito usa el reloj del sistema, con hora y microsegundos, distinto en cada corrida. Esa diferencia, verificada con evidencia real y no supuesta, es la razón concreta por la que dim_product_snapshot va a usar strategy: timestamp a partir de la lección 4.

Antes de avanzar deberías poder: explicar, sin mirar la tabla de esta lección, cuándo cada estrategia es la elección correcta; y describir con precisión qué significa que strategy: check sin updated_at "no sea reproducible".

La lección 4 declara, por fin, el snapshot real de este módulo: dim_product_snapshot, con strategy: timestamp, updated_at: product_updated_at, unique_key: product_id — y explica, con la misma evidencia que ya empezaste a ver aquí, por qué apunta al source crudo y no a stg_products.

Recursos

  • dbt Developer Hub — "Add snapshots to your DAG", sección de estrategias (timestamp y check), incluida la mención de que check acepta un updated_at opcional. docs.getdbt.com/docs/build/snapshots. En inglés.
  • dbt Developer Hub — "Snapshot configurations", la referencia completa de check_cols, incluido el valor especial 'all' para vigilar todas las columnas sin enumerarlas. docs.getdbt.com/reference/resource-configs/check_cols. En inglés.
  • data-modeling-for-analytics-guide, lección "El problema: dim_product no es estática" (módulo 4) — la comparación manual entre products_v1 y products_v2 con <>, el mismo criterio que la estrategia check automatiza. Guía hermana del mismo ecosistema.