dbt-clickhouse Adapter

repository·main·Indexed 43 days ago

https://github.com/clickhouse/dbt-clickhouse

A dbt adapter that enables modeling, testing, and documentation for ClickHouse databases. It supports various materializations including table, view, incremental, and materialized views, as well as ClickHouse-specific optimizations like Codec, TTL, and projections. The adapter is compatible with dbt Core 1.x (via Python package) and dbt v2/OSS (via Rust-based engine).

Tokens
23.5K
Snippets
52
Records
77
Agent score
56%

What's inside dbt-clickhouse

  1. Use Materialization with Explicit Target (Beta)

    main

    Starting from dbt-clickhouse version 1.10, you can use the explicit target feature to decouple the target table from the materialized views (MVs).

    In the default (implicit) approach, dbt manages the target table and all MVs within a single model using UNION ALL. The explicit target approach allows you to define the target table as a standalone table materialization and then reference it from multiple separate materialized_view models. This provides better resource separation, individual configuration for each resource, and easier iteration using dbt tooling.

    Note: This feature is in Beta. The API may change.

    -- Step 1: Define target table (events_daily.sql)
    {{ config(materialized='table', engine='SummingMergeTree()', ...) }}
    SELECT ... WHERE 0
    
    -- Step 2: Define MV (page_events_aggregator.sql)
    {{ config(materialized='materialized_view') }}
    {{ materialization_target_table(ref('events_daily')) }}
    SELECT ...
  2. Configure Refreshable Materialized Views

    main

    Refreshable materialized views periodically re-execute a query and store the result, making them suitable for periodic snapshots or aggregations rather than real-time triggers. This feature works with both implicit and explicit target approaches.

    To use them, add a refreshable configuration object to your model.

    Note on Updates: Since dbt-clickhouse 1.10.2, changes to interval, randomize, and depends_on are applied in-place using ALTER TABLE ... MODIFY REFRESH during a regular dbt run. However, converting between a regular MV and a refreshable one, or changing the append setting, requires a dbt run --full-refresh.

    {{
        config(
            materialized='materialized_view',
            engine='MergeTree()',
            order_by='(event_date)',
            refreshable={
                "interval": "EVERY 5 MINUTE",
                "randomize": "1 MINUTE",
                "append": True,
                "depends_on": ['schema.depend_on_model'],
                "depends_on_validation": True
            }
        )
    }}
    
    SELECT
        toStartOfDay(event_time) AS event_date,
        count() AS total
    FROM {{ source('raw', 'events') }}
    GROUP BY event_date
  3. Understand how dbt models are materialized in ClickHouse

    main

    In dbt, models are materialized as either views or tables.

    • Views: Rebuilt using CREATE OR REPLACE VIEW. They store no data and run the model's SQL against source tables every time they are queried. This is cost-effective for building but can be slower for complex queries.
    • Tables: Rebuilt from scratch on every run. The adapter creates a new table, performs an INSERT INTO ... SELECT, and atomically exchanges it with the old version. This provides much better query performance at the cost of storage and rebuild time.

    By default, if you do not specify a table engine or sorting key, the adapter uses MergeTree and ORDER BY tuple() (no sorting).

    models:
      jaffle_shop:
        staging:
          +materialized: view
        marts:
          +materialized: table
  4. How the default incremental strategy works

    main

    The default incremental strategy in the dbt-clickhouse adapter ensures atomicity by using temporary tables and an atomic swap.

    Workflow:

    1. Create orders__dbt_new_data containing only the new/changed rows.
    2. Create orders__dbt_tmp containing all existing rows from orders except those present in orders__dbt_new_data (deduplication).
    3. Insert all rows from orders__dbt_new_data into orders__dbt_tmp.
    4. Drop orders__dbt_new_data.
    5. Use EXCHANGE TABLES to atomically swap orders__dbt_tmp with orders.
    6. Drop the old version of the table.

    Note: Because step 2 involves copying the entire existing table, this strategy can be as expensive as a full rebuild for very large models.

  5. Understand dbt materializations in ClickHouse

    main

    In dbt, a model is a SQL SELECT statement. A materialization defines the strategy used to persist that model in ClickHouse. dbt-clickhouse supports the following core materialization types:

    • view (default): Creates a standard ClickHouse view.
    • table: Creates a standard ClickHouse table.
    • ephemeral: Does not create an object in the database; instead, it injects the model's logic as a Common Table Expression (CTE) into dependent models.
    • incremental: Materializes as a table initially, then performs incremental inserts/updates in subsequent runs to capture new data.
    • materialized view: Creates a ClickHouse materialized view. Note that creation with an explicit target is currently in Beta.
    -- Example mental model of a dbt model
    SELECT 
        user_id, 
        count(*) as event_count
    FROM raw_events
    GROUP BY user_id
  6. Use the `append` incremental strategy

    main

    The append strategy is the most efficient incremental method because it performs a direct INSERT INTO without creating temporary tables or copying existing data.

    When to use: Use this for immutable, event-style data where rows are never updated.

    Warning: Since there is no deduplication, if your incremental filter selects a row that already exists in the target table, you will have duplicates. To avoid this, ensure your filter uses a strict inequality (e.g., > instead of >=) and that your data is truly new.

    Configuration: Set incremental_strategy='append' in your model's config block.

    {{
        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() %}
    
    -- use '>' to avoid duplicates with append strategy
    where ordered_at > (select max(ordered_at) from {{ this }})
    
    {% endif %}
  7. Use the `insert_overwrite` incremental strategy (experimental)

    main

    The insert_overwrite strategy replaces entire partitions rather than individual rows. This is highly efficient and follows the 'partition immutability' best practice.

    Advantages:

    • Faster: It doesn't copy the entire table (unlike the default strategy).
    • Safer: It is atomic. The original table is only modified via ALTER TABLE ... REPLACE PARTITION after the new data has been successfully inserted into a staging table. If the process fails, the original table remains untouched.

    Requirements:

    • You must have a partition_by configuration defined in your model.

    Workflow:

    1. Create a staging table with the same structure as the target.
    2. Insert the new data into the staging table.
    3. Identify which partitions are present in the staging table.
    4. Use ALTER TABLE ... REPLACE PARTITION ... FROM to swap those specific partitions in the target table with the ones from the staging table.
    5. Drop the staging table.

    Configuration: Set incremental_strategy='insert_overwrite' in your model's config block.

    {{
        config(
            materialized='incremental',
            incremental_strategy='insert_overwrite',
            unique_key='order_id',
            engine='MergeTree()',
            order_by='(ordered_at, order_id)',
            partition_by='toYYYYMM(ordered_at)'
        )
    }}
  8. Compare Implicit vs Explicit target approaches for Materialized Views

    main

    The materialized_view materialization supports two management approaches: Implicit target and Explicit target (Beta).

    Implicit Target

    In this mode, dbt manages the target table and all associated materialized views as a single unit.

    • Management: Individual resources cannot be managed separately; all changes happen together.
    • Schema Changes: Managed via the standard on_schema_change setting (defaults to ignore).
    • Full Refresh: Recreates the target table empty and recreates all MVs. Uses catchup=True by default to backfill data using the SQL of all MVs combined.

    Explicit Target (Beta)

    This approach allows for individual management of the target table and the materialized views.

    • Management: Changes can be applied to the target table or specific MVs independently.
    • Schema Changes: The target table automatically detects if it is a target for a dbt-defined MV. Column evolution is managed by the mv_on_schema_change setting, which defaults to fail to provide a protection layer against unexpected schema changes.
    • Full Refresh: The target table is recreated normally, while MVs are dropped and recreated. catchup=True is available for an initial backfill.
  9. Materialize a ClickHouse dictionary (experimental)

    main

    A dbt model can be created as a ClickHouse dictionary. On every dbt run, the dictionary is replaced using CREATE OR REPLACE DICTIONARY.

    When using a clickhouse source (the default), the model's SQL is used as the dictionary source query. When using an http source, the model's SQL is ignored and you should use a select 1 placeholder.

    {{ config(
           materialized='dictionary',
           fields=[('id', 'UInt64'), ('name', 'String')],
           primary_key='id',
           layout='HASHED()',
           lifetime='MIN 0 MAX 300'
    ) }}
    
    select id, name from {{ source('raw', 'people') }}
  10. Materialize a distributed incremental model (experimental)

    main

    The distributed_incremental materialization allows for incremental updates on distributed tables. It uses different strategies depending on the configuration:

    1. Append Strategy: Just inserts data into the distributed table.
    2. Delete+Insert Strategy: Creates a distributed temp table to work with all data on every shard.
    3. Default (Legacy) Strategy: Creates distributed temp and intermediate tables.

    Only shard tables are replaced; the distributed table is only reloaded during a full_refresh or if the table structure changes.

    {{
        config(
            materialized='distributed_incremental',
            engine='MergeTree',
            incremental_strategy='append',
            unique_key='id,created_at'
        )
    }}
    
    select id, created_at, item
    from {{ source('db', 'table') }}
  11. Cross database macro support and limitations

    main

    dbt-clickhouse supports most dbt Core cross-database macros, but there are specific implementation details for two functions:

    • split_part: Implemented using ClickHouse's splitByChar function. The delimiter parameter must be a constant string and cannot be a column name.
    • replace: Implemented using ClickHouse's replace function. Both old_chars and new_chars parameters must be constant strings and cannot be column names.