Skip to main content
Esta guía recorre los aspectos específicos de ClickHouse en dbt usando el proyecto Jaffle Shop for ClickHouse, la adaptación a ClickHouse del clásico proyecto de ejemplo de dbt Labs. Partiendo de un proyecto que ya se construye correctamente, muestra cómo:
  1. Comprender cómo se materializan en ClickHouse las views y tables del proyecto.
  2. Cargar datos con seeds y controlar los tipos de ClickHouse y la disposición de la tabla.
  3. Configurar un modelo de tipo table con un motor de ClickHouse, una sorting key y particionamiento.
  4. Convertir una table en un modelo incremental y elegir una incremental strategy.
  5. Crear un snapshot.
  6. Usar vistas materializadas de ClickHouse.
Está pensada para leerse junto con el resto de la documentación, la página de funcionalidad y configuración y la referencia de materializaciones.

Antes de empezar

Sigue primero el README de ClickHouse/jaffle-shop-clickhouse. Allí se explica cómo configurar el proyecto con dbt Core 1.x, dbt OSS, dbt v2 o la plataforma dbt, cómo apuntarlo a un ClickHouse local (docker) o a ClickHouse Cloud, cómo cargar los datos de ejemplo con dbt seed y cómo ejecutar el primer dbt build. Una vez que dbt build finalice correctamente, vuelve aquí para ver los ejemplos y configuraciones específicos de ClickHouse. Tras seguir los pasos del README deberías tener dos bases de datos en ClickHouse:
  • raw: las seis tablas de origen cargadas desde CSV files mediante dbt seed (raw_customers, raw_orders, raw_items, raw_products, raw_stores, raw_supplies).
  • jaffle_shop (el schema de tu profile): seis vistas de staging (stg_*) y siete tablas de mart (customers, orders, order_items, products, locations, supplies, metricflow_time_spine).
Si tu profile usa un schema distinto, sustituye jaffle_shop por tu valor en las consultas que aparecen a continuación.
dbt Core 1.x, dbt OSS, dbt v2 y la plataforma dbt. Todos los comandos y modelos de esta guía son idénticos en todos ellos. Los ejemplos se probaron con dbt Core 1.12 y dbt-clickhouse 1.10, y con dbt OSS 2.0, contra ClickHouse 26.8; dbt v2 ejecuta el mismo adaptador, y la plataforma dbt ejecuta dbt v2. La salida de consola que se muestra corresponde a dbt Core 1.x, y se señalan los pocos casos en los que los motores se comportan de forma distinta. Consulta la página de dbt OSS, dbt v2 y la plataforma dbt para conocer el estado actual del adaptador v2, y Connect ClickHouse en la documentación de dbt para empezar a usar la plataforma dbt.
Todas las sentencias SQL que no sean comandos de dbt están pensadas para ejecutarse directamente contra ClickHouse, por ejemplo con clickhouse client, la SQL console de ClickHouse Cloud o el client SQL que prefieras.

Cómo se materializa el proyecto

Jaffle Shop define sus materializaciones en dbt_project.yml: los modelos de staging son vistas y los marts son tablas.
Un model de tipo view se reconstruye con una sentencia CREATE OR REPLACE VIEW en cada ejecución. No almacena datos, por lo que construirlo no tiene coste alguno, pero cada consulta que se realiza sobre él ejecuta el SQL del model sobre las source tables. ClickHouse conserva el SQL compilado del model en la view definition:
Un modelo table se reconstruye desde cero en cada ejecución: el adaptador crea una tabla nueva, ejecuta un INSERT INTO ... SELECT con el SQL del modelo y la intercambia atómicamente con la versión anterior. El rendimiento de las consultas es mucho mejor que con una vista, a costa del almacenamiento y de tener que reconstruir toda la tabla cada vez. Observa la tabla que dbt creó para el mart orders:
Aquí hay dos aspectos específicos de ClickHouse. El modelo no declara ningún motor de tabla, por lo que el adaptador usa MergeTree, y tampoco declara una sorting key, por lo que el adaptador usa ORDER BY tuple(), es decir, los datos no se ordenan en absoluto. Esto es aceptable en un proyecto de ejemplo, pero en una tabla real conviene definir ambos, que es justamente lo que se hace en las secciones siguientes. La página de materializaciones enumera todas las configuraciones de tabla que admite el adaptador.

