Saltar al contenido principal

De 377 objetos heredados a 51 modelos de dbt: una arquitectura de migración que realmente escala

Cómo redujimos 377 objetos de BI heredados a 51 modelos de dbt — y la arquitectura con aislamiento de dominios que los escaló a 161 sin colapsar.

AC
Arturo Cárdenas
Fundador y Chief Data Analytics & AI Officer
20 de marzo de 2026 · Actualizado 20 de marzo de 2026 · 10 min de lectura
De 377 objetos heredados a 51 modelos de dbt: una arquitectura de migración que realmente escala

Punto Clave

La mayoría de las guías de migración a dbt asumen que empiezas desde cero. Nosotros empezamos con 377 vistas, stored procedures y queries programadas — algunas sin documentar por años. Este post documenta el framework de reducción: elimina el código muerto, consolida objetos en el mismo grain, aísla dominios antes de escalar. La arquitectura resultante llevó el proyecto de 51 modelos al lanzamiento a 161 en 7 dominios sin ninguna ruptura entre dominios.

La documentación de dbt sobre estructura de proyectos es buena. Asume que estás empezando desde cero.

La mayoría de la gente no está en esa situación. Están empezando con 377 vistas, stored procedures y queries programadas que nadie entiende del todo — algunas construidas hace tres años, algunas el trimestre pasado, la mayoría sin documentar. La guía para ir desde ahí a una arquitectura limpia de tres capas no existe, o si existe, vive en charlas de conferencias y hilos de Slack difíciles de encontrar.

Así lo hicimos nosotros en un engagement de 5 meses con una empresa de seguridad en la nube. Pasamos de 377 objetos heredados a 51 modelos de dbt en el primer lanzamiento, y luego crecimos de forma deliberada hasta 161 modelos en 7 dominios. La reducción fue la arquitectura — y requirió un framework para decidir qué conservar, qué consolidar y qué eliminar.


Respuesta rápida: el framework de reducción

Si estás aquí por el resumen táctico:

  1. Audita todo. Categoriza cada objeto heredado por tipo (vista, procedimiento, query), dominio de datos y si algo lo consume aguas abajo.
  2. Elimina el código muerto primero. En la mayoría de los sistemas heredados, el 20–30% de los objetos son huérfanos. Mátalos antes de planear cualquier cosa.
  3. Mapea los objetos restantes al modelo de tres capas. Staging: 1:1 con tablas fuente. Intermediate: joins y lógica de negocio. Marts: outputs para BI.
  4. Consolida objetos que representan el mismo grain. Si tres vistas producen tres variantes de "registros de metering para la región X", se convierten en un modelo de staging con un filtro WHERE o un parámetro de macro.
  5. Aísla dominios antes de escalar. Fija los límites de tu schema (slv_<dominio> / gld_<dominio>) antes de escribir el primer modelo. Retrofitear el aislamiento de dominios en un proyecto en funcionamiento es doloroso.

Ese es el framework. El resto de este post es el detalle detrás de cada paso y las decisiones de arquitectura que determinaron cómo construimos cuando el proyecto escaló de 51 a 161 modelos.


El desorden heredado: contexto antes que estructura

La empresa de seguridad en la nube con la que trabajamos operaba un sistema de metering y analytics de ingresos a gran escala. Sus productos cloud reportaban datos de uso desde 12 regiones — algunas lo suficientemente grandes para tener pipelines de datos dedicados, otras lo suficientemente pequeñas para compartir un schema con un filtro WHERE. Los cálculos de ingresos involucraban precios escalonados, multiplicadores regionales, descuentos por cuenta y un calendario fiscal que no coincide con los trimestres ISO.

Todo esto vivía en herramientas de BI heredadas: una mezcla de vistas de base de datos, lógica de transformación embebida en queries de dashboards y macros de Jinja con constantes de tarifas hardcodeadas. 377 objetos en total.

El problema inmediato no era que el sistema estuviera mal — producía los números correctos la mayor parte del tiempo. El problema era que nadie podía explicar por qué producía esos números. Descubrimos cambios de precios que solo existían a nivel de base de datos sin rastro en el control de versiones — el tipo de opacidad acumulada que hace necesaria la migración. La historia completa está en el caso de estudio de la migración.

Arquitectura antes y después: el panel izquierdo muestra 377 objetos heredados como cajas superpuestas caóticas con conexiones enredadas, el panel derecho muestra 51-161 modelos organizados en 7 columnas de dominio a través de las capas staging, intermediate y marts


La reducción: de 377 a 51

