Saltar al contenido principal

Cómo correr dbt nativo en Snowflake (sin dbt Cloud): lo que nadie te cuenta

Un relato de práctica sobre correr un proyecto dbt en producción con el runner nativo de Snowflake — incluyendo las cuatro restricciones que la documentación del vendor omite.

AC
Arturo Cárdenas
Fundador y Chief Data Analytics & AI Officer
20 de marzo de 2026 · Actualizado 20 de marzo de 2026 · 5 min de lectura
Cómo correr dbt nativo en Snowflake (sin dbt Cloud): lo que nadie te cuenta

Punto Clave

Puedes correr dbt dentro de Snowflake sin dbt Cloud. Lo que la documentación omite: las subconsultas correlacionadas fallan silenciosamente en tiempo de ejecución, los macros en PARTITION BY producen resultados incorrectos sin errores, la inferencia de tipos en seeds corrompe IDs y no hay desarrollo local. Este post documenta cada restricción, el fix y el macro que todo proyecto de dbt nativo en Snowflake necesita antes de que el primer modelo toque producción.

Busca "dbt sin dbt Cloud en Snowflake" y vas a encontrar documentación de vendors, comparativas de precios y algunos hilos de Reddit que se quedan a medias. Lo que no vas a encontrar es a alguien que haya corrido un proyecto dbt en producción de esta forma y haya documentado qué falló en la realidad.

Este es ese post.


La respuesta corta

Puedes correr dbt de forma nativa dentro de Snowflake sin dbt Cloud y sin instalar dbt Core localmente. El runner integrado de Snowflake maneja la compilación y ejecución a través de su integración nativa con git. Para equipos que ya están en Snowflake y prefieren mantener el stack simple, es una opción razonable.

Lo que la documentación del vendor omite: las subconsultas correlacionadas fallan silenciosamente en tiempo de ejecución, los macros dentro de funciones de ventana producen resultados incorrectos sin errores, la inferencia de tipos en seeds va a corromper tus IDs, y no puedes correr ni depurar nada localmente. El workflow es más lento. La red de seguridad es más delgada. Y necesitas un macro de seguridad de producción desde el día uno — no la segunda semana.


Por qué lo elegimos

Nos contrataron para modernizar la plataforma de analytics de una empresa de seguridad en la nube — migrar de un BI heredado a un stack moderno de dbt + Snowflake + Sigma en cinco meses. El equipo de datos ya tenía un entorno de Snowflake, Okta SSO, y ninguna disposición a gestionar otra relación con un vendor SaaS. dbt Cloud estaba sobre la mesa. También dbt Core independiente conectado a un runner de CI. Evaluamos ambos y elegimos la integración nativa de dbt en Snowflake.

El razonamiento: la historia de autenticación ya era compleja (más sobre eso en el post sobre auth de Okta + Snowflake). Agregar la capa OAuth de dbt Cloud encima de Okta SSO encima de las políticas de red de Snowflake era fricción que el equipo no necesitaba. El runner nativo mantenía los secretos en un solo lugar, usaba la integración de git existente y dejaba que el scheduler de Snowflake manejara los deployments. En papel: elegante. En la práctica: un catálogo de restricciones que tuvimos que descubrir y documentar nosotros mismos.


El catálogo de gotchas

1. Las subconsultas correlacionadas fallan en tiempo de ejecución — sin error de compilación

Este fue el primer fallo en producción. Claude Code, operando con patrones estándar de dbt, generó lookups temporales como subconsultas correlacionadas. Compilaron sin problema. Fallaron cuando Snowflake los ejecutó.

-- Falla en tiempo de ejecución en dbt nativo de Snowflake (sin error de compilación)
SELECT *,
  (SELECT rate
   FROM pricing_rates
   WHERE effective_date <= t.billing_date
   ORDER BY effective_date DESC
   LIMIT 1) as rate
FROM billing t