Carga de datos con seeds

Jaffle Shop utiliza los seeds de dbt para cargar sus datos sin procesar desde los archivos CSV de seeds/jaffle-data. Los seeds están pensados para datos de referencia pequeños y estáticos (tablas de códigos, correspondencias), no para cargar un warehouse; el proyecto los usa por comodidad, para que puedas empezar sin necesidad de otra herramienta de ingestión, y por eso están deshabilitados a menos que pases --vars '{"load_source_data": true}'. Aun así, los seeds son un buen punto de partida para aprender cómo dbt crea tablas de ClickHouse. dbt infiere un tipo de columna para cada columna del CSV, y los tipos inferidos difieren entre motores: Cuando el tipo importa, fíjalo con column_types. El proyecto ya lo hace para la columna opened_at del seed raw_stores en dbt_project.yml:
Los seeds también aceptan las configuraciones de tabla de ClickHouse engine, order_by y partition_by. Por ejemplo, para ordenar el seed raw_orders por la hora del pedido y particionarlo por mes, añada un archivo de propiedades junto a los CSV: seeds/jaffle-data/_raw_orders.yml:
Utilice un archivo de propiedades para estas configuraciones de seed de ClickHouse en lugar de las claves +order_by o +engine dentro de seeds: en dbt_project.yml. dbt Core 1.x acepta ambas formas, pero dbt v2 solo las reconoce en un archivo de propiedades y rechaza las claves de dbt_project.yml con el mensaje Unrecognized key ... Custom keys must go under +meta.
Vuelva a cargar ese seed y compruebe la tabla resultante:
dbt seed --full-refresh elimina y vuelve a crear la tabla, por lo que debes ejecutarlo antes de crear cualquier elemento que dependa directamente de los datos de la tabla, como la vista materializada que se describe más adelante en esta guía.

Configuración de una tabla para ClickHouse

El mart orders es el punto de partida natural: lo consultan el mart customers y las métricas del proyecto, y es una tabla de tipo evento con un timestamp. Añade un bloque config al principio de models/marts/orders.sql para elegir el motor, la clave de ordenación y un esquema de particionado:
El resto del model permanece igual. materialized='table' repite lo que dbt_project.yml ya indica para los marts, lo que mantiene el model self-describing cuando más adelante lo cambies a incremental. Reconstruye únicamente este model:
Ahora la tabla tiene una sorting key adecuada y una partición por mes:
Además de engine, order_by y partition_by, los modelos de tipo tabla aceptan primary_key, ttl, settings, query_settings, projections e indexes, y las columnas pueden llevar codec y ttl a través de un contrato de modelo. Todos ellos se describen en la página de materializaciones.

Creación de un modelo incremental

Reconstruir orders desde cero en cada ejecución resulta aceptable para 62.000 filas, pero no para una tabla que crece en millones de filas al día. La incremental materialization de dbt solo procesa las filas que cambiaron desde la última ejecución. Para convertir el modelo orders hacen falta dos añadidos:
  1. unique_key: la columna que identifica una fila, en este caso order_id. El adaptador la utiliza para reemplazar las filas que se vuelven a procesar en lugar de duplicarlas.
  2. Un filtro incremental: una cláusula where envuelta en {% if is_incremental() %} que selecciona únicamente las filas que se van a procesar. Se aplica en las ejecuciones incrementales, pero no cuando la tabla se crea por primera vez (ni cuando se reconstruye con --full-refresh). Los pedidos llevan un timestamp, de modo que el filtro compara ordered_at con el valor más reciente que ya existe en la tabla, al que se hace referencia mediante la variable {{ this }}.