La primera pregunta era qué eliminar.

Corrimos una auditoría de dependencias en cada objeto heredado. Cualquier cosa sin consumidores aguas abajo en los últimos 90 días era candidata para eliminación. Solo ese paso eliminó aproximadamente 80 objetos — vistas que habían sido reemplazadas, queries exploratorias abandonadas en schemas compartidos, tablas de staging de migraciones que nunca se completaron.

Después: consolidación. Doce pipelines regionales se habían construido uno a la vez, cada uno ligeramente diferente. Existían tres variaciones del mismo cálculo de ARR para diferentes contextos de reporte. Dos dashboards que Finance pensaba que eran productos separados jalaban de la misma vista subyacente con diferentes alias de columnas.

La regla de consolidación: si múltiples objetos heredados producen el mismo grain para el mismo dominio, se convierten en un modelo de dbt. Las variaciones regionales se convierten en filtrado parametrizado. Las variaciones de reporte se convierten en columnas de mart, no en tablas separadas.

Después de la eliminación y consolidación, los 51 objetos restantes se mapearon limpiamente a un modelo de tres capas:

  • 29 modelos de staging — uno por entidad fuente, nada más
  • 12 modelos intermediate — joins, deduplicación, lógica de negocio
  • 10 marts — tablas de hechos y dimensiones orientadas a BI

Esa es la arquitectura inicial. El aislamiento de dominios y la estrategia de materialización fueron las siguientes decisiones que determinaron si 51 modelos podían escalar a 161 sin colapsar bajo su propio peso.


Aislamiento de dominios: la decisión que escaló

La estructura del proyecto de dbt que emergió se ve así:

models:
  metering_dbt_models:
    +persist_docs:
      relation: true
      columns: true

    staging:
      +materialized: view
      +schema: slv_general
      product_alpha:
        +schema: slv_product_alpha
      product_utilization:
        +schema: slv_product_utilization

    intermediate:
      +materialized: view
      +schema: slv_general
      product_alpha:
        +schema: slv_product_alpha
      platform_analytics:
        +schema: slv_product_platform
      risk_analytics:
        +schema: slv_product_risk

    marts:
      +materialized: table
      +schema: gld_general
      product_alpha:
        +schema: gld_product_alpha
      product_utilization:
        +schema: gld_product_utilization
      product_platform:
        +schema: gld_product_platform
      product_risk:
        +schema: gld_product_risk

Las decisiones clave en esta estructura:

Schemas separados por dominio, no por capa. Staging e intermediate de product_alpha van a slv_product_alpha. Los marts de product_alpha van a gld_product_alpha. Cuando el equipo de Finance consulta datos de staging, sabe en qué namespace de schema buscar. Cuando se agregó un nuevo dominio (risk_analytics) tres meses después, obtuvo su propio par de schemas sin tocar nada existente.

Naming Silver/Gold como contrato, no como preferencia. slv_* significa "limpio, conforme, aún no agregado". gld_* significa "desnormalizado, listo para BI, materializado como tabla". Los usuarios de Snowflake y los workbooks de Sigma se conectan únicamente a gld_*. El naming se hace cumplir por el macro de schema personalizado — no es solo una convención en un README.

persist_docs en la raíz del proyecto. Cada descripción de modelo fluye automáticamente a los metadatos de Snowflake. Parece menor hasta que estás en el mes cuatro y alguien de un dominio que tú no construiste pregunta por qué una columna es nula en un caso específico. La respuesta está en el DAG viewer.

Lo que habilita todo es un override del macro generate_schema_name que enruta los modelos al nombre de schema exacto especificado en dbt_project.yml — no el comportamiento predeterminado de dbt que antepone el schema de target (convirtiendo slv_product_alpha en prod_slv_product_alpha). Esto mantiene la estructura de schemas predecible en targets de dev, staging y prod.

Estructura de capas: tres bandas horizontales (staging 63 modelos, intermediate 50, marts 82) divididas en 7 columnas de dominio con conteos de modelos por celda


Estrategia de materialización: dónde se rompen las vistas

La posición predeterminada es "vistas para todo lo que no sea costoso". Es el punto de partida correcto. El quiebre ocurre en las uniones y los joins anchos.

Árbol de decisión de materialización: ruta verde muestra vistas para staging/intermediate simple, ephemeral para pasos de solo unión, tabla para agregaciones costosas y outputs de mart orientados a BI — con anotaciones de costo de almacenamiento y cómputo de Snowflake

