> ## Documentation Index
> Fetch the complete documentation index at: https://private-7c7dfe99-parallel-read-in-order-multi-part.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

> Guías para usar dbt con ClickHouse

# Guías

export const ClickHouseSupportedBadge = () => {
  return <div className="ClickHouseSupportedBadge">
            <div className="ClickHouseSupportedIcon">
                <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M1.30762 1.39073C1.30762 1.3103 1.37465 1.22986 1.46849 1.22986H2.64824C2.72868 1.22986 2.80912 1.29689 2.80912 1.39073V14.4886C2.80912 14.5691 2.74209 14.6495 2.64824 14.6495H1.46849C1.38805 14.6495 1.30762 14.5825 1.30762 14.4886V1.39073Z" fill="currentColor" />
                    <path d="M4.2832 1.39073C4.2832 1.3103 4.35023 1.22986 4.44408 1.22986H5.62383C5.70427 1.22986 5.7847 1.29689 5.7847 1.39073V14.4886C5.7847 14.5691 5.71767 14.6495 5.62383 14.6495H4.44408C4.36364 14.6495 4.2832 14.5825 4.2832 14.4886V1.39073Z" fill="currentColor" />
                    <path d="M7.25977 1.39073C7.25977 1.3103 7.3268 1.22986 7.42064 1.22986H8.60039C8.68083 1.22986 8.76127 1.29689 8.76127 1.39073V14.4886C8.76127 14.5691 8.69423 14.6495 8.60039 14.6495H7.42064C7.3402 14.6495 7.25977 14.5825 7.25977 14.4886V1.39073Z" fill="currentColor" />
                    <path d="M10.2354 1.39073C10.2354 1.3103 10.3024 1.22986 10.3962 1.22986H11.576C11.6564 1.22986 11.7369 1.29689 11.7369 1.39073V14.4886C11.7369 14.5691 11.6698 14.6495 11.576 14.6495H10.3962C10.3158 14.6495 10.2354 14.5825 10.2354 14.4886V1.39073Z" fill="currentColor" />
                    <path d="M13.2256 6.6057C13.2256 6.52526 13.2926 6.44482 13.3865 6.44482H14.5662C14.6466 6.44482 14.7271 6.51186 14.7271 6.6057V9.27354C14.7271 9.35398 14.6601 9.43442 14.5662 9.43442H13.3865C13.306 9.43442 13.2256 9.36739 13.2256 9.27354V6.6057Z" fill="currentColor" />
                </svg>
            </div>
            Compatible con ClickHouse
        </div>;
};

<ClickHouseSupportedBadge />

