Módulo 2: Sources And Staging Models

Mini-proyecto: la capa de staging completa de Kiosko

Descripción

Es momento de cerrar el módulo completando lo que falta y corriendo todo junto, de punta a punta. Las lecciones 2 a 4 declararon los cuatro sources de Kiosko y los conectaron a sus archivos físicos. Las lecciones 5 y 6 definieron la forma y el nombre de un staging model. La lección 7 construyó los dos primeros: stg_orders y stg_events. Este mini-proyecto completa los dos que faltan —stg_stores y stg_products—, corre la capa completa con dbt run --select staging, verifica que las cuatro tablas tienen exactamente el mismo conteo de filas que sus archivos crudos, y agrega la pieza nueva de este módulo: el segundo commit del proyecto de Kiosko, sobre el primero que dejó el módulo 1.

Conexión con el módulo. Este mini-proyecto no introduce ningún concepto nuevo de dbt —es la síntesis de las lecciones 2 a 7, ejecutadas sin interrupciones—, más la continuación exacta del hábito de control de versiones que el módulo 1 empezó. Al cerrar esta lección, kiosko_analytics/ tiene una capa de staging completa, verificada y versionada — la base sobre la que el módulo 3 va a construir el primer mart real.

Una analogía: el primer inventario completo del almacén

Vuelve, por última vez en este módulo, al almacén de las lecciones 2 a 4. Ya registraste a los cuatro proveedores (source(), lección 3), ya confirmaste la dirección exacta de cada muelle (external_location, lección 4), y ya empezaste a recibir mercadería de los dos proveedores más grandes (stg_orders, stg_events, lección 7). Este mini-proyecto es el día en que llega la mercadería de los dos proveedores restantes —stores y products— y, por primera vez, alguien camina por todo el almacén con una planilla, contando caja por caja, confirmando que lo que hay en el estante coincide exactamente con lo que el registro de recepción dice que debería haber. Ese conteo final, firmado y archivado en el libro de bitácora del almacén, es exactamente lo que vas a hacer al cerrar esta lección con un commit de git.

El material: los dos staging models que faltan

Completa models/staging/kiosko/ con los dos archivos que quedaron pendientes desde la lección 7:

-- models/staging/kiosko/stg_stores.sql
select
    store_id,
    store_name,
    city
from {{ source('kiosko_raw', 'stores') }}
-- models/staging/kiosko/stg_products.sql
select
    product_id,
    product_name,
    category,
    cast(unit_cost as decimal(10, 2)) as unit_cost,
    cast(product_updated_at as date) as product_updated_at
from {{ source('kiosko_raw', 'products') }}

stg_stores.sql no necesita ningún cast() — las tres columnas ya son texto en el archivo crudo, y texto es exactamente lo que deberían seguir siendo. stg_products.sql castea unit_cost a decimal(10, 2) por la misma razón que unit_price en stg_orders (lección 7: nunca dinero como float), y castea product_updated_at a date —no timestamp— porque esa columna representa un día completo, sin hora, la fecha que en el módulo 5 va a disparar el snapshot de dim_product.

Y completa _models.yml con las pruebas mínimas de los dos catálogos que faltan:

# models/staging/kiosko/_models.yml
version: 2

models:
  - name: stg_orders
    description: "Una fila por orden, tipos ya castings, sin joins ni agregaciones."
    columns:
      - name: order_id
        data_tests:
          - unique
          - not_null

  - name: stg_events
    description: "Una fila por evento de clickstream, tipos ya castings."
    columns:
      - name: event_id
        data_tests:
          - unique
          - not_null

  - name: stg_stores
    description: "Catalogo de tiendas, sin transformar."
    columns:
      - name: store_id
        data_tests:
          - unique
          - not_null

  - name: stg_products
    description: "Catalogo de productos version 1, sin transformar."
    columns:
      - name: product_id
        data_tests:
          - unique
          - not_null

La solución de referencia, verificada

Parte 1 — Correr la capa completa

dbt run --select staging

Qué esperar.

Running with dbt=1.12.2
Registered adapter: duckdb=1.11.0
Found 4 models, 8 data tests, 4 sources, 500 macros

Concurrency: 4 threads (target='dev')