Actualiza models/marts/orders.sql para que el bloque config y el final del modelo queden así:
stg_orders trunca ordered_at al día, por lo que el filtro usa >=: en cada ejecución se vuelve a procesar todo el último día y, gracias a unique_key, las filas ya cargadas se reemplazan en lugar de duplicarse. Eso es lo que hace que sea seguro para los pedidos que llegan más tarde ese mismo día. Ejecuta el modelo. La tabla ya existe, así que esta primera ejecución ya es incremental: solo se reprocesa el último día.
Ahora agreguemos algunos datos nuevos. Los datos de Jaffle Shop terminan en agosto de 2025, así que introducimos un nuevo cliente, Clicky McClickHouse, que pidió un jaffle ayer. Inserta un cliente, un pedido y su línea de pedido en las tablas sin procesar:
El id de la tienda es Philadelphia, el artículo es un jaffle nutellaphone who dis? a 11.00 y el impuesto es el 6 % de Philadelphia, por lo que las pruebas de datos del proyecto siguen superándose. Ejecute todo el proyecto para que las vistas de staging y la tabla order_items vean las nuevas filas antes que orders:
El nuevo pedido está en la tabla incremental y el mart customers, reconstruido a partir de ella, ya reconoce al nuevo cliente:

Internals

El registro de consultas de ClickHouse muestra las sentencias que ejecutó el adaptador para la actualización incremental:
La estrategia incremental predeterminada del adaptador funciona de la siguiente manera. En los diagramas de esta sección, una flecha que va de una tabla a una sentencia significa que la sentencia lee esa tabla; una flecha que va de una sentencia a una tabla significa que escribe en ella, la modifica, le cambia el nombre o la elimina:
  1. Se crea una tabla orders__dbt_new_data y se inserta en ella el SQL del model, incluido el filtro incremental. En la ejecución anterior se escribieron 378 filas: los 377 pedidos del último día ya cargados más el nuevo.
  2. Se crea una tabla orders__dbt_tmp con la misma structure que orders y se copian en ella todas las filas de orders cuyo order_id no esté en orders__dbt_new_data.
  3. Todas las filas de orders__dbt_new_data se insertan en orders__dbt_tmp. Los pasos 2 y 3 son los que reemplazan las filas del último día en lugar de duplicarlas.
  4. Se elimina orders__dbt_new_data.
  5. orders__dbt_tmp se intercambia con orders mediante una sentencia atómica EXCHANGE TABLES (a través de un cambio de nombre intermedio a orders__dbt_backup), de modo que orders pasa a contener la nueva versión.
  6. Se elimina la versión antigua.
El paso 2 copia la tabla completa, por lo que en modelos muy grandes esta estrategia resulta tan costosa como reconstruir la tabla; consulte las limitaciones. Las estrategias que se describen a continuación evitan esa copia.

Estrategia append

La estrategia append inserta las filas seleccionadas por el model directamente en la tabla de destino. No se crean tablas temporales ni se copia nada, por lo que resulta lo más económica que puede ser una ejecución incremental. El precio a pagar es que tampoco se deduplica nada: si el filtro incremental selecciona una fila que ya está en la tabla, la obtendrás dos veces. Úsala con datos inmutables de tipo evento y asegúrate de que el filtro incremental seleccione únicamente filas realmente nuevas. Con ordered_at truncado al día, eso implica cambiar el filtro incremental a >. Modifica el model:
Agregue un segundo cliente nuevo, Danny DeBito, con un pedido realizado hoy en Brooklyn (4 % de impuesto) que contiene un jaffle y un café:
El modelo incremental se ejecutó en una fracción del tiempo que tardó la ejecución anterior. Ambos clientes nuevos tienen exactamente un pedido en la tabla:
El registro de consultas confirma la diferencia: esta vez la única sentencia que afecta a orders es un único INSERT INTO jaffle_shop.orders ... SELECT ... con el SQL del model y el filtro incremental, y escribió una fila.
Con > y un timestamp truncado a nivel de día, un pedido que llegue más tarde el mismo día que el último pedido cargado nunca se recoge. En un proyecto real, aplica el filter sobre un timestamp con full precision, o sobre un tiempo de ingestión monótonamente creciente, cuando uses la estrategia append.