El reemplazo es el patrón JOIN + QUALIFY, que es idiomático en Snowflake y además es más rápido:

-- Funciona en dbt nativo de Snowflake — y rinde mejor
SELECT t.*, r.rate
FROM billing t
LEFT JOIN pricing_rates r
  ON r.effective_date <= t.billing_date
QUALIFY ROW_NUMBER() OVER (
  PARTITION BY t.billing_id
  ORDER BY r.effective_date DESC
) = 1

Una vez que documentamos esto en PATTERNS.md y CLAUDE.md, la IA dejó de generar el patrón roto por completo. Antes de documentarlo: cada lookup temporal era un fallo en tiempo de ejecución esperando el próximo push.

2. Los macros en PARTITION BY producen resultados incorrectos sin avisar

Peor que el problema de subconsultas correlacionadas, porque este no falla — simplemente calcula mal.

Cuando llamas a un macro de dbt dentro de la cláusula PARTITION BY u ORDER BY de una función de ventana, el runner nativo lo compila sin lanzar ningún error. La consulta corre. Los resultados son incorrectos. Lo descubrimos cuando las cifras de ARR trimestral dejaron de coincidir con la referencia conocida — la causa raíz era un macro de conversión de fechas que se estaba llamando dentro de PARTITION BY y producía particionamiento incorrecto.

-- NO hagas esto en dbt nativo de Snowflake (resultados incorrectos, sin error)
SELECT
  account_id,
  SUM(arr) OVER (
    PARTITION BY {{ fiscal_quarter(billing_date) }}
  ) as quarterly_arr
FROM arr_base

-- En cambio, escribe la lógica directamente
SELECT
  account_id,
  SUM(arr) OVER (
    PARTITION BY
      YEAR(billing_date),
      CEIL(MONTH(billing_date) / 3.0)
  ) as quarterly_arr
FROM arr_base

La regla que agregamos: ninguna llamada a macros dentro de cláusulas PARTITION BY ni ORDER BY. Escribe la lógica directamente, siempre.

3. La inferencia de tipos en seeds va a corromper tus IDs

El dbt nativo de Snowflake infiere tipos de columna a partir de los valores del CSV de seeds, y los infiere mal de maneras predecibles. El account ID 00123 se convierte en el entero 123. La precisión decimal se trunca silenciosamente. Las cadenas de fechas se parsean con formatos incorrectos.

La solución es declarar los tipos de columna explícitamente en seeds.yml — pero no vas a saber que lo necesitas hasta que estés depurando por qué los account lookups fallan para todo lo que tenga cero al inicio:

# seeds.yml — declara explícitamente cada tipo no obvio
version: 2

seeds:
  - name: pricing_rates
    config:
      column_types:
        account_id: varchar(20)
        region: varchar(50)
        rate_adjustment: number(5,4)
        effective_date: date
        product_tier: varchar(100)

Agregamos esto después de descubrir que 12 cuentas regionales con IDs de cero inicial estaban fallando silenciosamente en los lookups. Los datos estaban ahí. Los joins producían nulos. Los tipos explícitos en seeds.yml lo resolvieron por completo.

4. Sin desarrollo local — y lo que eso realmente implica

Con dbt nativo de Snowflake, no existe dbt run en tu laptop. El ciclo es: escribe código → haz commit → push → espera a que Snowflake lo tome → prueba en la UI → revisa resultados → inicia la siguiente sesión. No puedes correr dbt debug, dbt compile --select, ni dbt test localmente contra tu warehouse real.

Esto importa más de lo que parece. Depurar un modelo requiere un push completo. Un simple "¿esto compila?" requiere hacer commit. El tiempo total de iteración por cambio se mide en minutos, no en segundos.

La ventaja — y es real — es que no puedes subir código desechable. Cada prueba es un commit. El historial del repo queda limpio. "Funciona en mi máquina" es estructuralmente imposible. En un equipo de varios desarrolladores donde tres personas comparten un entorno de desarrollo, esa disciplina tiene valor.