Ephemeral para modelos de solo unión. Teníamos 12 modelos de staging regionales que necesitaban unirse antes de la deduplicación. Hacer el modelo de unión ephemeral significa que tiene costo de almacenamiento cero y cero entrada en el schema — es solo SQL que se inserta directamente en el modelo de dedup aguas abajo en tiempo de compilación.

{{ config(materialized='ephemeral') }}

select provider, region, customer_id, instance_id, start_date, end_date,
       plan_id, node_count, container_count, /* ... 57 columnas en total */
       status_code, retry_count
from {{ ref('stg_product_alpha__metering_us_east') }}
union all
select /* mismas columnas */ from {{ ref('stg_product_alpha__metering_us_south') }}
union all
-- 10 refs regionales más

57 columnas, 12 regiones, cero overhead de almacenamiento.

Tablas para modelos de deduplicación y enriquecimiento que alimentan 10+ modelos aguas abajo. El modelo de dedup se mantiene como vista — lo consume un solo modelo aguas abajo. El modelo de enriquecimiento de cuentas está materializado como tabla porque cada mart del dominio lo consume. El detalle clave: las partition keys deben coincidir exactamente con el sistema heredado para paridad en la validación, y encontrar la partition key correcta tomó dos iteraciones después de descubrir instancias reportando a múltiples regiones simultáneamente.


La lógica de negocio que la migración saca a la luz

La parte más valiosa de una migración a dbt no es la arquitectura — es la lógica de negocio que antes era invisible. El modelo de enriquecimiento de cuentas lo ilustra: las cuentas sin registro de clasificación se manejaban con reglas basadas en fecha sin documentar en el sistema heredado. La migración las sacó a la luz.

{{ config(materialized='table') }}

select
  u.*,
  date_trunc('month', u.start_date)::date as period_month,
  coalesce(a.account_type, '-') as account_type,
  coalesce(a.is_poc, '-') as is_poc,

  case
    when coalesce(a.account_type, '-') = 'INTERNAL' then 'Internal'
    when coalesce(a.account_type, '-') = 'EXTERNAL'
     and coalesce(a.is_poc, '-') = 'Yes' then 'External POC'
    when coalesce(a.account_type, '-') = 'EXTERNAL'
     and coalesce(a.is_poc, '-') = 'No' then 'External Upside'

    -- Cuentas huérfanas: coincide con el tratamiento basado en fecha del sistema heredado.
    -- Período ELA pre-2023 (Jul 2021 - Dic 2022): tratar huérfanas como External POC.
    when date_trunc('month', u.start_date) >= '2021-07-01'
     and date_trunc('month', u.start_date) < '2023-01-01'
      then 'External POC'

    when coalesce(a.account_type, '-') = '-' then 'Unknown'
    else 'Unknown'
  end as account_segment

from filtered_usage u
left join account_inventory a
  on u.account_id = a.account_id
  and u.plan_name = a.plan_name
  and date_trunc('month', u.start_date)::date = a.period

Este es uno de los 15 bugs que encontramos y corregimos durante la migración. La metodología completa de reconciliación está en el caso de estudio de la migración.


Escalar de 51 a 161: el patrón de crecimiento por dominio

La arquitectura escaló por adición de dominios, no por proliferación de modelos.

FaseModelosDominios agregados
Fase 1 (mes 1)~34Metering core (product_alpha)
Fase 2 (mes 2)~50Utilización de producto, datos de negocio
Fase 3 (mes 3)~90Precios FY27, modelos v2, dimensiones
Fase 4 (meses 4–5)161Platform, risk, components, segment

Cada nuevo dominio obtuvo su propio par de schemas y su propia carpeta en models/staging/, models/intermediate/ y models/marts/. Ningún modelo existente fue modificado. Los nuevos dominios no podían romper los existentes porque el aislamiento de schemas es estructural, no convencional.

Para cambios de lógica importantes — específicamente el rebuild de precios FY27 — corrimos modelos v1 y v2 en paralelo, comparando outputs fila por fila antes de deprecar v1. Ese sprint está cubierto en el post de precios FY27.

Diagrama de crecimiento de dominios: cuatro fases horizontales, cada fase muestra una nueva columna de dominio agregada a la izquierda del bloque de schema existente — sin modificaciones a los schemas existentes, solo adiciones

El desglose de materialización en 161 modelos: 29 staging (vistas), 50 intermediate (vistas + ephemeral), 82 marts (tablas + vistas de dimensiones). La proporción de aproximadamente 1:1.7:2.8 entre las tres capas es un prior razonable para un sistema de metering multidominio.


Preguntas frecuentes

¿Cómo decides qué va en intermediate vs. marts?

