INSERT ... SELECT queries. One setting, deduplicate_insert, controls synchronous and asynchronous inserts. INSERT ... SELECT needs extra care and has a setting of its own. See Settings that control insert deduplication.
Limitations
Uncertain insert status
The user must retry the insert operation until it succeeds. If all retries fail, it is impossible to determine whether the data was inserted or not. When materialized views are involved, it is also unclear in which tables the data may have appeared. The materialized views could be out of sync with the source table.Deduplication window limit
If more than*_deduplication_window other insert operations occur during the retry sequence, deduplication may not work as intended. In this case, the same data can be inserted multiple times.
Settings that control insert deduplication
ClickHouse deduplicates an insert only when both of the following hold:- The destination table keeps a deduplication log. This is a table-level setting.
- Deduplication is enabled for the query. This is a query-level setting.
Table-level settings
Only*MergeTree engines support deduplication on insertion.
For *ReplicatedMergeTree engines, the deduplication log is enabled by default and is controlled by the replicated_deduplication_window and replicated_deduplication_window_seconds settings. For non-replicated *MergeTree engines, the log is controlled by the non_replicated_deduplication_window setting, which is 0 by default. A plain MergeTree table therefore deduplicates nothing until you set that window to a positive value.
The settings above determine the parameters of the deduplication log for a table. The deduplication log stores a finite number of block_ids, which determine how deduplication works (see below).
replicated_deduplication_window_for_async_inserts and replicated_deduplication_window_seconds_for_async_inserts are legacy settings. Synchronous and asynchronous inserts now share one deduplication log, so replicated_deduplication_window governs both. The legacy settings only bound the old ClickHouse Keeper directory, which matters during a rolling upgrade.Query-level settings
deduplicate_insert accepts three values:
enable— deduplication is enabled for theINSERTquery.disable— deduplication is disabled for theINSERTquery.backward_compatible_choice— the decision is delegated to the legacy settingsinsert_deduplicate(synchronous inserts) andasync_insert_deduplicate(asynchronous inserts).
deduplicate_insert = disable writes no block_ids for its blocks. Such data can’t be deduplicated later, even if you retry the insert with deduplicate_insert = enable. The same holds when the destination table keeps no deduplication log: nothing is recorded, so nothing can be matched on a retry.
Precedence
- For an
INSERT ... SELECTquery,deduplicate_insert_selectdecides. See Deduplication for INSERT … SELECT. - For every other
INSERT,deduplicate_insertdecides. insert_deduplicateandasync_insert_deduplicateare read only whendeduplicate_insertisbackward_compatible_choice.
Legacy and obsolete settings
Version 26.2 also changed the defaults of
async_insert and deduplicate_blocks_in_dependent_materialized_views to enabled. The compatibility setting governs all three. If you set compatibility to a version earlier than 26.2, these settings keep their old defaults: deduplicate_insert becomes backward_compatible_choice, which hands the decision to insert_deduplicate and async_insert_deduplicate. A setting you assign explicitly is always honored and is never affected by compatibility.
How insert deduplication works
When data is inserted into ClickHouse, it splits data into blocks based on the number of rows and bytes. For tables using*MergeTree engines, each block is assigned a unique block_id, which is a hash of the data in that block. This block_id is used as a unique key for the insert operation. If the same block_id is found in the deduplication log, the block is considered a duplicate and isn’t inserted into the table.
This approach works well for cases where inserts contain different data. However, if the same data is inserted multiple times intentionally, you need to use the insert_deduplication_token setting to control the deduplication process. This setting allows you to specify a unique token for each insert, which ClickHouse uses to determine whether the data is a duplicate. insert_deduplication_token has higher priority: ClickHouse doesn’t use the hash sum of the data when the token is provided.
For INSERT ... VALUES queries, splitting the inserted data into blocks is deterministic and is determined by settings. Therefore, you should retry insertions with the same settings values as the initial operation.
Deduplication for INSERT ... SELECT
For INSERT ... SELECT queries, the SELECT part must return the same data in the same order on every attempt. Otherwise the blocks differ, the block_ids differ, and the retry isn’t recognized as a duplicate.
ClickHouse can’t verify that the source data is unchanged, but it can check whether the query itself produces a reproducible result. A SELECT is treated as stable when both of the following hold:
- The query carries an
ORDER BY ALLclause. Only the literalORDER BY ALLis recognized. A plainORDER BY <expressions>isn’t, and aUNIONof two or moreSELECTs is never stable. - The reading pipeline ends in a single stream.
insert_deduplication_token is an equivalent substitute for stability, because the token, and not the data, then identifies the insert.
The setting deduplicate_insert_select chooses what to do:
enable_when_possible and enable_even_for_bad_queries also honor deduplicate_insert: if it is disable, the query isn’t deduplicated. force_enable overrides deduplicate_insert.
Keep in mind that the selected table can be updated between retries. The two paths then behave in opposite ways:
- Without
insert_deduplication_token, theblock_ids are computed from the data. The changed result produces differentblock_ids, deduplication doesn’t occur, and the retry inserts the new data on top of whatever the first attempt already wrote. - With
insert_deduplication_token, the token alone identifies the insert. The retry is recognized as a duplicate and is dropped, even though it would have inserted different data.
Deduplication for asynchronous inserts
Asynchronous inserts (async_insert, enabled by default since version 26.2) are deduplicated on retries in the same way as synchronous inserts. deduplicate_insert controls both, so no separate switch is needed.
The two insert types also share one deduplication log and compute block_ids the same way. You can therefore switch a client between synchronous and asynchronous inserts without breaking deduplication, and a retry sent in one mode is still recognized as a duplicate of an attempt sent in the other. Moving a workload from synchronous to asynchronous inserts stays safe on a table that relies on deduplication.
Before version 26.2, deduplication of asynchronous inserts was disabled by default and was controlled by
async_insert_deduplicate. That setting is now read only when deduplicate_insert is backward_compatible_choice.Deduplication granularity
The server collects several asynchronous inserts into one batch and writes that batch as one or more parts, at least one per distinct partition key value. Deduplication works per user query, not per batch:- Each queued query contributes one deduplication token to the batch.
- A token is either the value of
insert_deduplication_token, when the query provides one, or a hash of the rows that this query contributed. - Batching doesn’t influence the tokens, and
insert_deduplication_tokendoesn’t influence how queries are grouped into batches.
- When one query in a batch is a duplicate, ClickHouse removes only the rows of that query. The rest of the batch is inserted normally. A part is skipped entirely only when every row in it is removed.
- When two queries in the same batch carry the same token, the second one is dropped before the part is written. This applies per partition: if the two queries write rows to different partitions, both survive.
DuplicatedAsyncInserts and SelfDuplicatedAsyncInserts events in system.events count these two cases.
Asynchronous inserts and materialized views
Deduplication of asynchronous inserts works together with dependent materialized views. The rule is simple: one block in, one block out. If the inner query of a view turns one input block into one output block, deduplication works. If the view emits a second block, ClickHouse throws aNOT_IMPLEMENTED exception.
A view emits a second block when its output no longer fits in one. max_block_size sets how many rows fit. Column transformations, filtering, and aggregation never add rows, so they always stay in one block. A JOIN can add rows. It works while the result stays under max_block_size, and it fails above that.
To insert through a view that emits more than one block, either set deduplicate_blocks_in_dependent_materialized_views = 0 or use synchronous inserts.
Insert deduplication with materialized views
When a table has one or more materialized views, the inserted data is also inserted into the destination of those views with the defined transformations. The transformed data is also deduplicated on retries. ClickHouse performs deduplications for materialized views in the same way it deduplicates data inserted into the target table. You can control this process using the following settings for the source table:replicated_deduplication_windowreplicated_deduplication_window_secondsnon_replicated_deduplication_window
deduplicate_blocks_in_dependent_materialized_views, which is enabled by default since version 26.2. Both switches must allow it: deduplicate_insert deduplicates the data inserted into the source table, and deduplicate_blocks_in_dependent_materialized_views additionally deduplicates the data in the dependent tables. Enable both if you want full deduplication.
When inserting blocks into tables under materialized views, ClickHouse calculates the block_id by hashing a string that combines the block_ids from the source table and additional identifiers. This ensures accurate deduplication within materialized views, allowing data to be distinguished based on its original insertion, regardless of any transformations applied before reaching the destination table under the materialized view.
Examples
Identical blocks after materialized view transformations
Identical blocks, which have been generated during transformation inside a materialized view, aren’t deduplicated because they’re based on different inserted data. Here is an example:dst table. 2 blocks from select — 2 parts on insert. The parts contains different data.
mv_dst table. That parts contain the same data, however they’re not deduplicated.
dst and mv_dst tables.
Identical blocks on insertion
dst. However, we see that only one block has been inserted into table dst. This occurred because the second block has been deduplicated. It has the same data and the key for deduplication block_id which is calculated as a hash from the inserted data. This behaviour isn’t what was expected. Such cases are a rare occurrence, but theoretically is possible. In order to handle such cases correctly, the user has to provide a insert_deduplication_token. Let’s fix this with the following examples:
Identical blocks in insertion with insert_deduplication_token
insert_deduplication_token has higher priority: ClickHouse doesn’t use the hash sum of data when insert_deduplication_token is provided.
Different insert operations generate the same data after transformation in the underlying table of the materialized view
mv_dst table. Data isn’t deduplicated because the source data was different.
Different materialized view inserts into one underlying table with equivalent data
mv_dst (as expected).
dst and mv_dst.