Lo que significa en la práctica: prueba la lógica en bloques, no línea por línea. Escribe archivos de modelo completos antes de hacer push. Agrupa tus ciclos de depuración. Y documenta +persist_docs en dbt_project.yml — ya que no tienes el sitio de docs de dbt Cloud, las descripciones de columnas y modelos escritas en los metadatos nativos de Snowflake son la mejor alternativa:

# dbt_project.yml
models:
  your_project:
    +persist_docs:
      relation: true    # Las descripciones de modelos aparecen en la UI de Snowflake
      columns: true     # Las descripciones de columnas aparecen en la UI de Snowflake

Sigma y otras herramientas de BI que se conectan a Snowflake heredan estas descripciones automáticamente. No es un sitio de documentación completo. Pero es suficiente.

Mapa de restricciones de dbt nativo en Snowflake: dos columnas mostrando qué funciona (JOIN+QUALIFY, lógica de ventana inlínea, tipos de seed explícitos, validate_dev_target) vs qué se rompe (subconsultas correlacionadas, macro en PARTITION-BY, inferencia de tipos en seeds, sin dev local)

Comparación del ciclo de desarrollo: loop local estándar de dbt (segundos) vs workflow nativo de Snowflake (minutos por iteración, ciclo commit-push-test)


El macro de seguridad de producción

Lo más importante que puedes agregar a un proyecto de dbt nativo en Snowflake es un macro que evite que un desarrollador corra accidentalmente contra producción.

Sin desarrollo local, el único entorno es el entorno de Snowflake. Y el runner nativo de Snowflake ejecuta contra cualquier target que esté configurado. Si alguien prueba un cambio de modelo y su target es prod, corre en producción. Sin sandbox. Sin dry run. El modelo corre.

Lo resolvimos con un macro validate_dev_target que se llama al inicio de cada sesión:

{% macro validate_dev_target() %}
  {#- Lista de targets de desarrollo válidos -#}
  {% set valid_dev_targets = ['dev_alice', 'dev_bob', 'dev_shared'] %}

  {#- Captura targets de dev desconocidos -#}
  {% if target.name.startswith('dev_') and target.name not in valid_dev_targets %}
    {{ exceptions.raise_compiler_error(
      "Target desconocido: '" ~ target.name ~ "'\n" ~
      "Targets válidos: " ~ valid_dev_targets | join(', ') ~ "\n" ~
      "¿Olvidaste agregarlo a profiles.yml?"
    ) }}
  {% endif %}

  {#- Bloquea runs de prod fuera de CI/CD -#}
  {% if target.name == 'prod' %}
    {% set is_ci = env_var('CI', 'false') %}
    {% if is_ci != 'true' %}
      {{ exceptions.raise_compiler_error(
        "¡El target PROD solo puede correr en CI/CD!\n" ~
        "   Target actual: " ~ target.name ~ "\n" ~
        "   Usa un target dev_* para desarrollo."
      ) }}
    {% endif %}
  {% endif %}

  {#- Confirma con log -#}
  {% do log("¡Validación de target exitosa!", info=True) %}
  {% do log("Target: " ~ target.name, info=True) %}
  {% do log("Database: " ~ target.database, info=True) %}
  {% do log("Schema: " ~ target.schema, info=True) %}

{% endmacro %}

Este macro hace dos cosas: primero, detecta targets de dev no reconocidos — el mensaje de error te dice exactamente qué está mal y cómo resolverlo. Segundo, bloquea cualquier run de prod a menos que la variable de entorno CI esté en 'true', lo que solo ocurre en el pipeline de deployment automatizado de Snowflake.

Esto corre en tiempo de compilación. Si alguien está mal configurado, la sesión falla antes de tocar los datos. En los cinco meses del engagement, tuvimos cero escrituras accidentales en producción. No es suerte — es un macro.


Los patrones específicos de Snowflake que realmente ayudan

Una vez que aceptas las restricciones, las funcionalidades SQL específicas de Snowflake se vuelven útiles. Tres que se ganaron su lugar:

QUALIFY para filtrar resultados de funciones de ventana — más limpio que envolver todo en una subconsulta, y es lo que hace funcionar el patrón JOIN + QUALIFY:

-- Una fila por cuenta, el período de facturación más reciente
SELECT account_id, billing_date, arr
FROM usage_data
QUALIFY ROW_NUMBER() OVER (
  PARTITION BY account_id
  ORDER BY billing_date DESC
) = 1

TRY_CAST para conversiones de tipo defensivas — devuelve NULL en caso de fallo en lugar de lanzar un error, lo que importa cuando estás migrando datos de un sistema heredado con tipos inconsistentes:

-- Conversión numérica segura — NULL en caso de fallo, no un error
TRY_CAST(legacy_account_id AS NUMBER) AS account_id_num

EQUAL_NULL para comparaciones de conjuntos — maneja NULL = NULL correctamente, cosa que el SQL estándar no hace. Esencial cuando comparas filas entre sistemas donde columnas nullables forman parte del grain:

-- Maneja NULL = NULL correctamente en detección de cambios
WHERE NOT EQUAL_NULL(source.discount_rate, target.discount_rate)

Ninguno de estos aparece en el tutorial estándar de dbt. Los tres eliminaron bugs reales del código base.


Qué haríamos diferente

Empezar el catálogo de gotchas desde el día uno. Descubrimos cada restricción durante un ciclo de push, lo que significa que aprendimos de la forma cara — escribir código, commit, push, fallar en la UI de Snowflake, entender qué se rompió, corregir, repetir. Para la segunda semana teníamos cuatro reglas fijas en CLAUDE.md y PATTERNS.md. Esas reglas deberían haber estado desde el inicio del proyecto.

Lo segundo: tener los tipos de columna en seeds.yml correctos antes de escribir cualquier modelo que haga join a un seed. El problema de corrupción por inferencia de tipos se multiplica. Si tus account IDs son enteros cuando deberían ser strings, cada modelo que hace join en account ID está produciendo resultados silenciosamente incorrectos. No vas a ver ningún error. Vas a ver números equivocados en tu herramienta de BI y vas a pasar horas buscando en el lugar equivocado.

Para el panorama completo sobre las restricciones de autenticación que moldearon este workflow — Okta SSO, whitelisting dinámico de IPs, el baile de permisos con el equipo de infraestructura — lee el post complementario sobre seguridad empresarial y velocidad del desarrollador.

Para ver cómo usamos este entorno para correr un sprint asistido por IA de 9 días con 265,638 filas validadas, lee Analytics Engineering Asistido por IA: Cómo Claude Code cambió nuestro workflow de dbt.


Mapa de restricciones de dbt nativo en Snowflake: cuatro cuadrantes mostrando errores en tiempo de compilación (arriba izquierda, gris), fallos en tiempo de ejecución (arriba derecha, rojo), corrupción silenciosa de datos (abajo derecha, ámbar) y restricciones de workflow (abajo izquierda, pizarra), con cada gotcha graficado por severidad y detectabilidad

Comparación del ciclo de desarrollo de dbt nativo de Snowflake vs dbt estándar: dos carriles horizontales mostrando el loop de iteración local de dbt estándar vs el ciclo push-commit-test del nativo, con anotaciones de tiempo en cada paso


Preguntas frecuentes

¿El dbt nativo de Snowflake es lo mismo que dbt Core?

No. El runner integrado de Snowflake usa dbt Core por dentro, pero se ejecuta dentro del entorno gestionado de Snowflake. No instalas dbt localmente, no hay profiles.yml en tu máquina y no puedes correr dbt run desde una terminal. La compilación ocurre en el pipeline de Snowflake, activada por commits de git o tareas programadas. Las restricciones documentadas aquí — fallos de subconsultas correlacionadas, inferencia de tipos en seeds, sin ejecución local — son específicas de este runtime gestionado, no de dbt Core en general.

¿dbt Cloud tiene las mismas restricciones?

No. La mayoría de estas restricciones son específicas del runner nativo de Snowflake, no de dbt Cloud. dbt Cloud soporta desarrollo local, tiene su propio sitio de docs y corre dbt fuera del entorno de ejecución de Snowflake. Si tienes la opción, dbt Cloud es el camino más flexible. La razón para elegir el runner nativo es una integración más estrecha con Snowflake, un vendor menos que gestionar y una autenticación que permanece dentro de tu trust boundary existente de Snowflake.

¿Cómo manejan la complejidad de RBAC en entornos Snowflake con múltiples desarrolladores?

Cada desarrollador tiene una base de datos aislada — DBT_DEV__<NOMBRE> — para que el trabajo de desarrollo nunca toque los schemas compartidos. El macro validate_dev_target evita runs accidentales a producción en la capa de compilación. Los deployments a producción pasan por un service role con una estructura de grants separada. El patrón completo de RBAC — jerarquías de roles, grants de warehouse, acceso a schemas para service roles — es su propio tema, cubierto en El Costo Oculto del RBAC de Snowflake.

¿Qué pasa con la documentación de dbt si no hay sitio de docs de dbt Cloud?

+persist_docs escribe las descripciones de modelos y columnas directamente en los metadatos nativos de Snowflake. La UI de Snowflake las muestra en el browser de schemas. Herramientas de BI como Sigma heredan las descripciones de columnas automáticamente. No es tan completo como el sitio de docs de dbt Cloud — pierdes el lineage graph y el portal de documentación con búsqueda — pero para equipos que ya están muy metidos en Snowflake, cubre la mayoría de los casos de uso del día a día. La pérdida más importante es no poder correr dbt docs generate y dbt docs serve localmente para explorar el lineage de forma interactiva.

¿Todo proyecto de dbt en Snowflake debería usar el macro validate_dev_target?

Sí, si tienes más de un desarrollador y un target de producción. El riesgo es bajo en un proyecto de una sola persona con un único entorno. Pero en cualquier proyecto con targets de dev compartidos, un pipeline de deployment a producción y datos reales en producción — lo que describe casi cualquier proyecto comercial de dbt — un target mal configurado eventualmente va a correr contra producción. El macro tarda 37 líneas en escribirse y no cuesta nada llamarlo. El escenario que previene cuesta mucho más que eso para recuperarse.


La documentación de dbt nativo en Snowflake te dice cómo configurarlo. No te dice qué se rompe, por qué se rompe silenciosamente ni qué necesitas antes de que el primer modelo toque producción. Para eso está este catálogo.

El workflow es más lento que el desarrollo estándar de dbt. Las restricciones son reales. También lo son las ventajas: historial de commits limpio, autenticación que permanece en un solo trust boundary y un pipeline de deployment que vive completamente dentro de tu inversión existente en Snowflake. Si ese tradeoff tiene sentido depende de tu equipo. Si lo eliges, arranca con el catálogo de restricciones ya escrito.

Una vez que dbt corre de forma nativa en Snowflake, Cortex Analyst es la siguiente pregunta — lo probamos con datos financieros de producción.


¿Estás considerando correr dbt de forma nativa en Snowflake? Podemos ayudarte a evaluar los tradeoffs antes de que te topes con las limitaciones. Agenda una consulta rápida.

Temas

dbt nativo snowflakedbt sin dbt cloudsnowflake dbt coresubconsulta correlacionada snowflake dbtinferencia tipos seeds snowflakemacro validate_dev_targetbug macro PARTITION BY dbtsnowflake dbt sin desarrollo local
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.