1 of 4 START sql view model main.stg_events .................................... [RUN]
2 of 4 START sql view model main.stg_orders .................................... [RUN]
3 of 4 START sql view model main.stg_products .................................. [RUN]
4 of 4 START sql view model main.stg_stores .................................... [RUN]
4 of 4 OK created sql view model main.stg_stores ............................... [OK in 0.07s]
1 of 4 OK created sql view model main.stg_events ............................... [OK in 0.07s]
3 of 4 OK created sql view model main.stg_products ............................. [OK in 0.07s]
2 of 4 OK created sql view model main.stg_orders ............................... [OK in 0.07s]

Finished running 4 view models in 0 hours 0 minutes and 0.18 seconds (0.18s).

Completed successfully

Done. PASS=4 WARN=0 ERROR=0 SKIP=0 NO-OP=0 REUSED=0 TOTAL=4

Cuatro modelos, cuatro OK, corriendo en paralelo —dbt no tiene ninguna razón para ordenarlos entre sí, porque ninguno depende de otro, la garantía exacta de la regla "sin joins" de la lección 5. Si esta corrida falla, no sigas a la Parte 2 — vuelve a la lección correspondiente según cuál archivo sea la causa: _sources.yml (lecciones 2-4), la sintaxis del .sql (lección 5 y 7), o dbt_project.yml (lección 6).

Parte 2 — Correr la suite de pruebas completa

dbt test --select staging

Qué esperar.

Running with dbt=1.12.2
Registered adapter: duckdb=1.11.0
Found 4 models, 8 data tests, 4 sources, 500 macros

Concurrency: 4 threads (target='dev')

1 of 8 START test not_null_stg_events_event_id ................................. [RUN]
2 of 8 START test not_null_stg_orders_order_id ................................. [RUN]
3 of 8 START test not_null_stg_products_product_id ............................. [RUN]
4 of 8 START test not_null_stg_stores_store_id ................................. [RUN]
1 of 8 PASS not_null_stg_events_event_id ....................................... [PASS in 0.05s]
4 of 8 PASS not_null_stg_stores_store_id ....................................... [PASS in 0.05s]
3 of 8 PASS not_null_stg_products_product_id ................................... [PASS in 0.05s]
5 of 8 START test unique_stg_events_event_id ................................... [RUN]
6 of 8 START test unique_stg_orders_order_id ................................... [RUN]
7 of 8 START test unique_stg_products_product_id ............................... [RUN]
2 of 8 PASS not_null_stg_orders_order_id ....................................... [PASS in 0.07s]
8 of 8 START test unique_stg_stores_store_id ................................... [RUN]
8 of 8 PASS unique_stg_stores_store_id ......................................... [PASS in 0.02s]
7 of 8 PASS unique_stg_products_product_id ..................................... [PASS in 0.03s]
5 of 8 PASS unique_stg_events_event_id ......................................... [PASS in 0.04s]
6 of 8 PASS unique_stg_orders_order_id ......................................... [PASS in 0.04s]

Finished running 8 data tests in 0 hours 0 minutes and 0.18 seconds (0.18s).

Completed successfully

Done. PASS=8 WARN=0 ERROR=0 SKIP=0 NO-OP=0 REUSED=0 TOTAL=8

Ocho pruebas —dos por cada una de las cuatro tablas—, ocho PASS. Ningún identificador duplicado, ningún identificador nulo, en ninguna de las cuatro tablas de Kiosko.

Parte 3 — La reconciliación: crudo contra staging, tabla por tabla

Esta es la verificación que de verdad importa —la que ningún PASS=4 ni PASS=8 puede reemplazar—: confirmar, con una consulta, que el conteo de filas de cada staging model coincide exactamente con el de su archivo crudo correspondiente.

import duckdb

con = duckdb.connect("kiosko.duckdb")
raw = {
    "orders": con.sql("select count(*) from read_csv_auto('raw_data/kiosko/orders_*.csv')").fetchone()[0],
    "events": con.sql("select count(*) from read_ndjson_auto('raw_data/kiosko/events_*.jsonl')").fetchone()[0],
    "stores": con.sql("select count(*) from read_csv_auto('raw_data/kiosko/stores.csv')").fetchone()[0],
    "products": con.sql("select count(*) from read_csv_auto('raw_data/kiosko/products_v1.csv')").fetchone()[0],
}
staged = {
    "orders": con.sql("select count(*) from stg_orders").fetchone()[0],
    "events": con.sql("select count(*) from stg_events").fetchone()[0],
    "stores": con.sql("select count(*) from stg_stores").fetchone()[0],
    "products": con.sql("select count(*) from stg_products").fetchone()[0],
}