La regla que usamos: los modelos intermediate pueden ser consumidos por otros modelos intermediate o por marts, pero nunca contienen columnas diseñadas para display en BI. Si estás agregando una columna formatted_revenue o una etiqueta de display, eso va en un mart. Los modelos intermediate deben contener el grain, las llaves y la lógica de negocio que los modelos aguas abajo necesitan — no la capa de presentación. La prueba práctica: ¿podría un mart consumir este modelo sin ningún join? Si la respuesta es sí, probablemente es un mart. Si aún necesita joins para ser útil, es intermediate.

¿Cuándo se rompe el modelo de tres capas?

Se complica con dependencias altamente cíclicas — casos donde el dominio A enriquece al dominio B y el dominio B clasifica al dominio A. Vimos una versión de esto con los segmentos de cuenta (derivados del dominio de account inventory) alimentando el dominio de ingresos de metering. El fix fue hacer que la derivación del segmento de cuenta fuera autosuficiente dentro de un único modelo intermediate en lugar de una dependencia entre dominios. Si te encuentras queriendo un ref() que cruza límites de dominio en la capa de staging, eso suele ser una señal de que tus límites de dominio están mal trazados.

¿Cómo manejan la separación de schemas dev/prod sin dbt Cloud?

Usamos bases de datos por desarrollador y un macro validate_dev_target que previene runs de producción a menos que el flag de CI esté activado. El schema de cada desarrollador es dev_<nombre>_slv_product_alpha en lugar de slv_product_alpha. El macro generate_schema_name maneja el enrutamiento. La restricción clave en dbt nativo de Snowflake (sin dbt Cloud) es que no hay workflow de desarrollo local — cada prueba de modelo requiere un push y ejecución en Snowflake. Eso hace que las bases de datos por desarrollador sean más importantes, no menos, porque no puedes validar localmente primero.

¿Cuál es el número correcto de modelos para un proyecto de este tamaño?

161 modelos para 7 dominios en 5 meses está en el extremo alto, pero el conteo de dominios lo impulsó más que la complejidad. El dominio core de metering (product_alpha) por sí solo representa 29 staging + ~20 intermediate + ~30 marts — aproximadamente 79 modelos para un dominio que tenía 12 pipelines regionales y 31 macros de precios. Un proyecto de un solo dominio de complejidad similar terminaría en el rango de 60–80 modelos. Si tienes 200+ modelos y sigues en un solo dominio, eso suele ser señal de que los modelos intermediate están haciendo demasiado trabajo que debería dividirse en modelos más pequeños y enfocados.

¿Cómo previenen el schema drift cuando se unen nuevos ingenieros al proyecto?

Dos mecanismos: dbt_project.yml aplica el enrutamiento de schemas a nivel de configuración, así que un modelo nuevo en models/staging/product_alpha/ automáticamente aterriza en slv_product_alpha sin importar lo que alguien escriba en el config del modelo. Y CLAUDE.md — el archivo de instrucciones para IA del proyecto — tiene las convenciones de naming y las reglas de schema escritas explícitamente tanto para contribuidores humanos como para IA. Cualquier sesión que empiece leyendo CLAUDE.md tiene las convenciones antes de tocar código. Esa combinación cubre tanto el drift humano como el drift de IA con el mismo mecanismo.


377 objetos se convirtieron en 51 porque la mayoría no necesitaban existir. Los que sí lo necesitaban se mapean limpiamente a tres capas — y una vez que el aislamiento de dominios está en su lugar, agregar los otros 110 modelos se da por sí solo.

El aislamiento por dominio es un patrón central en nuestro enfoque de Unified Data Foundations. La historia completa de cómo esta migración creció de un dominio a siete está en Crecimiento de Alcance Bien Hecho.


Si tu proyecto de dbt heredó años de objetos legacy y no sabes qué conservar, ya construimos el framework de reducción. Empieza con una revisión de arquitectura.

Temas

arquitectura migración dbtestructura proyecto dbtaislamiento dominios dbtmigración bi heredado dbtarquitectura tres capas dbtdbt staging intermediate martsaislamiento schema snowflake dbtframework reducción modelos dbt
Compartir este artículo:
AC

Arturo Cárdenas

Fundador y Chief Data Analytics & AI Officer

Arturo es un consultor senior en analítica e IA que ayuda a empresas medianas y grandes a eliminar el caos de datos para desbloquear claridad, velocidad y ROI medible.

¿Listo para convertir datos en decisiones?

Hablemos de cómo lograr ROI medible en meses.