Skip to main content
A materialized_view materialization should be a SELECT from an existing (source) table. Unlike PostgreSQL, a ClickHouse materialized view is not “static” (and has no corresponding REFRESH operation). Instead, it acts as an insert trigger, inserting new rows into a target table by applying the defined SELECT transformation on rows inserted into the source table. See the ClickHouse materialized view documentation for more details on how materialized views work in ClickHouse.
For general materialization concepts and shared configurations (engine, order_by, partition_by, etc.), see the Materializations page.

How the target table is managed

When you use the materialized_view materialization, dbt-clickhouse needs to create both a materialized view and a target table where the transformed rows are inserted. There are two ways to manage the target table: The approach you choose affects how schema changes, full refreshes, and multi-MV setups are handled. The following sections describe each approach in detail.

Materialization with implicit target

This is the default behavior. When you define a materialized_view model, the adapter will:
  1. Create a target table with the model name
  2. Create a ClickHouse materialized view with the name <model_name>_mv
The target table schema is inferred from the columns in the MV’s SELECT statement. All resources (target table + MVs) share the same model configuration.
See the test file for additional examples.
You can also define column-level codec and ttl on the target table by enforcing a model contract. See Column Configuration for details.

Multiple materialized views

ClickHouse allows more than one materialized view to write records to the same target table. To support this in dbt-clickhouse with the implicit target approach, you can construct a UNION in your model file, wrapping the SQL for each materialized view with comments of the form --my_mv_name:begin and --my_mv_name:end. For example, the following will build two materialized views both writing data to the same destination table of the model. The names of the materialized views will take the form <model_name>_mv1 and <model_name>_mv2:
When updating a model with multiple materialized views (MVs), especially when renaming one of the MV names, dbt-clickhouse does not automatically drop the old MV. Instead, you will encounter the following warning:Warning - Table <previous table name> was detected with the same pattern as model name <your model name> but was not found in this run. In case it is a renamed mv that was previously part of this model, drop it manually (!!!)

How to iterate the target table schema

Starting with dbt-clickhouse version 1.9.8, you can control how the target table schema is iterated when dbt run encounters different columns in the MV’s SQL.
By default, dbt will not apply any changes to the target table (ignore setting value), but you can change this setting to follow the same behavior as the on_schema_change config in incremental models. Also, you can use this setting as a safety mechanism. If you set it to fail, the build will fail if the columns in the MV’s SQL differ from the target table that was created by the first dbt run.

Data catch-up

By default, when creating or recreating a materialized view (MV), the target table is first populated with historical data before the MV itself is created (catchup=True). You can disable this behavior by setting the catchup config to False.
Data Loss Risk with Full RefreshUsing catchup: False with dbt run --full-refresh will discard all existing data in the target table. The table will be recreated empty and only capture new data going forward. Ensure you have backups if the historical data might be needed later.

Materialization with explicit target (Beta)

BetaThis feature is in beta and available starting from dbt-clickhouse version 1.10. The API may change based on community feedback.
By default, dbt-clickhouse creates and manages both the target table and the materialized views within a single model (the implicit target approach described above). This approach has some limitations:
  • All resources (target table + MVs) share the same configuration. If multiple MVs are pointing to the same target table, they must be defined together using UNION ALL syntax.
  • None of these resources can be iterated separately, all need to be managed using the same model file.
  • You cannot easily control the name of each MV.
  • All settings are shared between the target table and the MVs, making it difficult to configure each resource individually and to reason about which configuration belongs to each resource.
The explicit target feature allows you to define the target table separately as a regular table materialization and then reference it from your materialized view models.

Benefits

  • Fully separated resources: Now each resource can be defined separately, improving readability
  • 1:1 resources between dbt and CH: Now you can use dbt tooling to manage and iterate them separately.
  • Different configurations now available: Now a different configuration can be applied to each one.
  • No more need to keep naming conventions: Now all resources are created using the name you give, not the custom one added with the _mv for MVs.