print(f"{'tabla':<10}{'crudo':>8}{'staging':>10}{'match':>8}")
for t in ["orders", "events", "stores", "products"]:
    m = "OK" if raw[t] == staged[t] else "MISMATCH"
    print(f"{t:<10}{raw[t]:>8}{staged[t]:>10}{m:>8}")

Qué esperar.

tabla        crudo   staging   match
orders          40        40      OK
events          32        32      OK
stores           3         3      OK
products         4         4      OK

Cuatro filas, cuatro OK. Fíjate en algo deliberado de este script: cuenta el lado "crudo" leyendo los archivos directamente con read_csv_auto/read_ndjson_auto —sin pasar por source() ni por ningún staging model—, y el lado "staging" consultando las vistas que dbt run ya construyó. Son dos caminos completamente independientes hacia el mismo archivo en disco; que coincidan exactamente confirma que la conversión completa —de CSV/JSONL crudo, a source(), a stg_*— no perdió ni duplicó una sola fila en ningún paso de la cadena.

Parte 4 — Confirmar el listado completo del proyecto

dbt ls --select staging
dbt ls --select source:kiosko_raw

Qué esperar.

kiosko_analytics.staging.kiosko.stg_events
kiosko_analytics.staging.kiosko.stg_orders
kiosko_analytics.staging.kiosko.stg_products
kiosko_analytics.staging.kiosko.stg_stores

source:kiosko_analytics.kiosko_raw.events
source:kiosko_analytics.kiosko_raw.orders
source:kiosko_analytics.kiosko_raw.products
source:kiosko_analytics.kiosko_raw.stores

Cuatro sources, cuatro staging models — una correspondencia uno a uno perfecta, exactamente como prometió la lección 6. Ningún source sin su staging model; ningún staging model sin su source.

Parte 5 — El segundo commit del proyecto

Con todo verificado, es momento de versionar el trabajo del módulo. Revisa primero qué cambió, sin agregar nada todavía:

git status --short

Qué esperar.

 M dbt_project.yml
 D models/example/my_first_dbt_model.sql
?? models/staging/
?? raw_data/

Cuatro tipos de cambio, cada uno con una historia: dbt_project.yml modificado (el bloque staging: +materialized: view de la lección 6), models/example/my_first_dbt_model.sql eliminado (el modelo trivial del módulo 1, borrado en la lección 2), y dos carpetas nuevas sin trackear, models/staging/ y raw_data/. Fíjate en que ningún archivo generado aparece en esta lista —ni kiosko.duckdb, ni target/, ni logs/— porque el .gitignore que escribiste en el módulo 1 sigue haciendo exactamente su trabajo, sin que hayas tenido que tocarlo.

Agrega los archivos y commitea:

git add raw_data models dbt_project.yml
git commit -m "Module 2: declare Kiosko sources and build the staging layer"

Qué esperar.

[master a1b2c3d] Module 2: declare Kiosko sources and build the staging layer
 24 files changed, 91 insertions(+), 7 deletions(-)
 create mode 100644 models/staging/kiosko/_models.yml
 create mode 100644 models/staging/kiosko/_sources.yml
 create mode 100644 models/staging/kiosko/stg_events.sql
 create mode 100644 models/staging/kiosko/stg_orders.sql
 create mode 100644 models/staging/kiosko/stg_products.sql
 create mode 100644 models/staging/kiosko/stg_stores.sql
 delete mode 100644 models/example/my_first_dbt_model.sql
 create mode 100644 raw_data/kiosko/events_2026-08-03.jsonl
 create mode 100644 raw_data/kiosko/orders_2026-08-03.csv
 ...

(El identificador corto del commit, a1b2c3d en este ejemplo, va a ser distinto en tu máquina — como ya viste en el módulo 1, es un hash generado a partir del contenido exacto y el momento del commit.) Confirma la historia completa y el árbol de trabajo limpio:

git log --oneline
git status

Qué esperar.

a1b2c3d (HEAD -> master) Module 2: declare Kiosko sources and build the staging layer
af0b710 First dbt project: kiosko_analytics scaffolding

En la rama master
nada para hacer commit, el árbol de trabajo está limpio

Dos commits, cada uno con un mensaje que describe, en una línea, qué cambió y por qué — un compañero de equipo que corra git log años después de que termine esta guía va a poder leer, sin ambigüedad, en qué momento el proyecto pasó de tener un modelo trivial a tener sources y staging models reales.

