¡Hola, estimados lectores, y sean bienvenidos a la segunda parte de esta serie de notas sobre DBT! En la primera entrega, hicimos una introducción general a este maravilloso framework de transformación de datos, vimos sus conceptos generales, los componentes de un proyecto y ejecutamos una serie de comandos sencillos para inicializar nuestro primer proceso de transformación de datos. Si no la viste, podés leerla acá.
Hoy vamos a profundizar incluso más en varios de los conceptos ya mencionados, con el foco en entenderlos a fondo y aprovechar las capacidades completas de la herramienta. Vamos a charlar de modelos, fuentes de datos y seeds, y vamos a seguir profesionalizando nuestro proyecto de prueba. ¡Vamos allá!
Modelos
Este es el corazón de nuestro proyecto. En DBT, un modelo es simplemente un archivo de SQL que contiene una declaración SELECT, ni más, ni menos. Aunque esto suena bastante simple, detrás de bambalinas los modelos funcionan como abstracciones que nos permiten crear lógicas de transformación modulares, mantenibles, con control de versionado y testeable para nuestro data warehouse. Veamos más en detalle qué son y cómo funcionan.
¿Qué es realmente un modelo?
Un modelo es un archivo .sql ubicado en el directorio models/ de nuestro proyecto de DBT. Cada modelo típicamente representa un paso de transformación, como puede ser limpieza de datos, setear el tipo de una columna, agregar o sumarizar data, unir fuentes de datos, o un sinfín de opciones más. Veamos un ejemplo sencillo de modelo:
En este caso, estamos seleccionando los clientes activos y haciendo referencia al modelo raw_customers usando la función ref() (que en breve veremos en mayor profundidad) para indicar de dónde se obtendrán los datos. Aquí podemos referenciar fuentes crudas de datos como data warehouses o podemos indicar que debe consumir los datos de otro modelo. Así es como se concatenan las transformaciones. Mi referencia a raw_customers bien podría haber sido a un hipotético int_select_customers, un .sql anterior que filtre los clientes con los que queremos trabajar.
Cuando usamos el comando dbt run para ejecutar nuestros modelos, estos se compilan en SQL y corren en nuestro data warehouse, lo que tiene como resultado una materialización generalmente en forma de vista o tabla (DBT soporta otros tipos de materialización pero son mucho más infrecuentes), dependiendo de cómo lo configuremos. De hecho, una vez ejecutado podemos inspeccionar el código compilado en el directorio target/compiled y veremos cómo las abstracciones como el ref() se traducen en código SQL puro y duro.
Ahora, analicemos las partes de un modelo.
Lógica SQL
Como dijimos anteriormente, el SELECT statement es el núcleo del modelo. Puede ser tan simple o tan complejo como sea necesario, con filtros, joins, subqueries, CTEs… Tenemos todo el arsenal de SQL a nuestra disposición.
La función ref()
Esta función se utiliza para referenciar a otro modelo y cumple varios roles:
- Indica a DBT que hay una dependencia entre modelos.
- Resuelve al schema o tabla correcta en la ejecución.
- Ayuda a que DBT pueda construir un DAG (sigla que significa “directed acyclic graph”) de las dependencias de los modelos.
Cuando en nuestro ejemplo usamos FROM {{ ref(‘raw_customers’) }}, DBT entiende que primero debe construir ‘raw_customers’ antes de ejecutar nuestro modelo actual.
Configuraciones y settings del modelo
Los modelos en DBT pueden tener configuraciones específicas que determinan cómo y dónde se materializan los resultados, cómo se nombran las tablas resultantes, o cómo se agrupan para tareas automatizadas, entre otras cosas. Estas configuraciones se definen al principio del archivo .sql del modelo usando el bloque {{ config(…) }} o en el archivo dbt_project.yml.
Veamos los parámetros más comunes y su utilidad:
materialized
Este es probablemente el setting más importante, ya que define de qué forma se materializa el resultado del modelo en el data warehouse. Puede tomar uno de los siguientes valores:
- view: el resultado del modelo se crea como una vista y es la opción por defecto. No ocupa espacio en disco y siempre refleja datos actualizados, aunque puede implicar un costo mayor de cómputo al realizar una query.
- table: el modelo se materializa como una tabla. Esto implica que los datos se persisten al momento de ejecutar dbt run. Es ideal para transformaciones pesadas o cuando se necesita optimizar el tiempo de query.
- incremental: esta opción permite actualizar sólo una parte de los datos cada vez que se ejecuta el modelo, en lugar de recrear toda la tabla. Es útil para manejar grandes volúmenes de datos en pipelines productivos.
- ephemeral: no se materializa en el warehouse. El modelo se convierte en un CTE (Common Table Expression) dentro de otro modelo que lo consuma. Es la mejor opción para pasos intermedios que no se desean persistir.
schema
Permite sobrescribir el schema por defecto configurado en el proyecto. Nos sirve para cuando queremos segmentar nuestros modelos por entornos (como por ejemplo dev, staging, prod) o por procesos específicos.
alias
Controla el nombre final con el que se creará la tabla o vista en el warehouse. Por defecto, DBT usa el nombre del archivo .sql como nombre de la tabla generada, pero con alias podemos cambiarlo si deseamos hacerlo al mismo tiempo que mantenemos alguna convención de nomenclatura de archivos en nuestro proceso de transformación.
tags
Permiten etiquetar los modelos para agruparlos lógicamente. Estas etiquetas pueden luego usarse para ejecutar solo una parte específica del proyecto con comandos como dbt run –select tag:incrementales, o para análisis y documentación.
Sources
En todo modelo de DBT, el primer paso es consumir datos que ya existen en nuestro sistema: bases de datos transaccionales, sistemas de terceros, APIs o cualquier fuente que consideremos necesaria o apropiada. Dentro del framework de DBT, estas fuentes externas (es decir, tablas que no fueron generadas por DBT, sino que ya están presentes en el data warehouse) se configuran como sources.
Esto permite que DBT sepa que esos datos son parte de nuestra lógica de transformación y nos habilita a usar funcionalidades como tests automáticos, documentación centralizada, y una visualización clara en el DAG de dependencias.
Definición de sources
Las fuentes de datos se definen en archivos .yml dentro del directorio models/, típicamente en archivos como src_*.yml, aunque no es necesaria una nomenclatura específica y puede ser cualquier nombre. Allí usamos una estructura YAML para dejar un registro de nuestros datos externos.
En este caso, tenemos los siguientes componentes:
- raw_db es un identificador lógico para la base de datos de origen.
- schema define el esquema en el warehouse donde se encuentran las tablas reales.
- tables es la lista de tablas concretas que queremos usar como fuentes.
Implementación de sources
Ahora que ya tenemos las fuentes de datos definidas en nuestro YAML y listas para usarse… ¿cómo las implementamos efectivamente en nuestro modelo? Acá viene la función source() a ayudarnos. Veamos un pequeño ejemplo:
En estas dos sencillas líneas de código, lo que le estamos diciendo a DBT es: “Quiero consumir la tabla customers que está en el schema raw y que forma parte de la source raw_db”. Es decir, al igual que con la función ref(), aquí DBT abstrae mediante funciones gran parte de lo que hace tras bambalinas. Pero, a diferencia de ref(), source() se usa para referenciar datos externos.
Usar source(), además, tiene una serie de beneficios por encima de usar directamente el nombre de la tabla en el modelo:
- Trazabilidad y documentación: DBT puede incluir estos datos en la documentación generada automáticamente con el comando dbt docs generate (que veremos luego para crear la documentación de nuestro proyecto).
- Tests: Podemos aplicar tests automáticos sobre las tablas externas (como verificar que no tengan valores nulos, que tengan una clave primaria válida, entre muchas otras opciones).
- Control de cambios: Si el esquema o el nombre de la tabla cambia, solo tenemos que modificarlo en el archivo .yml y no en todos los modelos.
- Visualización del DAG: Las fuentes aparecen como nodos en el grafo de dependencias, lo que facilita enormemente el entendimiento del flujo completo.
Algunas configuraciones adicionales
Lo último que veremos de sources por hoy son dos configuraciones adicionales bastante simples pero útiles para la definición de nuestras sources. Por ejemplo, podemos documentar tanto las fuentes como las tablas:
Y podemos declarar tests que queremos implementar sobre nuestras tablas fuente:
Esto nos permite garantizar que el customer_id es único y no es un valor nulo. Es una excelente práctica definir buenos tests que den robustez a nuestros pipelines de transformación de datos.
Seeds
El último componente que veremos hoy son los seeds. En DBT, los seeds son archivos CSV que viven dentro del proyecto y se cargan directamente en el data warehouse como tablas. Funcionan como pequeñas bases de datos estáticas: pueden representar catálogos de referencia, listas de códigos, reglas de negocio, mapeos, o incluso datos de prueba. Es decir, son especialmente útiles cuando tenemos un conjunto de datos que no sufrirá modificaciones o serán modificaciones muy controladas, como una lista de países de operación de una empresa o una lista acotada de proveedores.
Esta funcionalidad es especialmente útil cuando queremos incorporar datasets simples o controlados por el equipo de datos, sin tener que depender de integraciones externas ni procesos de ingesta complejos. Copiamos el CSV en la carpeta /seeds de nuestro directorio, ejecutamos el comando dbt seed y pum, estamos listos para consumir esos datos.
La diferencia clave entre seeds y sources a la hora de usar los datos es que los seeds se referencia con la función ref() que ya hemos visto, al igual que los modelos. En la demo veremos un seed en acción.
Demo time!
Ahora que ya tenemos estos conceptos más cocinados, vamos a seguir avanzando con la demo que empezamos la edición pasada. Antes que nada, para mantener la organización del proyecto, eliminemos la carpeta de my_project/models/example que generó automáticamente DBT al iniciar el proyecto, y luego vamos a crear una base de datos dummy para tener a nuestra disposición un poco de data que podamos procesar y ver sus resultados.
Para volver a levantar el contenedor de Docker (salvo que hayan aguardado todas estas semanas pacientemente con la compu prendida y el contenedor activo), vamos a correr estos comandos en nuestra terminal para activar el entorno virtual que creamos la vez pasada y el contenedor:

Ahora, vamos a crear dos tablas dummies dentro de nuestro contenedor para poder crear algunas transformaciones en DBT. Copien y peguen el siguiente código en la terminal.
Ahora vamos a agregar data dummy. Esta es la data de raw_customers:
Y esta es la data de raw_orders:
Luego pueden correr esta query para verificar los contenidos de las tablas que creamos:


Ahora vamos a definir y configurar nuestras nuevas sources. Dentro de la carpeta de modelos (my_project/models/), vamos a crear un YAML al que llamaremos src_raw_data.yml. En él, vamos a pegar el siguiente contenido:
Aquí ya tenemos definidas como sources de nuestro proyecto las dos tablas que hemos creado, y además configuramos un par de tests (unicidad y no nulidad de los ID). Paso siguiente, vamos a actualizar nuestro archivo para incluir la información del nuevo modelo dbt_project.yml. Pongamos este código:
Aquí ya seteamos las configuraciones de nuestros modelos en el archivo general del proyecto, y definimos nuestra estrategia de materialización como “view”. ¡Ahora podemos finalmente crear nuestro modelo intermedio! Dentro de la carpeta /models, vamos a iniciar un nuevo archivo .sql y vamos a pegar el siguiente código SQL.
Este es un modelo tal cual el que vimos al comienzo de la nota. Lo que hace es tomar nuestras dos sources, asignarles un alias, y unirlas mediante el uso de nuestro customer_id. Ahora, para correrlo, ejecuten el siguiente comando:
dbt run --select int_summarize_data
Si hicieron todo correctamente, deberían ver algo similar a esto:

Si quieren ver el código compilado, pueden correr esto:
dbt compile –select int_summarize_data
Y si queremos ver el resultado de lo que se materializó como vista, se puede hacer con esta línea de código:
psql -U postgres -h localhost -p 5432 -d postgres -c “SELECT * FROM postgres.int_summarize_data LIMIT 10;”
Esto nos va a pedir la contraseña que definimos la vez pasada en el archivo profiles.yml. El resultado debería ser este:

¡Listo! Con esto ya corrimos nuestro primer modelo custom de DBT, y unimos la data de las dos tablas usando el customer_id como clave común. Ahora podemos visualizar el nombre, apellido, mail, fecha de creación, número de orden, fecha de orden y monto de orden para cada uno de nuestros clientes.
Para cerrar nuestra demo de hoy, vamos a crear un seed y usarlo para clasificar nuestras ventas según su tamaño. Pueden encontrar el CSV con la información aquí y descargarlo directamente (¡borren el – Hoja 1 del nombre!). Este archivo contiene categorías de clasificación de las órdenes de nuestros clientes y, como es data estática y breve que no cambiará frecuentemente, es apropiado considerarlo como un seed.
Si se fijan, debajo de la carpeta de /models tenemos una llamada seeds. Vamos a dejar nuestro archivo recién descargado allí y vamos a actualizar nuestro dbt_project.yml con la configuración de seeds.
Hecho todo esto, podemos correr el seed para cargarlo.
dbt seed
El resultado debería ser algo como esto:

Y posterior a esto, vamos a crear un nuevo intermedio. Lo vamos a llamar int_categorize_orders y va a ser un archivo .sql. Allí, vamos a alojar la siguiente query.
Ahora, si corremos este modelo, vamos a unificar nuestras transformaciones.
dbt run –select int_categorize_orders
Y si ejecutamos una query sobre la vista, veremos los resultados.
psql -U postgres -h localhost -p 5432 -d postgres -c “SELECT * FROM postgres.int_categorize_orders LIMIT 10;”

En conclusión…
Ha sido otra jornada de mucho aprendizaje, muchos nuevos conceptos y muchas nuevas herramientas en nuestro arsenal. Hoy entendimos en profundidad qué son los modelos, cómo se configuran y cómo se ejecutan; aprendimos a definir y usar sources para nuestras transformaciones de datos; nos familiarizamos con el concepto de seeds; entendimos cómo configurar settings de nuestro proyecto; e incluso en la demo ya trabajamos con nuestras primeras transformaciones encadenadas. ¡Bastante bien!
¡Pero no teman, queridos lectores! Si se quedaron con ganas, aún hay más. Vamos a hacer una nueva nota en la que veamos cómo crear marts, cómo documentar nuestros procesos y cómo correr tests para garantizar la integridad de nuestra data. Como siempre, gracias por leer y espero que les haya sido de utilidad.