Limitations

  • Target table definition is not natural to dbt: it’s not a SQL that will read from a source table, so you lose dbt validations here. MV’s SQL will still get validated using dbt utilities and its compatibility with the target table’s columns will be validated at CH level.
  • We found some problems related to limitations to the ref() function: We need to use it to reference models between them but it can only be used to reference upstream models, not downstream. This causes some problems for this implementation. We have created an issue in the dbt-core repo and we are currently talking with them to look for possible solutions (dbt-labs/dbt-core#12319):
    • When ref() is called from inside the config block, it returns the current model, not the one shared. This blocks us from defining it in the config() section, forcing us to use a comment to add this dependency. We are following the same pattern as defined in the dbt docs with the “—depends_on:” approach.
    • ref() works for us as it forces the target table to be created first, but in the dependency chart in the generated documentation, the target table will be drawn as another upstream dependency, not downstream, making it a bit difficult to understand.
    • unit-test also forces us to define some data for the target table even when the idea is not to read from it. The workaround is just to leave the data for this table empty.

Usage

Step 1: Define the target table as a regular table model Model events_daily.sql:
This is the workaround we mention in the limitations section. You may lose some dbt validations here, but the schema will still be checked at ClickHouse level. Step 2: Define materialized views pointing to the target table For example, you can define different MVs in different models like this, even pointing to the same target table. Note the new {{ materialization_target_table(ref('events_daily')) }} macro call, which configures the target table for the MV. Model page_events_aggregator.sql:
Model mobile_events_aggregator.sql:

Configuration options

When using explicit target tables, apart from the general materialization configurations and the table-specific configurations, the following configurations apply: On the target table (materialized='table'): On the materialized view (materialized='materialized_view'):
You’ll usually only want to set catchup to True in MVs or repopulate_from_mvs_on_full_refresh to True in their target tables. If you set both to True, it may duplicate data.

Common operations

Full refresh with explicit targets

When using --full-refresh, explicit target tables will be recreated (so you may lose data if ingestion is happening during this process). This will behave in different ways depending on your configurations: Option 1: default --full-refresh behavior. All gets recreated, but during the recreation of the MVs, the target table will be empty or partially loaded. All gets dropped and recreated. If you want to reinsert the data using the MVs SQL, keep the setting catchup=True:
Option 2: I want to recreate the target table and I don’t want to read empty data while the MVs are being recreated. If you need to update the sql of the MVs first, you can set in them catchup=False and then do a dbt run or dbt run --full-refresh on the MVs. Make sure that the MVs are created before running --full-refresh on the target table, as it uses the MV definitions from ClickHouse. Set repopulate_from_mvs_on_full_refresh=True on the target table model. On a dbt run --full-refresh, this will:
  1. Create a new temporary table
  2. Execute INSERT-SELECT using each MV’s SQL
  3. Atomically swap the tables
So you will not see empty data in your table while the MVs are being recreated.

Changing the target table

You cannot change the target table of an MV without a --full-refresh. If you try to run a regular dbt run after changing the materialization_target_table() reference, the build will fail with an error message indicating that the target has changed. To change the target:
  1. Update the materialization_target_table() call
  2. Run dbt run --full-refresh -s your_mv_model

Troubleshooting common issues

Target table is empty while/after run is executed

There are a few reasons why this can happen:
  • Materialized views may be configured with catchup=False or the target table may be configured with repopulate_from_mvs_on_full_refresh=False, so no backfill is executed when the materialized views are created or when the target table is recreated. This is the expected behavior, so if you want to reinsert the data using the materialized views SQL, make sure to set catchup=True in the materialized view (this is the default value) or repopulate_from_mvs_on_full_refresh=True in the target table. Make sure you are not activating both at the same time to avoid duplicates. Check the configuration section for more details.
  • While a dbt run --full-refresh is executed, if the materialized views use the catchup=True default, the target will get recreated and the MVs will reinsert the data sequentially. To avoid this situation, check the Full refresh with explicit targets.

dbt run --full-refresh in a target table with repopulate_from_mvs_on_full_refresh=True uses the logic from old materialized view versions, not from the SQL that is currently in the project

repopulate_from_mvs_on_full_refresh=True uses the existing MV SQL that’s already defined in ClickHouse. To make sure the new materialized view definition is used, do a dbt run for each materialized view before doing a dbt run --full-refresh in the target table.

There’s duplicate data after a run is executed

Possible reasons:
  • Both catchup=True on the materialized views and repopulate_from_mvs_on_full_refresh=True on the target table may be enabled: Keep only one of them depending on the operations you want to run. Check the configuration section for more details.
  • Target table is not defined with WHERE 0: target table should be created empty, but the internal query may insert data if the WHERE 0 is not included. Make sure the clause is included.

Data loss during active ingestion after a dbt run --full-refresh is executed

Some rows from the source table are missing in the target table after a dbt run --full-refresh is executed. ClickHouse materialized views act as insert triggers — they only capture data while they exist. During a full refresh, there is a brief window where the MV is dropped and recreated (the “blind window”). Any rows inserted into the source table during this window are not captured. Check the Behavior during active ingestion section for more details.

Debugging techniques

Check the current target of an MV in ClickHouse

Query system.tables to see where a materialized view is writing:

Check if dbt recognizes a table as a materialized view target

During a dbt run, look for this log message:
Table <table_name> is used as a target by a dbt-managed materialized view. Defaulting mv_on_schema_change to “fail” to prevent data loss.
If this message appears, dbt has detected that the table is targeted by at least one dbt-managed materialized view. If you expect this message but don’t see it, verify that:
  • The materialized view model defines {{ materialization_target_table(ref('your_target')) }} correctly
  • The materialized view model has materialized='materialized_view' in its config
  • Both the materialized view and the target table have been run at least once

Migrating from implicit to explicit target

If you have existing materialized view models using the implicit target approach and want to migrate to the explicit target approach, follow these steps: 1. Create the target table model Create a new model file with materialized='table' that defines the same schema as the current MV target table. Use a WHERE 0 clause to create an empty table. Use the same name as the current implicit materialized view model. You’ll be able to use this model now to iterate the target table.
2. Update your MV models Create new models that will include each the MV SQL and the materialization_target_table() macro call pointing to the new target table. If you were previously using the UNION ALL remove that part and the comments. For the model names you’ll have to follow this naming convention:
  • if only one MV was defined, this will have the name: <old_model_name>_mv
  • if multiple MVs were defined, each will have the name: <old_model_name>_mv_<name_in_comments>
Before in my_model.sql (implicit target, single model with UNION ALL):
After (explicit target, separate model files):
3. Iterate them as needed following the instructions in the explicit target section.

Behavior comparison between implicit and explicit target approaches

How they behave in general

Behavior during active ingestion

When iterating your models, you need to be aware of how the different operations interact with the data being inserted:
  • As ClickHouse materialized views act as insert triggers, they only capture data while they exist. If a materialized view is dropped and recreated (e.g. during a --full-refresh), any rows inserted into the source table during that window will not be processed by the materialized view. This is referred to as the materialized view being “blind”.
  • The different catchup processes are all based on INSERT INTO ... SELECT operations using the materialized views SQL and are independent of how the materialized views work. Once the INSERT starts, new data is not captured by it, but it will be captured by the attached materialized view.
The following table summarizes the safety of each operation when inserts are actively happening on the source table.

Implicit target operations

Explicit target operations

Materialized view models: Target table model:
Recommendations for production environments with active ingestion
  • Pause the ingestion during dbt operations if possible: This will make all operations safe and no data will be lost.
  • Use a deduplicating engine if possible (e.g. ReplacingMergeTree) on the target table to handle potential duplicates from catch-up overlaps.
  • Prefer ALTER TABLE ... MODIFY QUERY (regular dbt run without --full-refresh) when possible — this is always safe.
  • Be aware of problematic windows during dbt operations.

Refreshable Materialized Views

Refreshable Materialized Views are a special type of materialized view in ClickHouse that periodically re-executes the query and stores the result, similar to how materialized views work in other databases. This is useful for scenarios where you want periodic snapshots or aggregations rather than real-time insert triggers.
Refreshable materialized views can be used with both the implicit target and explicit target approaches. The refreshable config is independent of how the target table is managed.
To use a refreshable materialized view, add a refreshable config object to your MV model with the following options:

Example with implicit target

Example with explicit target

Limitations

  • When creating a refreshable materialized view (MV) in ClickHouse that has a dependency, ClickHouse does not throw an error if the specified dependency does not exist at the time of creation. Instead, the refreshable MV remains in an inactive state, waiting for the dependency to be satisfied before it starts processing updates or refreshing. This behavior is by design, but it may lead to delays in data availability if the required dependency is not addressed promptly. You should ensure all dependencies are correctly defined and exist before creating a refreshable materialized view.
  • As of today, there is no actual “dbt linkage” between the mv and its dependencies, therefore the creation order is not guaranteed.
  • The refreshable feature was not tested with multiple mvs directing to the same target model.
Last modified on June 19, 2026