Buenas prácticas
Cierre del manual: decisiones que se revisan en PR, no eslóganes. El CI del capítulo 7 solo es útil si los modelos tienen grain, tests con intención y relaciones resueltas por ref()/source().
Documentación: best-practice workflows, how we structure projects, model contracts, model versions.
Ámbito: dbt Core (v1) self-hosted. Contratos, versions y Mesh son features vigentes; no son un requisito de cada modelo. Fusion / dbt platform se etiquetan si cambia el runtime.
Capas (convención, no parser)
models/
├── staging/ stg_* source() → rename / cast
├── intermediate/ int_* solo si reutilizas de verdad
└── marts/ dim_* / fct_* (si modelas en dimensional)dbt no exige estas carpetas. Sí aplica la materialización que pongas en dbt_project.yml por directorio (capítulo 6). Un proyecto de cuatro modelos no necesita intermediate/.
Staging. Una source, una limpieza ligera: nombres, tipos, filtros obvios (amount is not null). Lógica de “qué es un cliente activo” no va aquí: el grain del origen se conserva. Excepción rara: un trampolín mínimo antes de un snapshot (capítulo 4).
Intermediate. Joins o reglas que dos marts compartirían. Si solo hay un consumidor, el SQL puede vivir en el mart. No inventes una capa para el diagrama.
Marts. Tablas que alguien consulta. En modelado dimensional, dim_ (entidad) y fct_ (evento/medida) ayudan. No es la única arquitectura: un mart ancho por proceso de negocio también es válido si el grain está escrito.
Prefijos stg_ / int_ / dim_ / fct_ son consistencia para humanos. No son tokens del parser.
Grain
Cada modelo responde: ¿qué es una fila?
fct_orders: una fila = un pedido. Entonces:
unique+not_nullenorder_id(capítulo 3);- ningún JOIN que duplique pedidos sin agregación;
unique_keyde un incremental = ese grain (capítulo 6);- la descripción YAML lo dice en una frase.
Si no puedes escribir el grain, el modelo mezcla hechos. Parte el fichero; no “añadas una columna más”.
source() en el borde, ref() dentro
No hardcodees raw.public.orders ni analytics.stg_orders. El DAG, el lineage y el Slim CI dependen de ello (capítulo 2). Staging lee sources; marts leen models (y a veces seeds/snapshots). Un mart que salta a source() duplica tipado y desaparece del grafo de staging.
Tests con intención
No pongas not_null en las 40 columnas. Prioriza contratos reales:
- claves de negocio / primary keys;
relationshipsdonde el mart asume integridad;accepted_valuesen estados que el BI filtra;- singulares para invariantes (“
amountdel pedido ≥ 0”).
Un test es una aserción que alguien defenderá cuando falle. warn mientras el origen aún miente; error cuando el contrato ya es de equipo. CI que avisa de todo y no bloquea nada no es contrato.
Documentación útil
description: "Order id" no aporta. Sí aporta:
- grain;
- origen (
raw.orders, filtro aplicado); - owner / canal si el mart es interfaz;
- casos límite (pedidos a 0, cancelados, zona horaria).
dbt docs generate dibuja el DAG. Un DAG bonito no arregla un grain mal definido.
Contracts y versions (cuando el mart es API)
Contract (contract: {enforced: true} + columnas/tipos en YAML): dbt comprueba el SELECT contra el esquema declarado y lo refleja en el DDL. Vigente. Útil en marts que otros equipos consumen. Ruido en staging que cambia cada semana. Un contract se rompe al quitar una columna o cambiar el tipo; Slim CI puede errar esa PR a propósito.
Model versions (Mesh): varias versiones del mismo mart en el proyecto, con ventana de deprecación, cuando no puedes migrar a todos los consumidores el mismo día. No es git. No versionas cada stg_* “por si acaso”: el coste es materializar v1 y v2 a la vez. Cambio aditivo (columna nueva) → no hace falta versión. Breaking change en un contrato maduro → versión o coordinación explícita.
Ni contracts ni versions aplican a seeds/snapshots/sources.
Incrementales (capítulo 6)
Antes de materialized='incremental':
- grain estable y
unique_keyrealmente única (sin nulls); - filtro
is_incremental()que incluye datos tardíos (>= max(updated_at), no>); - estrategia que el adapter soporta;
- plan de
--full-refresh(cuándo, quién, cuánto cuesta); - tests de unicidad en cada CI, no solo en el backfill.
Si el run aún es barato en table, no adelantes el incremental.
Macros (capítulo 5)
Repetir cinco líneas de SQL legible ≠ necesidad de macro. Extrae cuando la regla es estable (céntimos → decimal, surrogate key de paquete). Si la macro es el mart, el review se vuelve arqueología Jinja.
SQL revisable
Nombres de CTE que dicen el paso (orders_renamed, no tmp2). Un modelo, un grain. No hay límite mágico de líneas; hay límite de lo que un reviewer entiende en un diff.
Packages
packages.yml + dbt deps. Fija versiones (version: 1.3.2, no ">0"). Revisa upgrades como cualquier dependencia. No añadas dbt_utils para una macro de tres líneas que ya tienes.
Entornos
Los mismos modelos, distintos targets: dev, CI, prod. ref() resuelve el schema del target (capítulo 2 y 7). Un literal analytics_prod.fct_orders en el SQL anula CI selectiva y el portátil del compañero.
Coste del warehouse
dbt no abarata un SELECT * de tres años. Mide modelos lentos (logs, run_results, UI del warehouse). Materializa tables donde BI pega; deja views el staging ligero. Full refresh e incrementales mal filtrados salen caros en Snowflake, BigQuery, Redshift o Databricks por igual: el mecanismo de facturación cambia, el desperdicio no.
Lineage ≠ diseño
50 modelos en fila no son “más dbt”. Fan-out (un stg que alimenta 40 marts copiados) y profundidad inútil encarecen Slim CI: state:modified+ reconstruye todos los hijos. Prefiere reutilizar un int_ de verdad o aceptar un mart más ancho.
Seguridad (mínimo dbt)
- Secrets fuera del git (capítulo 7).
- Usuario dbt con el privilegio justo: crear en su schema, leer sources.
- CI aislado; defer lee prod, no la pisa.
- Seeds y logs sin PII.
debug/--printno vuelcanprofiles.yml.
Workflow de equipo
cambio SQL / YAML
↓
dbt compile / build local (target de desarrollo)
↓
PR
↓
CI: dbt build selectivo o completo, schema efímero
↓
review: SQL + grain + tests + lineage (no solo el YAML)
↓
merge
↓
job de producción + guardar manifest
↓
observar tiempos, tests, costeSi el review no mira el grain, el CI solo automatiza el desastre.
Errores comunes
intermediate/vacío “porque lo dice la guía”.not_nulldecorativo en columnas que el origen deja vacías a propósito.- Contract en todo el proyecto el segundo mes.
- Incremental sin plan de late-arriving.
- Macro por cada CTE.
- DAG profundo como señal de madurez.
Ejercicio
- Escribe el grain de
fct_ordersy destg_ordersen una línea cada uno. Si coinciden mal, parte o documenta el filtro. - Lista los tests de un mart y borra los que no defenderías en un incidente.
- Coge un modelo >200 líneas con dos facts implícitos y propón el corte.
- Recorre el workflow de arriba con el último cambio real: ¿dónde faltó
ls, test o schema de CI?
