ClickHouse® 26.8 is the newest LTS release, and it is a substantial one: 21 backward-incompatible changes, 49 new features, 127 performance improvements, and around 30 settings that changed their default value.
This article covers 26.8 on its own - what landed, what breaks, and what to verify before you upgrade.
Release status at time of writing (27 August 2026). ClickHouse® 26.8 has been announced, but the release is not fully published yet and the upstream changelog still marks the 26.8 section as in progress. Confirm the current release status before you plan an upgrade window.
At a glance
| Category | Entries |
|---|---|
| Backward incompatible changes | 21 |
| New features | 49 |
| Experimental features | 48 |
| Performance improvements | 127 |
| Improvements | 138 |
| Bug fixes | 556 |
Breaking changes
All 21, grouped by what they affect.
Ingestion and the write path
max_insert_threads now defaults to auto. It resolves to the number of CPU cores available to the server, parallelising INSERT SELECT by default. It can also parallelise the writing side of a plain INSERT where the destination write path can safely fan out.
Two consequences worth planning for: the number of parts created by such queries changes, and so does the order of inserted rows. If you have tight parts_to_throw_insert thresholds, or anything depending on insertion order - a non-deterministic ORDER BY tie-break, _part or _block_number assumptions, a ReplacingMergeTree without a proper version column - test this before rolling out.
A detail that catches people diffing configurations: the declared default is 0 in both 26.7 and 26.8. What changed is the setting's type, from UInt64 to MaxThreads. Under UInt64, both 0 and 1 meant single-threaded; under MaxThreads, 0 resolves to auto. So the value column in system.settings reads 0 before and after, while the behaviour has flipped. Upstream's own compatibility mapping records the change as 1 → 0 for this reason - 1 is what you now set to get the old behaviour, not what the declaration used to say.
Restore with max_insert_threads = 1, or compatibility below 26.8.
Lightweight UPDATE patch parts use a new v2 on-disk format. They are now sorted by (sorting_key..., _block_number, _block_offset) and applied with a new merging algorithm. Peak memory during apply is bounded by the largest equal-sort-key run rather than the full patch, and updates crossing merge boundaries no longer fall back to an in-memory Join apply. Old-format patch parts remain readable.
For replicated clusters this requires a rolling-upgrade pin. Note that patch_parts_version is a MergeTree setting rather than a session setting:
<merge_tree>
<patch_parts_version>v1</patch_parts_version>
</merge_tree>ALTER TABLE my_table MODIFY SETTING patch_parts_version = 'v1';Hold that until every replica is on 26.8, then remove it.
Object-storage disk transactions now use the metadata storage's native transactions by default instead of the previous fake transactions. The upstream changelog does not explain why this is listed as backward-incompatible, so treat it as worth testing if you run object-storage disks.
disable_insertion_and_mutation now also stops background consumption from Kafka, RabbitMQ and NATS tables, while still permitting direct writes to external storage. Gated Kafka2, NATS and RabbitMQ tables no longer initialise consumers for a direct SELECT. Separately, message_queue_disable_insertion now requires a server restart to take effect.
Type and function semantics
Unquoted JSON numbers are now Unix timestamps. In JSONEachRow and similar formats, an unquoted number for a DateTime or DateTime64 column is read as a Unix timestamp with optional sub-second precision, consistent with Values, CAST and toDateTime64.
Previously a fractional value such as 1703363853.035 was rejected outright, and a bare integer such as 1703363853 was read as the raw scaled value of DateTime64, producing a 1970 timestamp. Quoted strings and ClickHouse®'s own JSON output are unaffected.
For most pipelines this is a silent fix. If yours was compensating for the old raw-value behaviour, it will now double-correct. Restore with input_format_read_datetime_number_as_raw_value = 1.
Date32 range extended from [1900-01-01, 2299-12-31] to [0000-01-01, 9999-12-31], matching DateTime64. Parsing and conversions accept the extended range instead of silently clamping.
The compatibility note matters: in toDate32(N), values in [120530, 2932896] are now interpreted as day numbers - dates from 2300-01-01 to 9999-12-31 - rather than Unix timestamps in early 1970. Numbers below the day number of 0000-01-01, and timestamps after 9999-12-31, saturate to the new boundaries.
arrayIntersect and arraySymmetricDifference deduplication fixed. A value repeated inside a single argument is no longer treated as though it appeared in several arguments. A value now counts for an argument only when it was present in every argument before it, so the result contains exactly the values present in all arguments.
SELECT arrayIntersect([1], [2], [1, 1]); -- [] (was [1])
SELECT arrayIntersect([1, 2], [2], [1, 1, 2]); -- [2] (was [1, 2])
SELECT arraySymmetricDifference([1], [2], [1, 1]); -- [2, 1] (was [2])
SELECT arraySymmetricDifference([1], [2, 2]); -- [2, 1] (was [1])arrayUnion is unaffected. As part of the same change, arrayIntersect builds its hash table from the smallest argument rather than all of them - up to 1.85x faster and a third less memory when argument sizes differ significantly.
Window functions over AggregateFunction columns are rejected. A window PARTITION BY or ORDER BY over an AggregateFunction column now raises ILLEGAL_COLUMN, as top-level ORDER BY over such a column already did. Previously at least one analyzer accepted it, and window PARTITION BY partitioned differently depending on max_threads.
The refusal covers states nested in Array, Tuple, Map, Variant or SimpleAggregateFunction. A SimpleAggregateFunction over an ordinary type, QBit, and GROUP BY or DISTINCT over a state, are all unaffected.
Timespan settings that overflow Int64 microseconds are rejected. Applies to millisecond and second settings.
Query planning and output
Trivial views over Distributed tables are pushed to the shards. For a view whose body is a plain SELECT over a single Distributed table, the whole outer query now goes to the shards. This is the new optimize_trivial_view_pushdown_to_distributed, enabled by default.
Observable behaviour changes in two ways: FINAL and SAMPLE written on the view reference are now propagated to the shard-local table instead of being ignored, and extremes is not reported on single-shard clusters. If you had views where FINAL was silently a no-op, results will change. Set the setting to 0 to restore.
EXPLAIN SYNTAX returns a single record. The reformatted query comes back as one String value with embedded newlines rather than one record per line, so SELECT count() FROM (EXPLAIN SYNTAX ...) returns 1.
EXPLAIN SYNTAX single_record = 0 SELECT 1;
-- or session-wide:
SET explain_syntax_single_record = 0;Other EXPLAIN kinds - PLAN, PIPELINE, AST - keep their per-line tree output.
Security and configuration
A clear theme this release: the server no longer opens filesystem paths supplied from SQL, because it opens them with its own privileges.
MySQL source TLS credentials (ssl_ca, ssl_cert, ssl_key) can no longer be given as file paths from SQL - not in CREATE NAMED COLLECTION, query arguments, or CREATE DICTIONARY. Paths remain supported in the server configuration file. Elsewhere, pass the contents via the new ssl_ca_pem, ssl_cert_pem and ssl_key_pem parameters, which are masked in logs and SHOW output the way passwords are.
NATS credentials move inline. The new nats_credentials setting takes the same payload as a .creds file. nats_credential_file is no longer accepted from SQL - it can only be set in a named collection defined in the server configuration, or as nats.credential_file in the server configuration itself. A query may replace a configured path with inline nats_credentials unless the operator pinned it with <nats_credential_file overridable="false">. Tables created before the restriction keep working.
PostgreSQL database engines respect remote_url_allow_hosts. The PostgreSQL and MaterializedPostgreSQL database engines now honour it, as the table engine, table function and DDL-created dictionaries already did. With it configured, CREATE DATABASE and user-issued ATTACH DATABASE pointing at disallowed hosts fail with UNACCEPTABLE_URL. Existing databases still load at startup.
SYSTEM ... CACHE ON CLUSTER privilege checks are granular. Each command now uses its own privilege rather than the SYSTEM DROP CACHE group. This lets a holder of a single granular cache privilege run its matching command, and prevents a holder of the group from running SYSTEM SYNC FILESYSTEM CACHE ON CLUSTER without SYSTEM SYNC FILESYSTEM CACHE.
include_from no longer defaults to /etc/metrika.xml. That file was previously used for configuration substitutions whenever it existed, even with nothing in the ClickHouse® configuration referring to it. If you relied on it, add the element explicitly:
<include_from>/etc/metrika.xml</include_from>Separately loaded users.xml and XML dictionary configs each need their own include_from element.
Removals
The library dictionary source is gone. SOURCE(LIBRARY(...)) now fails with UNKNOWN_ELEMENT_IN_CONFIG, and the dictionaries_lib_path server setting is obsolete with no effect.
The Apache Arrow library-based reader and writer for Arrow and ArrowStream are removed. The native ClickHouse® implementation, default since 26.7, is now the only one. input_format_arrow_use_native_reader and output_format_arrow_use_native_writer are still accepted but have no effect - so a query that set them to 0 to force the Apache Arrow path now silently uses the native one.
The experimental ALP(STD) codec now performs Float32 scaling arithmetic in Float64. This improves compression ratios and eliminates exception-heavy compression of decimal data, but Float32 values written by earlier versions may decode 1 ULP differently.
Monitoring and introspection
Asynchronous metrics can now be Map-typed, and the per-CPU-core and per-device metrics were consolidated. OSUserTimeCPU0, OSUserTimeCPU1 and so on became a single OSUserTimeCPU metric holding a map from core number to value.
The same applies to the other OS*TimeCPU* metrics, CPUFrequencyMHz_*, Temperature*, EDAC*, Block*_*, Network(Receive|Send)*_*, Disk*_*, *BlobsQueueEstimate and AsyncLogging*QueueSize.
Downstream effects:
system.asynchronous_metricsgainskey_values Map(LowCardinality(String), Float64); thevaluecolumn isNaNfor these metricssystem.asynchronous_metric_loglogs one row per key via a newkeycolumn- The Prometheus endpoint exports them with a label, e.g.
ClickHouse®AsyncMetrics_BlockReadBytes{device="sda"} - The Graphite
MetricsTransmittersends them as<prefix>.<Metric>.<key>
This is the change most likely to break existing dashboards. Panels keyed on the old metric names will not error - they will simply return nothing, which is easy to miss.
system.users.valid_until changed type from Array(DateTime) to Array(DateTime64(0)), so deadlines beyond the year 2106 are represented exactly. Tooling reading this column needs to handle the new type. This arrived alongside a new VALID FOR <interval> clause on CREATE USER and ALTER USER, a shorthand for VALID UNTIL where the deadline is computed at query execution time and stored in VALID UNTIL form.
Default settings that changed in 26.8
Around 30 core settings and several MergeTree settings changed their defaults. These do not appear in the breaking-change list but will change behaviour on upgrade.
Behaviour
| Setting | Previous | New |
|---|---|---|
max_insert_threads | 1 - single-threaded | auto - all available cores |
input_format_read_datetime_number_as_raw_value | true | false |
optimize_trivial_view_pushdown_to_distributed | - | true |
explain_syntax_single_record | false | true |
filesystem_cache_wait_for_concurrent_download_timeout_milliseconds | 60000 | 1000 |
Security
| Setting | Previous | New |
|---|---|---|
ai_function_allow_insecure_endpoint | true | false |
ai_function_max_api_calls_per_query | 0 (unbounded) | 1000 |
Performance (enabled by default)
enable_adaptive_aggregator, enable_group_by_top_k_optimization, enable_packed_string_keys_in_aggregation, enable_parallel_single_level_merge, read_in_order_use_virtual_row, query_plan_push_down_volume_reducing_functions, query_plan_short_circuit_constant_false_join, use_query_condition_cache_for_top_k, allow_distinct_partitions_independently, allow_window_partitions_independently, allow_creating_set_partitions_independently, optimize_trivial_count_with_sparsity_filter, input_format_parquet_spatial_filter_push_down, query_plan_optimize_count_from_text_index, and materialize_statistics_on_insert (with a 25 GiB table-size cap).
MergeTree - on-disk formats
| Setting | Previous | New | Compatibility |
|---|---|---|---|
text_index_serialization_version | v1_with_codec | v2_with_positions | Older servers cannot read the new format. Pin to v1 during a rolling upgrade |
packed_skip_index_max_bytes | 0 | 1 MiB | New parts only; still readable by older servers, though pre-26.6 ignores packed indices for pruning |
compute_exact_num_defaults_for_sparse_columns | false | true | The flag in serialization.json is ignored by older versions, so parts survive a downgrade |
text_index_posting_list_apply_mode | materialize | lazy | Posting lists decoded on demand |
text_index_max_memory_usage_before_flush | unlimited | 1 GiB | Memory-based flush trigger for index builders |
One protocol-level note: the native protocol changed how String columns are transmitted - a separate stream of cumulative byte offsets followed by concatenated data, once both peers are on revision 54489 or later. Around 4x faster client-side reads. It is negotiated by protocol revision, so old clients and servers are unaffected, but maintainers of custom native-protocol clients should handle the new revision.
What's new
SQL surface
Pipe operators. GoogleSQL-style |> chaining, where each pipe wraps the preceding query in a subquery, so the resulting AST matches the nested equivalent:
FROM events
|> WHERE status = 'active'
|> AGGREGATE count() AS total GROUP BY user_id
|> ORDER BY total DESC
|> LIMIT 10;In a query starting with FROM, SELECT is now optional and defaults to SELECT *.
GROUPS window frame mode (SQL:2011). Frame boundaries count whole peer groups rather than physical rows or value distances, completing the set alongside ROWS and RANGE.
Query AST as JSON. New parseQueryToJSON and formatQueryFromJSON functions, plus an experimental ClickHouse®_json dialect behind enable_json_ast_dialect.
Also new: arr[indexes] for subscripting an array with an array of positions, notHas, gini, and mergedJSONPatch.
Server and operations
SQL-defined HTTP handlers. CREATE HANDLER, ALTER HANDLER and DROP HANDLER define custom HTTP endpoints from SQL, persisted locally or in Keeper. Supporting additions: currentHandler(), currentRequestURL(), a system.handlers table, and http_handler_name / http_request_url columns in system.query_log. For teams maintaining small API services that exist only to expose a query over HTTP, this is worth evaluating.
run_query_in_background. The server accepts the query, returns immediately, and runs it to completion regardless of what happens to the connection. Track it by query_id in system.processes and system.query_log. Aimed at long INSERT SELECT, CREATE TABLE AS SELECT, and POPULATE operations.
Atomic POPULATE. A plain CREATE MATERIALIZED VIEW ... POPULATE is now locally atomic, controlled by materialized_views_populate_atomically and on by default: rows inserted through the same server during population are no longer missed or duplicated. This applies to the local insert path only and requires a snapshot-capable source - the MergeTree family or Memory. CREATE OR REPLACE, REPLACE, and Replicated databases keep the legacy path. POPULATE also works with TO now.
Introspection port. A native-protocol TCP listener that starts before tables attach and stops after detach completes, so SHOW PROCESSLIST and system.stack_trace are available during startup and shutdown. Alongside it, shutdown_wait_unfinished moved from 5 seconds to 120 - the old default was shorter than the connection poll interval.
Keeper on-disk storage via a custom LSM tree, enabled with use_lsmt_storage = true and storage_memory_only = false, configured through data_storage_path or data_storage_disk (point it at an s3_plain disk for S3). The Keeper dashboard gains a Cluster tab showing Raft membership as a topology graph.
HTTP URL-path access to tables - /database/table.format.gz?filter=a>0 - behind a set of http_allow_* opt-ins.
Framing output formats (framing_output_format) multiplex data chunks, totals and extremes, progress, profile events, logs and exceptions into a single HTTP stream. Available as EventStream (server-sent events), JSONEachPacketBase64 and JSONEachPacketString.
Also: default_session_user as a server setting, Prometheus constant labels via a <labels> element inside <prometheus>, and ALTER TABLE ... MODIFY PROJECTION to change projection settings without a rebuild (applied lazily via merges).
Observability
system.user_query_log- every user sees their own query log rows without needing access tosystem.query_logcreate_union_system_log_tables- auto-maintainedall_...tables such assystem.all_query_log, unioning a log table, its rotated versions, and the same table across cluster replicas.remote,remoteSecure,clusterandclusterAllReplicasnow accept a trailingSETTINGSclausesystem.mutations.finish_time- mutation duration without inferring itsystem.tables.skipping_indices_types- a cheap summary of which index types a table uses- Play UI - server-side sorting, filtering and paging, encoded in the page URL so a shared link reproduces the result
Joins and text search
IEJoin is a sort-based algorithm for ON clauses containing two inequality comparisons. Previously such joins ran as a CROSS JOIN with a filter, and only as INNER. Enable by adding ie_join to join_algorithm.
parallel_full_sorting_merge shards a full sorting merge join by join-key hash across threads. Upstream benchmarks put it around 2.4x faster and 3.3x lighter on memory than parallel_hash, while keeping streaming memory behaviour. The result is unordered.
New text index tokenizers: japanese (MeCab), chinese (jieba-style dictionary plus HMM), icu (locale-aware Unicode segmentation), and splitByRegexp, which keeps tokens such as C++ and C# intact.
Data lakes
A bigquery table function and BigQuery table engine, authenticating by OAuth token, service account JSON, or refresh token. Snowflake Horizon catalog support for reading and writing Iceberg. The S3 Tables catalog now supports INSERT. Puffin file format support. A URL database engine and s3_base setting complete the URL unification - ClickHouse®-local's default database now uses it with a file:// base, so SELECT * FROM 'https://example.com/data.csv' works directly.
AI functions
The experimental aiSimilarity, aiFilter and aiRedact functions were hardened this release: insecure http endpoints to remote hosts are denied by default, outbound calls per query are bounded at 1000, and provider error responses are sanitised before logging.
Performance
127 performance entries, the majority enabled by default. The ones with the broadest reach:
Aggregation. A new adaptive parallel GROUP BY where each thread aggregates into its own cache-resident hash table until it hits a threshold, then freezes it. Bounded-heap pruning for GROUP BY ... ORDER BY ... LIMIT. Smaller hash-table cells for single-String keys. Parallelised final merge of single-level tables. Aggregations without aggregate functions now use HashSet rather than HashMap, up to 1.8x faster.
Reads. Lazy materialization for Parquet on object storage: on a 200 MB S3 file, an ORDER BY ... LIMIT 10 read 3.3 MB instead of 171 MB - 51x less I/O and 8x faster. read_in_order_use_virtual_row on by default reduces peak memory when reading in primary key order across many parts. Dictionary-page-based row group skipping in the Parquet V3 reader.
Per-partition processing. DISTINCT, window functions and IN (subquery) set building can now keep each partition's rows in a single stream when the partition expression is a deterministic function of the relevant columns, skipping the hash scatter entirely.
The trade-off worth stating plainly: query plans and resource usage will change after upgrading, even for queries you did not touch. Most workloads benefit, but plan for a period of observation rather than assuming parity.
Upgrade checklist
Before upgrading
-- Dictionaries using the removed library source
SELECT name, source FROM system.dictionaries WHERE source ILIKE '%library%';
-- Non-default settings you're carrying
SELECT name, value, default FROM system.settings WHERE changed;
SELECT name, value FROM system.server_settings WHERE changed;Then check by hand:
- Named collections and dictionaries using MySQL
ssl_ca/ssl_cert/ssl_keypaths - NATS tables or collections using
nats_credential_file - Reliance on the implicit
/etc/metrika.xmlsubstitutions file PostgreSQL/MaterializedPostgreSQLdatabases against hosts outsideremote_url_allow_hosts- Ingestion clients writing unquoted epoch numbers into
DateTime64columns viaJSONEachRow - Queries using
toDate32on epoch-second values - Anything parsing
EXPLAIN SYNTAXoutput - Tooling reading
system.users.valid_until - Dashboards keyed on individual per-CPU or per-device asynchronous metric names
During a rolling upgrade
Pin patch_parts_version = 'v1' and text_index_serialization_version = 'v1_with_codec' until every replica is on 26.8, then remove both.
After upgrading
Watch part counts and merge queue depth in system.parts and system.merges for the max_insert_threads effect, and system.errors for UNACCEPTABLE_URL, ILLEGAL_COLUMN, UNKNOWN_ELEMENT_IN_CONFIG and BAD_ARGUMENTS, which cover most of the new rejections.
If you want to stage the transition, SET compatibility = '26.7' restores the majority of the default flips in one move, letting you upgrade the binaries first and enable the new behaviour deliberately afterwards. It does not cover removals or type changes - those need code fixes, which the checks above should surface.
Summary
The three changes most likely to affect a running deployment:
max_insert_threadsdefaulting toauto- changes part creation patterns and insert row order on everyINSERT SELECT- Asynchronous metric consolidation - breaks dashboards silently rather than loudly
- The patch parts v2 format - requires a rolling-upgrade pin on replicated clusters
Beyond that, 26.8 is a strong release. The security tightening around credential handling is overdue and welcome, the operational additions - SQL-defined handlers, background queries, atomic POPULATE, the introspection port - address real gaps, and 127 performance improvements mostly enabled by default is a meaningful return on the upgrade work.



