INSERT ... SELECT can take the asynchronous insert queue route, not just a plain INSERT ... VALUES. This page describes when it does, how the write side is accounted, and what happens when the query is cancelled or times out.
Eligibility and routing
AnINSERT ... SELECT takes the async queue route only when all of these hold:
async_insert = 1.async_insert_select_as_async_insert = 1(the default). This is the master switch: with it off,INSERT ... SELECTis always synchronous regardless ofasync_insert. Settingcompatibilityto a version before 26.10 flips its default to0; an explicitasync_insert_select_as_async_insert = 1opts back in.- The whole
SELECTresult is a single block no larger thanasync_insert_max_data_size. - The destination is a
MergeTree-family table (an alias is followed to its target for this check).
- The result spans more than one block, exceeds
async_insert_max_data_size, or is empty. - Non-parallel quorum is used:
insert_quorumis set andinsert_quorum_parallel = 0. - The destination has dependent materialized views.
- The destination is a materialized view. The
MergeTreecheck applies to the view, not its target table, so it is not eligible. - The destination is a table function, or a remote table engine such as
Distributed. - The
SELECTreads its own destination table (INSERT INTO t SELECT ... FROM t). insert_null_as_defaultmust substitute a default for aNullableSELECTcolumn feeding a non-Nullabletarget column.- The insert is internal (refreshable materialized view refresh,
POPULATE, orCREATE TABLE ... AS SELECT). These are always synchronous and ignoreasync_insert_select_as_async_insertandasync_insert.
implicit_transaction = 1), the async route is unsupported and, by default, ClickHouse raises NOT_IMPLEMENTED rather than falling back. Only with throw_on_unsupported_query_inside_transaction = 0 does the query fall back to a synchronous insert.
Routing chosen before the async gate wins, so the SELECT side can rule the async route out on its own. With parallel_distributed_insert_select (on by default), an INSERT ... SELECT that reads from a cluster source such as s3Cluster into a replicated table runs on the distributed insert path and never reaches the async queue. Parallel replicas behave the same way.
Return mode and waiting
wait_for_async_insert works as for any async insert: 1 (the default) waits for the flush; 0 returns as soon as the block is queued.
To help a SELECT produce the single block the async route requires: optimize_trivial_insert_select caps a trivial INSERT INTO dest SELECT ... FROM source between distinct tables to max_insert_threads reading threads. Set max_insert_threads = 1 and the SELECT commonly produces one block.
Write accounting and observability
The write side is attributed to the flush query, not to the originalINSERT ... SELECT:
- The flush query owns the write
ProfileEvents(InsertedRows,InsertedBytes) and gets its own write-sidesystem.query_logrow. The originalINSERT ... SELECTrow reads0for thoseProfileEvents. To see the flush’s own write numbers, read its row insystem.query_logandsystem.asynchronous_insert_log. - With
wait_for_async_insert = 1(the default), the query’s ownwritten_rowsandwritten_bytes(and the HTTPX-ClickHouse-Summaryheader) report the rows and bytes accepted from theSELECT, not the flush query’s own total. A failed flush still surfaces as an error to the client. - With
wait_for_async_insert = 0, those numbers are not reported, because the query returns before the flush result exists. - The
WRITTEN_BYTESquota is charged once, on the flush path, and is not rolled back if the flush fails.
Cancellation and timeouts
The block is queued before the wait for the flush starts, so cancelling the query stops the client from waiting, not the queue from flushing.KILL QUERY, or reachingmax_execution_timeduring the wait, returns an error to the client, but the rows may still be written. Do not treat a cancelled or timed-out query on this route as proof that nothing was written.- With
timeout_overflow_mode = 'break'there is no error and no early return: the query waits for the flush pastmax_execution_time, up towait_for_async_insert_timeout. Breaking out early would report success before the write is confirmed.
When decisions are bound
Eligibility for the async route is decided once at query start, before the block is queued, and an up-front access check rejects an unauthorized insert early. Whether anINSERT ... SELECT takes the async route at all depends on the result’s block shape, not just on async_insert.
The destination lookup and the binding authorization then run at flush time, the same as for any asynchronous insert: a materialized view attached to the destination, or a grant revoked, between queueing and flush affects the flush.