Estrategia de eliminación e inserción

Históricamente, ClickHouse ha ofrecido un soporte limitado para actualizaciones y eliminaciones, en forma de mutaciones asíncronas. Estas pueden consumir muchísimo IO y, por lo general, conviene evitarlas. ClickHouse 22.8 introdujo las eliminaciones ligeras y ClickHouse 25.7, las actualizaciones ligeras. Con ellas, el efecto de una sola sentencia de eliminación o actualización es visible de inmediato desde la perspectiva del usuario, aunque se materialice de forma asíncrona. La estrategia delete+insert se basa en las eliminaciones ligeras y se configura mediante el parámetro incremental_strategy:
Opera directamente sobre la tabla de destino, por lo que si algo falla a mitad del proceso es probable que los datos del modelo incremental queden en un estado inválido: no hay un intercambio atómico. En resumen:
  1. Se crea una tabla temporal (orders__dbt_new_data_<run_id>) y en ella se insertan las filas seleccionadas por el modelo.
  2. Se ejecuta un DELETE sobre orders para cada order_id presente en la tabla temporal.
  3. Las filas de la tabla temporal se insertan en orders.
  4. Se elimina la tabla temporal.

Estrategia insert overwrite (experimental)

La estrategia insert_overwrite reemplaza particiones completas, por lo que requiere una configuración partition_by, como la mensual de orders. Realiza los siguientes pasos:
  1. Crea una staging table (orders__dbt_new_data_<run_id>) con la misma estructura que orders.
  2. Inserta en la staging table únicamente las filas seleccionadas por el model.
  3. Enumera las particiones presentes en la staging table a partir de system.parts.
  4. Reemplaza exactamente esas particiones en orders mediante ALTER TABLE ... REPLACE PARTITION ... FROM desde la staging table.
  5. Elimina la staging table.
Este enfoque ofrece las siguientes ventajas:
  • Es más rápido que la estrategia predeterminada porque no copia la tabla completa.
  • Es más seguro que las demás estrategias porque no modifica la tabla original hasta que la operación INSERT se completa correctamente: si se produce un fallo intermedio, la tabla original queda intacta.
  • Aplica la mejor práctica de ingeniería de datos de la «inmutabilidad de particiones», lo que simplifica el procesamiento de datos incremental y paralelo, los rollbacks, etc.
La página de materializaciones describe las demás opciones de la materialización incremental, incluidas la estrategia microbatch y on_schema_change.

Crear un snapshot

Los snapshots de dbt registran cómo cambian con el tiempo las filas de una tabla mutable, de modo que los analistas puedan consultar el estado de los datos en cualquier momento del pasado. Implementan dimensiones de cambio lento de tipo 2: cada versión de una fila se almacena junto con el intervalo durante el cual fue válida. El mart customers es un buen candidato: count_lifetime_orders, lifetime_spend y customer_type cambian cada vez que un cliente vuelve a realizar un pedido. Antes de continuar, vuelve a configurar el model orders con la incremental strategy predeterminada de la sección incremental (elimina incremental_strategy='append' y cambia de nuevo el filter a >=), para que se recojan los pedidos realizados más adelante en el día. A partir de dbt 1.9, los snapshots se definen en YAML. Cree snapshots/customers_snapshot.yml:
La strategy check compara las columnas indicadas entre el current snapshot y el source en cada ejecución y registra una nueva version siempre que alguna de ellas haya cambiado. Si tu model cuenta con una columna de timestamp fiable de “última actualización”, la strategy timestamp resulta más económica: establece strategy: timestamp y updated_at: <column>. El campo last_ordered_at de Jaffle Shop está truncado al día, por lo que no detectaría un segundo pedido realizado el mismo día; por eso este ejemplo utiliza check. Tome el primer snapshot:
La tabla del snapshot se crea junto a los models. El macro generate_schema_name del proyecto coloca cada relation en el esquema del target para los targets que no son de production, por lo que una configuración schema en el snapshot solo surtiría efecto con el target prod. Contiene una fila por cliente, con las columnas de control de dbt dbt_valid_from y dbt_valid_to; esta última es NULL en la versión actual de una fila:
Hoy Clicky vuelve a tomarse un café:
Ejecute los models para que orders y customers reflejen el nuevo pedido y, a continuación, tome un segundo snapshot:
Clicky ahora tiene dos filas en el snapshot. La primera versión se cerró al establecer su dbt_valid_to, y la nueva versión, ahora un cliente returning con dos pedidos, queda abierta. Danny no cambió, por lo que su fila permanece intacta:
Internamente, el adaptador construye la nueva versión del snapshot en una tabla customers_snapshot__snapshot_upsert y la intercambia mediante EXCHANGE TABLES (o con un drop y un rename cuando el servidor no puede intercambiar tablas), de modo que los lectores ven la versión anterior o la nueva del snapshot. Consulte la sección snapshot de la página de materializaciones para ver la referencia de configuración.