Esta guía recorre los aspectos específicos de ClickHouse en dbt usando el proyecto [Jaffle Shop for ClickHouse](https://github.com/ClickHouse/jaffle-shop-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](/es/integrations/connectors/data-ingestion/etl-tools/dbt/index), la página de [funcionalidad y configuración](/es/integrations/connectors/data-ingestion/etl-tools/dbt/features-and-configurations) y la [referencia de materializaciones](/es/integrations/connectors/data-ingestion/etl-tools/dbt/materializations).

<h2 id="before-you-start">
  Antes de empezar
</h2>

Sigue primero el README de [ClickHouse/jaffle-shop-clickhouse](https://github.com/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.

<Note>
  **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](/es/integrations/connectors/data-ingestion/etl-tools/dbt/dbt-core-v2-fusion-and-platform) para conocer el estado actual del adaptador v2, y [Connect ClickHouse](https://docs.getdbt.com/docs/platform/connect-data-platform/connect-clickhouse) en la documentación de dbt para empezar a usar la plataforma dbt.
</Note>

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.

<h2 id="views-and-tables">
  Cómo se materializa el proyecto
</h2>

Jaffle Shop define sus materializaciones en `dbt_project.yml`: los modelos de staging son vistas y los marts son tablas.

```yaml theme={null}
models:
  jaffle_shop:
    staging:
      +materialized: view
    marts:
      +materialized: table
```

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:

```sql theme={null}
SHOW CREATE VIEW jaffle_shop.stg_orders;
```

```response theme={null}
CREATE VIEW jaffle_shop.stg_orders
(
    `order_id` String,
    `location_id` String,
    `customer_id` String,
    ...
    `ordered_at` DateTime
)
AS WITH
    source AS
    (
        SELECT *
        FROM raw.raw_orders
    ),
    renamed AS
    (
        SELECT
            id AS order_id,
            store_id AS location_id,
            ...
            dateTrunc('day', ordered_at) AS ordered_at
        FROM source
    )
SELECT *
FROM renamed
```

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`:

```sql theme={null}
SHOW CREATE TABLE jaffle_shop.orders;
```

```response theme={null}
CREATE TABLE jaffle_shop.orders
(
    `order_id` String,
    `location_id` String,
    `customer_id` String,
    ...
    `customer_order_number` UInt64
)
ENGINE = MergeTree
ORDER BY tuple()
SETTINGS replicated_deduplication_window = '0', index_granularity = 8192
```

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](/es/integrations/connectors/data-ingestion/etl-tools/dbt/materializations) enumera todas las configuraciones de tabla que admite el adaptador.

<h2 id="seeds">
  Carga de datos con seeds
</h2>

Jaffle Shop utiliza los [seeds](https://docs.getdbt.com/docs/build/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:

| Valor del CSV | dbt v1 | motor v2 |
| - | - | - |
| `700` | `Int32` | `Int64` |
| `0.06` | `Float32` | `Float64` |
| `2024-09-01T15:01:00` | `DateTime` | `DateTime64(6)` |
| `Philadelphia` | `String` | `String` |

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`:

```yaml theme={null}
seeds:
  jaffle_shop:
    +schema: raw
    jaffle-data:
      +enabled: "{{ var('load_source_data', false) }}"
      raw_stores:
        +column_types:
          opened_at: DateTime64(3)
```

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`:

```yaml theme={null}
seeds:
  - name: raw_orders
    config:
      order_by: (ordered_at, id)
      partition_by: toYYYYMM(ordered_at)
```

<Note>
  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`.
</Note>

Vuelva a cargar ese seed y compruebe la tabla resultante:

```bash theme={null}
dbt seed --select raw_orders --full-refresh --vars '{"load_source_data": true}'
```

```sql theme={null}
SHOW CREATE TABLE raw.raw_orders;
```

```response theme={null}
CREATE TABLE raw.raw_orders
(
    `id` String,
    `customer` String,
    `ordered_at` DateTime,
    `store_id` String,
    `subtotal` Int32,
    `tax_paid` Int32,
    `order_total` Int32
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(ordered_at)
ORDER BY (ordered_at, id)
SETTINGS index_granularity = 8192
```

`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.

<h2 id="table-configuration">
  Configuración de una tabla para ClickHouse
</h2>

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:

```sql theme={null}
{{
    config(
        materialized='table',
        engine='MergeTree()',
        order_by='(ordered_at, order_id)',
        partition_by='toYYYYMM(ordered_at)'
    )
}}

with

orders as (

    select * from {{ ref('stg_orders') }}

),
...
```

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:

```bash theme={null}
dbt run --select orders
```

```response theme={null}
1 of 1 START sql table model `jaffle_shop`.`orders` ............................ [RUN]
1 of 1 OK created sql table model `jaffle_shop`.`orders` ....................... [OK in 0.44s]
```

Ahora la tabla tiene una sorting key adecuada y una partición por mes:

```sql theme={null}
SHOW CREATE TABLE jaffle_shop.orders;
```

```response theme={null}
CREATE TABLE jaffle_shop.orders
(
    ...
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(ordered_at)
ORDER BY (ordered_at, order_id)
SETTINGS replicated_deduplication_window = '0', index_granularity = 8192
```

```sql theme={null}
SELECT partition, sum(rows) AS rows
FROM system.parts
WHERE database = 'jaffle_shop' AND table = 'orders' AND active
GROUP BY partition
ORDER BY partition;
```

```response theme={null}
┌─partition─┬─rows─┐
│ 202409    │ 1497 │
│ 202410    │ 1698 │
│ 202411    │ 2262 │
...
│ 202508    │ 9389 │
└───────────┴──────┘
```

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](/es/integrations/connectors/data-ingestion/etl-tools/dbt/materializations).

<h2 id="incremental">
  Creación de un modelo incremental
</h2>

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](https://docs.getdbt.com/docs/build/incremental-models) 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í:

```sql theme={null}
{{
    config(
        materialized='incremental',
        unique_key='order_id',
        engine='MergeTree()',
        order_by='(ordered_at, order_id)',
        partition_by='toYYYYMM(ordered_at)'
    )
}}

with

orders as (

    select * from {{ ref('stg_orders') }}

),

...

select * from customer_order_count

{% if is_incremental() %}

-- this filter will only be applied on an incremental run
where ordered_at >= (select max(ordered_at) from {{ this }})

{% endif %}
```

`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.

```bash theme={null}
dbt run --select orders
```

```response theme={null}
1 of 1 START sql incremental model `jaffle_shop`.`orders` ...................... [RUN]
1 of 1 OK created sql incremental model `jaffle_shop`.`orders` ................. [OK in 0.94s]
```

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:

```sql theme={null}
INSERT INTO raw.raw_customers VALUES ('clicky-0001', 'Clicky McClickHouse');

INSERT INTO raw.raw_orders VALUES
    ('clicky-order-0001', 'clicky-0001', now() - INTERVAL 1 DAY,
     '4b6c2304-2b9e-41e4-942a-cf11a1819378', 1100, 66, 1166);

INSERT INTO raw.raw_items VALUES ('clicky-item-0001', 'clicky-order-0001', 'JAF-001');
```

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`:

```bash theme={null}
dbt run
```

```response theme={null}
...
10 of 13 OK created sql table model `jaffle_shop`.`order_items` ................ [OK in 0.30s]
...
12 of 13 START sql incremental model `jaffle_shop`.`orders` .................... [RUN]
12 of 13 OK created sql incremental model `jaffle_shop`.`orders` ............... [OK in 0.73s]
13 of 13 START sql table model `jaffle_shop`.`customers` ....................... [RUN]
13 of 13 OK created sql table model `jaffle_shop`.`customers` .................. [OK in 0.28s]
```

El nuevo pedido está en la tabla incremental y el mart `customers`, reconstruido a partir de ella, ya reconoce al nuevo cliente:

```sql theme={null}
SELECT order_id, customer_id, ordered_at, order_total, customer_order_number
FROM jaffle_shop.orders
WHERE customer_id = 'clicky-0001';
```

```response theme={null}
┌─order_id──────────┬─customer_id─┬──────────ordered_at─┬─order_total─┬─customer_order_number─┐
│ clicky-order-0001 │ clicky-0001 │ 2026-09-14 00:00:00 │       11.66 │                     1 │
└───────────────────┴─────────────┴─────────────────────┴─────────────┴───────────────────────┘
```

```sql theme={null}
SELECT customer_name, count_lifetime_orders, lifetime_spend, customer_type
FROM jaffle_shop.customers
WHERE customer_id = 'clicky-0001';
```

```response theme={null}
┌─customer_name───────┬─count_lifetime_orders─┬─lifetime_spend─┬─customer_type─┐
│ Clicky McClickHouse │                     1 │          11.66 │ new           │
└─────────────────────┴───────────────────────┴────────────────┴───────────────┘
```

<h3 id="internals">
  Internals
</h3>

El registro de consultas de ClickHouse muestra las sentencias que ejecutó el adaptador para la actualización incremental:

```sql theme={null}
SELECT event_time, written_rows, tables
FROM system.query_log
WHERE query_kind = 'Insert' AND type = 'QueryFinish'
  AND has(databases, 'jaffle_shop')
  AND event_time > now() - INTERVAL 15 MINUTE
ORDER BY event_time;
```

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.

```mermaid theme={null}
flowchart TB
    stg[("stg_orders")]
    items[("order_items")]
    orders[("orders<br/>(current version)")]
    new_data[("orders__dbt_new_data")]
    tmp[("orders__dbt_tmp")]
    new_orders[("orders<br/>(new version, was orders__dbt_tmp)")]
    old_orders[("orders__dbt_tmp<br/>(old version, was orders)")]
    q1["1. INSERT INTO orders__dbt_new_data<br/>SELECT ... model SQL ...<br/>WHERE ordered_at >= (SELECT max(ordered_at) FROM orders)"]
    q2["2. INSERT INTO orders__dbt_tmp<br/>SELECT * FROM orders<br/>WHERE order_id NOT IN (SELECT order_id FROM orders__dbt_new_data)"]
    q3["3. INSERT INTO orders__dbt_tmp<br/>SELECT * FROM orders__dbt_new_data"]
    q4["4. DROP TABLE orders__dbt_new_data"]
    q5["5. EXCHANGE TABLES orders__dbt_tmp AND orders"]
    q6["6. DROP TABLE orders__dbt_tmp"]
    stg -->|reads| q1
    items -->|reads| q1
    q1 -->|inserts the changed rows| new_data
    orders -->|reads| q2
    new_data -->|reads the keys| q2
    new_data -->|reads| q3
    q2 -->|"inserts the rows<br/>that did not change"| tmp
    q3 -->|"inserts the<br/>changed rows"| tmp
    q3 ~~~ q4
    q4 -->|drops| new_data
    tmp ~~~ q5
    q5 -->|"swaps the names"| new_orders
    q5 -->|"swaps the names"| old_orders
    q5 ~~~ q6
    q6 -->|drops| old_orders
    classDef temp fill:#dbeafe,stroke:#1d4ed8;
    classDef target fill:#fef3c7,stroke:#b45309;
    classDef source fill:#dcfce7,stroke:#15803d;
    classDef query fill:#f3f4f6,stroke:#6b7280;
    class new_data,tmp,old_orders temp;
    class orders,new_orders target;
    class stg,items source;
    class q1,q2,q3,q4,q5,q6 query;
```

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](/es/integrations/connectors/data-ingestion/etl-tools/dbt/index#limitations). Las estrategias que se describen a continuación evitan esa copia.

<h3 id="append-strategy">
  Estrategia append
</h3>

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:

```sql theme={null}
{{
    config(
        materialized='incremental',
        incremental_strategy='append',
        unique_key='order_id',
        engine='MergeTree()',
        order_by='(ordered_at, order_id)',
        partition_by='toYYYYMM(ordered_at)'
    )
}}

...

{% if is_incremental() %}

-- this filter will only be applied on an incremental run
where ordered_at > (select max(ordered_at) from {{ this }})

{% endif %}
```

Agregue un segundo cliente nuevo, Danny DeBito, con un pedido realizado hoy en Brooklyn (4 % de impuesto) que contiene un jaffle y un café:

```sql theme={null}
INSERT INTO raw.raw_customers VALUES ('danny-0001', 'Danny DeBito');

INSERT INTO raw.raw_orders VALUES
    ('danny-order-0001', 'danny-0001', now(),
     '40e6ddd6-b8f6-4e17-8bd6-5e53966809d2', 1900, 76, 1976);

INSERT INTO raw.raw_items VALUES
    ('danny-item-0001', 'danny-order-0001', 'JAF-003'),
    ('danny-item-0002', 'danny-order-0001', 'BEV-004');
```

```bash theme={null}
dbt run
```

```response theme={null}
...
12 of 13 START sql incremental model `jaffle_shop`.`orders` .................... [RUN]
12 of 13 OK created sql incremental model `jaffle_shop`.`orders` ............... [OK in 0.11s]
...
```

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:

```sql theme={null}
SELECT order_id, customer_id, ordered_at, order_total, is_food_order, is_drink_order
FROM jaffle_shop.orders
WHERE customer_id IN ('clicky-0001', 'danny-0001')
ORDER BY ordered_at;
```

```response theme={null}
┌─order_id──────────┬─customer_id─┬──────────ordered_at─┬─order_total─┬─is_food_order─┬─is_drink_order─┐
│ clicky-order-0001 │ clicky-0001 │ 2026-09-14 00:00:00 │       11.66 │             1 │              0 │
│ danny-order-0001  │ danny-0001  │ 2026-09-15 00:00:00 │       19.76 │             1 │              1 │
└───────────────────┴─────────────┴─────────────────────┴─────────────┴───────────────┴────────────────┘
```

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.

<Warning>
  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`.
</Warning>

<h3 id="delete-insert-strategy">
  Estrategia de eliminación e inserción
</h3>

Históricamente, ClickHouse ha ofrecido un soporte limitado para actualizaciones y eliminaciones, en forma de [mutaciones](/es/reference/statements/alter/index) asíncronas. Estas pueden consumir muchísimo IO y, por lo general, conviene evitarlas. ClickHouse 22.8 introdujo las [eliminaciones ligeras](/es/reference/statements/delete) y ClickHouse 25.7, las [actualizaciones ligeras](/es/reference/statements/update). 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`:

```sql theme={null}
{{
    config(
        materialized='incremental',
        incremental_strategy='delete+insert',
        unique_key='order_id',
        engine='MergeTree()',
        order_by='(ordered_at, order_id)',
        partition_by='toYYYYMM(ordered_at)'
    )
}}
```

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.

```mermaid theme={null}
flowchart TB
    stg[("stg_orders")]
    items[("order_items")]
    orders[("orders")]
    new_data[("orders__dbt_new_data_#lt;run_id#gt;")]
    q1["1. CREATE TABLE orders__dbt_new_data_#lt;run_id#gt; AS<br/>SELECT ... model SQL ...<br/>WHERE ordered_at >= (SELECT max(ordered_at) FROM orders)"]
    q2["2. DELETE FROM orders<br/>WHERE order_id IN (SELECT order_id FROM orders__dbt_new_data_#lt;run_id#gt;)"]
    q3["3. INSERT INTO orders<br/>SELECT * FROM orders__dbt_new_data_#lt;run_id#gt;"]
    q4["4. DROP TABLE orders__dbt_new_data_#lt;run_id#gt;"]
    stg -->|reads| q1
    items -->|reads| q1
    q1 -->|creates and fills with the changed rows| new_data
    new_data -->|reads the keys| q2
    q2 -->|deletes the matching rows| orders
    new_data -->|reads| q3
    q3 -->|inserts the changed rows| orders
    q4 -->|drops| new_data
    classDef temp fill:#dbeafe,stroke:#1d4ed8;
    classDef target fill:#fef3c7,stroke:#b45309;
    classDef source fill:#dcfce7,stroke:#15803d;
    classDef query fill:#f3f4f6,stroke:#6b7280;
    class new_data temp;
    class orders target;
    class stg,items source;
    class q1,q2,q3,q4 query;
```

<h3 id="insert-overwrite-strategy">
  Estrategia insert overwrite (experimental)
</h3>

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.

```mermaid theme={null}
flowchart TB
    stg[("stg_orders")]
    items[("order_items")]
    orders[("orders<br/>PARTITION BY toYYYYMM(ordered_at)")]
    staging[("orders__dbt_new_data_#lt;run_id#gt;")]
    parts[("system.parts")]
    q1["1. CREATE TABLE orders__dbt_new_data_#lt;run_id#gt; AS orders"]
    q2["2. INSERT INTO orders__dbt_new_data_#lt;run_id#gt;<br/>SELECT ... model SQL ...<br/>WHERE ordered_at >= (SELECT max(ordered_at) FROM orders)"]
    q3["3. SELECT DISTINCT partition_id FROM system.parts<br/>WHERE table = 'orders__dbt_new_data_#lt;run_id#gt;' AND active"]
    q4["4. ALTER TABLE orders<br/>REPLACE PARTITION ID '202509' FROM orders__dbt_new_data_#lt;run_id#gt;,<br/>REPLACE PARTITION ID ... (one per partition found in step 3)"]
    q5["5. DROP TABLE orders__dbt_new_data_#lt;run_id#gt;"]
    q1 -->|creates empty, same structure as orders| staging
    stg -->|reads| q2
    items -->|reads| q2
    q2 -->|inserts the changed rows| staging
    parts -->|reads the partitions of the staging table| q3
    q3 -->|partition ids| q4
    staging -->|reads| q4
    q4 -->|replaces those partitions| orders
    q5 -->|drops| staging
    classDef temp fill:#dbeafe,stroke:#1d4ed8;
    classDef target fill:#fef3c7,stroke:#b45309;
    classDef source fill:#dcfce7,stroke:#15803d;
    classDef query fill:#f3f4f6,stroke:#6b7280;
    class staging temp;
    class orders target;
    class stg,items,parts source;
    class q1,q2,q3,q4,q5 query;
```

La [página de materializaciones](/es/integrations/connectors/data-ingestion/etl-tools/dbt/materializations#materialization-incremental) describe las demás opciones de la materialización incremental, incluidas la estrategia `microbatch` y `on_schema_change`.

<h2 id="snapshot">
  Crear un snapshot
</h2>

Los [snapshots](https://docs.getdbt.com/docs/build/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](https://en.wikipedia.org/wiki/Slowly_changing_dimension#Type_2:_add_new_row): 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](#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`:

```yaml theme={null}
snapshots:
  - name: customers_snapshot
    relation: ref('customers')
    config:
      unique_key: customer_id
      strategy: check
      check_cols:
        - count_lifetime_orders
        - lifetime_spend
        - customer_type
```

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:

```bash theme={null}
dbt snapshot
```

```response theme={null}
1 of 1 START snapshot `jaffle_shop`.`customers_snapshot` ....................... [RUN]
1 of 1 OK snapshotted `jaffle_shop`.`customers_snapshot` ....................... [OK in 0.17s]
```

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:

```sql theme={null}
SELECT customer_id, count_lifetime_orders, lifetime_spend, customer_type, dbt_valid_from, dbt_valid_to
FROM jaffle_shop.customers_snapshot
WHERE customer_id IN ('clicky-0001', 'danny-0001')
ORDER BY customer_id, dbt_valid_from;
```

```response theme={null}
┌─customer_id─┬─count_lifetime_orders─┬─lifetime_spend─┬─customer_type─┬──────dbt_valid_from─┬─dbt_valid_to─┐
│ clicky-0001 │                     1 │          11.66 │ new           │ 2026-09-15 01:15:32 │         ᴺᵁᴸᴸ │
│ danny-0001  │                     1 │          19.76 │ new           │ 2026-09-15 01:15:32 │         ᴺᵁᴸᴸ │
└─────────────┴───────────────────────┴────────────────┴───────────────┴─────────────────────┴──────────────┘
```

Hoy Clicky vuelve a tomarse un café:

```sql theme={null}
INSERT INTO raw.raw_orders VALUES
    ('clicky-order-0002', 'clicky-0001', now(),
     '4b6c2304-2b9e-41e4-942a-cf11a1819378', 600, 36, 636);

INSERT INTO raw.raw_items VALUES ('clicky-item-0002', 'clicky-order-0002', 'BEV-001');
```

Ejecute los models para que `orders` y `customers` reflejen el nuevo pedido y, a continuación, tome un segundo snapshot:

```bash theme={null}
dbt run
dbt snapshot
```

```response theme={null}
1 of 1 START snapshot `jaffle_shop`.`customers_snapshot` ....................... [RUN]
1 of 1 OK snapshotted `jaffle_shop`.`customers_snapshot` ....................... [OK in 0.73s]
```

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:

```sql theme={null}
SELECT customer_id, count_lifetime_orders, lifetime_spend, customer_type, dbt_valid_from, dbt_valid_to
FROM jaffle_shop.customers_snapshot
WHERE customer_id IN ('clicky-0001', 'danny-0001')
ORDER BY customer_id, dbt_valid_from;
```

```response theme={null}
┌─customer_id─┬─count_lifetime_orders─┬─lifetime_spend─┬─customer_type─┬──────dbt_valid_from─┬────────dbt_valid_to─┐
│ clicky-0001 │                     1 │          11.66 │ new           │ 2026-09-15 01:15:32 │ 2026-09-15 01:16:16 │
│ clicky-0001 │                     2 │          18.02 │ returning     │ 2026-09-15 01:16:16 │                ᴺᵁᴸᴸ │
│ danny-0001  │                     1 │          19.76 │ new           │ 2026-09-15 01:15:32 │                ᴺᵁᴸᴸ │
└─────────────┴───────────────────────┴────────────────┴───────────────┴─────────────────────┴─────────────────────┘
```

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](/es/integrations/connectors/data-ingestion/etl-tools/dbt/materializations#snapshot) para ver la referencia de configuración.

<h2 id="materialized-views">
  Uso de vistas materializadas
</h2>

Todo lo visto hasta ahora requiere un `dbt run` para incorporar nuevos datos a los modelos. Las [vistas materializadas](/es/concepts/features/materialized-views/index) 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:

```sql theme={null}
{{
    config(
        materialized='materialized_view',
        engine='SummingMergeTree()',
        order_by='(order_date, store_id)'
    )
}}

select
    toDate(ordered_at) as order_date,
    store_id,
    count() as orders,
    sum(order_total) as revenue_cents
from {{ source('ecom', 'raw_orders') }}
group by order_date, store_id
```

`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.

```bash theme={null}
dbt run --select daily_store_revenue
```

```response theme={null}
1 of 1 START sql materialized_view model `jaffle_shop`.`daily_store_revenue` ... [RUN]
1 of 1 OK created sql materialized_view model `jaffle_shop`.`daily_store_revenue`  [OK in 0.25s]
```

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:

```sql theme={null}
SELECT name, engine
FROM system.tables
WHERE database = 'jaffle_shop' AND name LIKE 'daily_store_revenue%';
```

```response theme={null}
┌─name───────────────────┬─engine───────────┐
│ daily_store_revenue    │ SummingMergeTree │
│ daily_store_revenue_mv │ MaterializedView │
└────────────────────────┴──────────────────┘
```

```sql theme={null}
SHOW CREATE TABLE jaffle_shop.daily_store_revenue_mv;
```

```response theme={null}
CREATE MATERIALIZED VIEW jaffle_shop.daily_store_revenue_mv TO jaffle_shop.daily_store_revenue
(
    `order_date` Date,
    `store_id` String,
    `orders` UInt64,
    `revenue_cents` Int64
)
AS SELECT
    toDate(ordered_at) AS order_date,
    store_id,
    count() AS orders,
    sum(order_total) AS revenue_cents
FROM raw.raw_orders
GROUP BY
    order_date,
    store_id
```

Ahora inserta otro pedido sin procesar para Danny, sin ejecutar dbt después:

```sql theme={null}
INSERT INTO raw.raw_orders VALUES
    ('danny-order-0002', 'danny-0001', now(),
     '40e6ddd6-b8f6-4e17-8bd6-5e53966809d2', 1400, 56, 1456);

INSERT INTO raw.raw_items VALUES ('danny-item-0003', 'danny-order-0002', 'JAF-004');
```

La tabla de destino ya lo refleja. Brooklyn tiene ahora dos pedidos hoy:

```sql theme={null}
SELECT order_date, store_id, sum(orders) AS orders, sum(revenue_cents) AS revenue_cents
FROM jaffle_shop.daily_store_revenue
WHERE order_date >= yesterday()
GROUP BY order_date, store_id
ORDER BY order_date, store_id;
```

```response theme={null}
┌─order_date─┬─store_id─────────────────────────────┬─orders─┬─revenue_cents─┐
│ 2026-09-14 │ 4b6c2304-2b9e-41e4-942a-cf11a1819378 │      1 │          1166 │
│ 2026-09-15 │ 40e6ddd6-b8f6-4e17-8bd6-5e53966809d2 │      2 │          3432 │
│ 2026-09-15 │ 4b6c2304-2b9e-41e4-942a-cf11a1819378 │      1 │           636 │
└────────────┴──────────────────────────────────────┴────────┴───────────────┘
```

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](/es/integrations/connectors/data-ingestion/etl-tools/dbt/materialization-materialized-view) 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.

<h2 id="further-information">
  Más información
</h2>

Esta guía apenas roza la superficie de dbt. La [documentación de dbt](https://docs.getdbt.com/docs/introduction) 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](/es/integrations/connectors/data-ingestion/etl-tools/dbt/features-and-configurations) para conocer los ajustes del profile y las funcionalidades globales, la [página de materializaciones](/es/integrations/connectors/data-ingestion/etl-tools/dbt/materializations) para cada configuración utilizada anteriormente, y la [página de dbt OSS, dbt v2 y la plataforma de dbt](/es/integrations/connectors/data-ingestion/etl-tools/dbt/dbt-core-v2-fusion-and-platform) si utiliza dbt OSS, dbt v2 o la plataforma de dbt. Toda contribución de nuevos ejemplos a [Jaffle Shop for ClickHouse](https://github.com/ClickHouse/jaffle-shop-clickhouse) es bienvenida.