Diagrama: el proyecto completo al cerrar el módulo 2

flowchart LR
    subgraph raw["raw_data/kiosko/ (16 archivos)"]
        O["orders_*.csv (40 filas)"]
        E["events_*.jsonl (32 filas)"]
        S["stores.csv (3 filas)"]
        P["products_v1.csv (4 filas)"]
    end
    subgraph src["sources.yml (kiosko_raw)"]
        SO["source: orders"]
        SE["source: events"]
        SS["source: stores"]
        SP["source: products"]
    end
    subgraph stg["models/staging/kiosko/"]
        GO["stg_orders (view, 40 filas)"]
        GE["stg_events (view, 32 filas)"]
        GS["stg_stores (view, 3 filas)"]
        GP["stg_products (view, 4 filas)"]
    end
    O --> SO --> GO
    E --> SE --> GE
    S --> SS --> GS
    P --> SP --> GP

Cuatro flechas independientes, sin ningún cruce entre ellas — la representación visual exacta de la regla de la lección 5: cada staging model depende de exactamente un source, sin combinarse con ningún otro.

Errores comunes

Olvidar git add sobre raw_data/ por pensarlo "solo datos de prueba". Qué pasa: alguien, acostumbrado a que kiosko.duckdb no se versiona (lección 8 del módulo 1), asume por reflejo que raw_data/ tampoco debería versionarse, y la deja fuera del commit. Por qué pasa: ambas carpetas contienen "datos", y es fácil generalizar la regla incorrectamente. Cómo detectarlo: si clonaras este repositorio en una máquina nueva y corrieras dbt run --select staging sin raw_data/, fallaría de inmediato con el mismo IO Error: No files found que ya viste en la lección 4 — porque, a diferencia de kiosko.duckdb, raw_data/ no es un artefacto derivado que dbt pueda regenerar; es la fuente original de la que todo lo demás depende. Cómo corregirlo: la regla correcta no es "datos sí/no se versionan" — es "¿este archivo se puede reconstruir a partir de otro archivo ya versionado?". kiosko.duckdb sí (se reconstruye con dbt run); raw_data/kiosko/*.csv y *.jsonl no —son el punto de partida—, así que sí se versionan.

Confundir dbt run con dbt run --select staging cuando ya hay más de cuatro modelos. Qué pasa: alguien, en un módulo futuro con marts además de staging models, corre dbt run --select staging esperando que también corran los marts que dependen de esos staging models. Por qué pasa: es fácil asumir que "correr staging" implica, en cascada, correr todo lo que depende de staging. Cómo detectarlo: si consultas un mart después de correr solo --select staging y ves datos desactualizados o el mart no existe todavía, revisa exactamente qué seleccionaste. Cómo corregirlo: --select staging selecciona únicamente los modelos dentro de la carpeta models/staging/ (y sus subcarpetas) — nada de lo que dependa de ellos corre automáticamente, a menos que uses el operador + (--select staging+), algo que vas a conocer en el módulo 3, cuando ref() haga que esa cascada tenga sentido por primera vez.

Interpretar PASS=8 en dbt test como "los datos de Kiosko no tienen ningún problema". Qué pasa: alguien ve las ocho pruebas en verde y concluye que el dataset completo de Kiosko está libre de cualquier inconsistencia. Por qué pasa: ocho PASS se siente exhaustivo, pero solo se declararon dos tipos de prueba (unique, not_null) sobre cuatro columnas específicas (los identificadores primarios) — nada más. Cómo detectarlo: pregúntate qué columnas y qué reglas no tienen ningún test todavía —por ejemplo, nada valida todavía que event_type solo contenga page_view/add_to_cart/purchase, o que quantity nunca sea negativo—. Cómo corregirlo: esta lección declaró, a propósito, el mínimo necesario para cerrar el módulo — el módulo 4 completo está dedicado a expandir esa cobertura con los otros dos tests genéricos de fábrica, un test propio, y tests singulares para reglas de negocio específicas.

Ejercicios

Ejercicio 1 — Reconstruye la reconciliación para un quinto archivo hipotético. Si Kiosko agregara deliveries.csv (del ejercicio de la lección 6) con 15 filas, y construyeras stg_deliveries correctamente, ¿qué línea nueva agregarías al diccionario raw y al diccionario staged del script de la Parte 3, y qué esperarías ver en la salida final?

Ver solución
raw["deliveries"] = con.sql("select count(*) from read_csv_auto('raw_data/kiosko/deliveries.csv')").fetchone()[0]
staged["deliveries"] = con.sql("select count(*) from stg_deliveries").fetchone()[0]

Y en el bucle final, agregarías "deliveries" a la lista de tablas a recorrer. Si stg_deliveries.sql está bien escrito —un solo source(), sin joins—, la salida esperada sería una quinta fila: deliveries 15 15 OK, exactamente el mismo patrón de las otras cuatro.

Ejercicio 2 — Provoca una reconciliación fallida a propósito. Modifica temporalmente stg_stores.sql para que tenga un WHERE city != 'Lima' (filtrando la tienda S02), y vuelve a correr el script de reconciliación de la Parte 3. ¿Qué cambia en la salida?

Ver solución

La fila de stores ahora muestra stores 3 2 MISMATCH — el conteo crudo sigue siendo 3 (el archivo no cambió), pero el conteo de staging bajó a 2, porque el WHERE filtró una fila. Este es exactamente el tipo de error que la reconciliación existe para atrapar: un dbt run seguiría reportando PASS=1 sin ningún problema —el SQL es perfectamente válido—, pero la reconciliación expone, con un número concreto, que el staging model ya no representa fielmente su fuente. Deshaz el cambio antes de continuar.

Ejercicio 3 — Argumenta, con tus propias palabras, por qué este mini-proyecto es el punto de partida correcto para el módulo 3. En 2-3 frases, explica qué garantías te da tener las cuatro tablas de staging completas, probadas y versionadas, antes de empezar a construir dim_store, dim_date y fact_orders.

Ver solución

Los marts del módulo 3 van a combinar varias de estas cuatro tablas con JOIN —algo que, como viste en la lección 5, un staging model nunca debería hacer—, así que necesitan partir de una base ya limpia, con tipos correctos y sin pérdida de filas, para que cualquier problema que aparezca después sea claramente un problema del JOIN o la lógica del mart, no un problema heredado y escondido de la capa de staging. Además, con los ocho data_tests ya corriendo en verde y el commit ya hecho, cualquier cambio futuro a stg_orders o stg_events que rompa algo se va a detectar de inmediato —con dbt test— y va a quedar registrado en el historial de git exactamente en qué commit ocurrió.

Resumen y siguiente paso

En este mini-proyecto completaste la capa de staging del proyecto de Kiosko: stg_stores y stg_products, los dos modelos que faltaban, corridos junto a stg_orders y stg_events con dbt run --select staging (PASS=4) y probados con dbt test --select staging (PASS=8). Verificaste, con una reconciliación independiente entre el archivo crudo y la vista de staging, que las cuatro tablas —40, 32, 3 y 4 filas— no perdieron ni duplicaron una sola fila en ningún paso de la cadena. Y cerraste el módulo con el segundo commit del proyecto, extendiendo el hábito de control de versiones que el módulo 1 empezó.

Con esto cierras el módulo 2. Tienes ahora cuatro sources declarados y verificados, cuatro staging models limpios, probados y versionados, y un proyecto kiosko_analytics/ con dos commits en su historia — cada uno documentando, en una línea, un hito real y verificable del proyecto.

Hacia dónde sigues. El módulo 3 introduce la función ref() —la que construye el grafo de dependencias que dbt resuelve por ti— y usa stg_orders y stg_stores como los primeros dos modelos de los que un mart real depende: vas a reconstruir dim_store, dim_date y fact_orders, el star schema completo que data-modeling-for-analytics-guide diseñó a mano, ahora como modelos dbt encadenados. La capa de staging que construiste en este módulo no cambia en absoluto a partir de aquí; lo único que cambia es que, por primera vez, algo más del proyecto va a depender de ella.

Recursos

  • dbt Developer Hub — "About dbt projects", la vista general que confirma la estructura completa —sources, staging, y lo que sigue— que este mini-proyecto consolida. docs.getdbt.com/docs/build/projects. En inglés.
  • dbt Developer Hub — "dbt ls", de nuevo la referencia del comando usado en la Parte 4 para confirmar la correspondencia uno a uno entre sources y staging models. docs.getdbt.com/reference/commands/list. En inglés.
  • Git — documentación oficial de git log, incluidas las opciones de formato (--oneline, usada en esta lección) para revisar el historial de commits de un proyecto. git-scm.com/docs/git-log. En inglés.