Uso de vistas materializadas

Todo lo visto hasta ahora requiere un dbt run para incorporar nuevos datos a los modelos. Las vistas materializadas de ClickHouse funcionan de otra forma: son desencadenadores de inserción. Cada bloque de filas insertado en la tabla de origen se transforma mediante el SELECT de la vista y se escribe en una tabla de destino, sin que intervenga ninguna planificación. El adaptador las expone a través de la materialización materialized_view. Cree models/marts/daily_store_revenue.sql con el número de pedidos y los ingresos por tienda y día, leyendo directamente de la tabla de pedidos sin procesar:
engine y order_by se aplican a la tabla de destino. SummingMergeTree suma las columnas numéricas de las filas que comparten la misma clave de ordenación al fusionar las partes, que es justo lo que necesita una agregación por día y por tienda.
El adaptador creó dos objetos: la tabla de destino, que toma el nombre del model, y la propia vista materializada con el suffix _mv, que apunta a la tabla de destino mediante una clause TO. De forma predeterminada (catchup=True), la tabla de destino también se rellenó (backfill) con los pedidos existentes:
Ahora inserta otro pedido sin procesar para Danny, sin ejecutar dbt después:
La tabla de destino ya lo refleja. Brooklyn tiene ahora dos pedidos hoy:
La consulta agrega con sum() y GROUP BY de forma deliberada: SummingMergeTree solo colapsa las filas con la misma clave cuando las partes se fusionan en segundo plano, así que hasta ese momento los dos pedidos de Brooklyn son dos filas en la tabla. Con los motores de suma y de agregación, hay que agregar siempre en la lectura (o usar FINAL). Mientras tanto, el modelo incremental orders sigue teniendo un único pedido para Danny hasta el siguiente dbt run. Las ejecuciones posteriores de dbt run conservan la tabla de destino y sus datos, y solo actualizan la definición de la vista, con ALTER TABLE ... MODIFY QUERY cuando el cambio lo permite, por lo que es seguro mantener el modelo en el proyecto. dbt run --full-refresh reconstruye la tabla de destino y vuelve a rellenarla (a menos que catchup sea False). La página de vistas materializadas cubre el resto: cambios de esquema con on_schema_change, cómo desactivar el backfill con catchup, vistas materializadas actualizables, varias vistas que alimentan el mismo destino y la definición de la tabla de destino como un modelo propio.

Más información

Esta guía apenas roza la superficie de dbt. La documentación de dbt es la referencia para todo lo que no sea específico de ClickHouse. En cuanto al adaptador, consulte la página de funcionalidad y configuración para conocer los ajustes del profile y las funcionalidades globales, la página de materializaciones para cada configuración utilizada anteriormente, y la página de dbt OSS, dbt v2 y la plataforma de dbt si utiliza dbt OSS, dbt v2 o la plataforma de dbt. Toda contribución de nuevos ejemplos a Jaffle Shop for ClickHouse es bienvenida.
Última modificación el 26 de septiembre de 2026