- Comprender cómo se materializan en ClickHouse las views y tables del proyecto.
- Cargar datos con seeds y controlar los tipos de ClickHouse y la disposición de la tabla.
- Configurar un modelo de tipo table con un motor de ClickHouse, una sorting key y particionamiento.
- Convertir una table en un modelo incremental y elegir una incremental strategy.
- Crear un snapshot.
- Usar vistas materializadas de ClickHouse.
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 condbt 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 mediantedbt seed(raw_customers,raw_orders,raw_items,raw_products,raw_stores,raw_supplies).jaffle_shop(elschemade tu profile): seis vistas de staging (stg_*) y siete tablas de mart (customers,orders,order_items,products,locations,supplies,metricflow_time_spine).
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.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 endbt_project.yml: los modelos de staging son vistas y los marts son tablas.
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:
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:
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 deseeds/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:
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.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 martorders 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:
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:
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
Reconstruirorders 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:
unique_key: la columna que identifica una fila, en este casoorder_id. El adaptador la utiliza para reemplazar las filas que se vuelven a procesar en lugar de duplicarlas.- Un filtro incremental: una cláusula
whereenvuelta 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 comparaordered_atcon el valor más reciente que ya existe en la tabla, al que se hace referencia mediante la variable{{ this }}.
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.
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:
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:- Se crea una tabla
orders__dbt_new_datay 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. - Se crea una tabla
orders__dbt_tmpcon la misma structure queordersy se copian en ella todas las filas deorderscuyoorder_idno esté enorders__dbt_new_data. - Todas las filas de
orders__dbt_new_datase insertan enorders__dbt_tmp. Los pasos 2 y 3 son los que reemplazan las filas del último día en lugar de duplicarlas. - Se elimina
orders__dbt_new_data. orders__dbt_tmpse intercambia conordersmediante una sentencia atómicaEXCHANGE TABLES(a través de un cambio de nombre intermedio aorders__dbt_backup), de modo queorderspasa a contener la nueva versión.- Se elimina la versión antigua.
Estrategia append
La estrategiaappend 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:
orders es un único INSERT INTO jaffle_shop.orders ... SELECT ... con el SQL del model y el filtro incremental, y escribió una fila.
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 estrategiadelete+insert se basa en las eliminaciones ligeras y se configura mediante el parámetro incremental_strategy:
- Se crea una tabla temporal (
orders__dbt_new_data_<run_id>) y en ella se insertan las filas seleccionadas por el modelo. - Se ejecuta un
DELETEsobreorderspara cadaorder_idpresente en la tabla temporal. - Las filas de la tabla temporal se insertan en
orders. - Se elimina la tabla temporal.
Estrategia insert overwrite (experimental)
La estrategiainsert_overwrite reemplaza particiones completas, por lo que requiere una configuración partition_by, como la mensual de orders. Realiza los siguientes pasos:
- Crea una staging table (
orders__dbt_new_data_<run_id>) con la misma estructura queorders. - Inserta en la staging table únicamente las filas seleccionadas por el model.
- Enumera las particiones presentes en la staging table a partir de
system.parts. - Reemplaza exactamente esas particiones en
ordersmedianteALTER TABLE ... REPLACE PARTITION ... FROMdesde la staging table. - Elimina la staging table.
- 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.
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 martcustomers 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:
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:
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:
orders y customers reflejen el nuevo pedido y, a continuación, tome un segundo snapshot:
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:
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 undbt 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.
_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:
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.