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.
In This Article
- At a glance
-
Breaking changes
- Ingestion and the write path
- Type and function semantics
- Query planning and output
- Security and configuration
- Removals
- Monitoring and introspection
-
Default settings that changed in 26.8
- Behaviour
- Security
- Performance (enabled by default)
- MergeTree - on-disk formats
-
What's new
- SQL surface
- Server and operations
- Observability
- Joins and text search
- Data lakes
- AI functions
Performance
Upgrade checklist
Summary
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 breaking changes, 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:
SET max_insert_threads = 1;
Or use a compatibility version 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>
Or:
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
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 changes
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:
SET input_format_read_datetime_number_as_raw_value = 1;
Date32 range extended
The Date32 range is 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.
Examples:
SELECT arrayIntersect([1], [2], [1, 1]);
-- []
SELECT arrayIntersect([1, 2], [2], [1, 1, 2]);
-- [2]
SELECT arraySymmetricDifference([1], [2], [1, 1]);
-- [2, 1]
SELECT arraySymmetricDifference([1], [2, 2]);
-- [2, 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
This 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
setting, enabled by default.
Observable behaviour changes in two ways:
-
FINALandSAMPLEwritten on the view reference are now propagated to the shard-local table instead of being ignored. -
extremesis 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 the previous behaviour:
SET optimize_trivial_view_pushdown_to_distributed = 0;
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
To restore the old behaviour:
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
MySQL source TLS credentials:
ssl_cassl_certssl_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_pemssl_cert_pemssl_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
-
nats.credential_filein 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 remote_url_allow_hosts, 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
The dictionaries_lib_path server setting is obsolete with no effect.
Apache Arrow library-based reader and writer removed
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.
The following settings are still accepted but have no effect:
input_format_arrow_use_native_reader
output_format_arrow_use_native_writer
So a query that set them to 0 to force the Apache Arrow path now silently uses the native one.
Experimental ALP(STD) codec changes
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
Asynchronous metrics can now be Map-typed, and the per-CPU-core and per-device metrics were consolidated.
For example:
OSUserTimeCPU0
OSUserTimeCPU1
...
became a single:
OSUserTimeCPU
metric holding a map from core number to value.
The same applies to the other:
OS*TimeCPU*CPUFrequencyMHz_*Temperature*EDAC*Block*_*Network(Receive|Send)*_*Disk*_**BlobsQueueEstimateAsyncLogging*QueueSize
metrics.
Downstream effects:
-
system.asynchronous_metricsgainskey_values Map(LowCardinality(String), Float64). - The
valuecolumn isNaNfor these metrics. -
system.asynchronous_metric_loglogs one row per key via a newkeycolumn. - The Prometheus endpoint exports them with a label, for example:
ClickHouse®AsyncMetrics_BlockReadBytes{device="sda"}
- Graphite's
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
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.
It is 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)
The following performance improvements are now enabled by default:
enable_adaptive_aggregatorenable_group_by_top_k_optimizationenable_packed_string_keys_in_aggregationenable_parallel_single_level_mergeread_in_order_use_virtual_rowquery_plan_push_down_volume_reducing_functionsquery_plan_short_circuit_constant_false_joinuse_query_condition_cache_for_top_kallow_distinct_partitions_independentlyallow_window_partitions_independentlyallow_creating_set_partitions_independentlyoptimize_trivial_count_with_sparsity_filterinput_format_parquet_spatial_filter_push_downquery_plan_optimize_count_from_text_indexmaterialize_statistics_on_insert
The last setting has 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 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.
This is around 4x faster for 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 is now supported.
Each pipe wraps the preceding query in a subquery, so the resulting AST matches the nested equivalent.
Example:
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
GROUPS is a SQL:2011 window frame mode.
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 functions:
parseQueryToJSON
formatQueryFromJSON
An experimental ClickHouse®_json dialect is also available behind:
enable_json_ast_dialect
Also new:
-
arr[indexes]for subscripting an array with an array of positions notHasginimergedJSONPatch
Server and Operations
SQL-defined HTTP handlers
CREATE HANDLER, ALTER HANDLER and DROP HANDLER define custom HTTP endpoints from SQL.
Handlers can be persisted locally or in Keeper.
Supporting additions include:
currentHandler()currentRequestURL()system.handlers-
http_handler_nameinsystem.query_log -
http_request_urlinsystem.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.processessystem.query_log
This is aimed at long-running operations such as:
INSERT SELECTCREATE TABLE AS SELECTPOPULATE
Atomic POPULATE
A plain:
CREATE MATERIALIZED VIEW ... POPULATE
is now locally atomic, controlled by:
materialized_views_populate_atomically
and enabled 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 starts before tables attach and stops after detach completes.
This makes:
SHOW PROCESSLIST
and:
system.stack_trace
available during startup and shutdown.
Alongside it, shutdown_wait_unfinished moved from 5 seconds to 120 seconds.
The old default was shorter than the connection poll interval.
Keeper on-disk storage
Keeper now supports on-disk storage via a custom LSM tree.
Enable with:
use_lsmt_storage = true
storage_memory_only = false
Configure through:
data_storage_path
or:
data_storage_disk
This can point at an s3_plain disk for S3.
The Keeper dashboard also gains a Cluster tab showing Raft membership as a topology graph.
HTTP URL-path access to tables
Tables can now be accessed through HTTP URL paths such as:
/database/table.format.gz?filter=a>0
This is behind a set of http_allow_* opt-ins.
Framing output formats
framing_output_format multiplexes:
- data chunks
- totals and extremes
- progress
- profile events
- logs
- exceptions
into a single HTTP stream.
Available formats include:
EventStream
JSONEachPacketBase64
JSONEachPacketString
Also new:
-
default_session_useras a server setting - Prometheus constant labels via a
<labels>element inside<prometheus> -
ALTER TABLE ... MODIFY PROJECTIONto change projection settings without a rebuild
Projection changes are applied lazily via merges.
Observability
Several useful observability improvements landed in 26.8:
system.user_query_log
Every user sees their own query log rows without needing access to:
system.query_log
create_union_system_log_tables
This creates auto-maintained all_... tables such as:
system.all_query_log
These union:
- a log table
- its rotated versions
- the same table across cluster replicas
remote, remoteSecure, cluster and clusterAllReplicas now accept a trailing SETTINGS clause.
system.mutations.finish_time
Provides mutation duration without having to infer it.
system.tables.skipping_indices_types
Provides a cheap summary of which index types a table uses.
Play UI
The Play UI now supports:
- server-side sorting
- filtering
- paging
These settings are encoded in the page URL so a shared link reproduces the result.
Joins and Text Search
IEJoin
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
parallel_full_sorting_merge shards a full sorting merge join by join-key hash across threads.
Upstream benchmarks put it around:
- 2.4x faster
- 3.3x lighter on memory
than parallel_hash, while keeping streaming memory behaviour.
The result is unordered.
New text index tokenizers
New tokenizers include:
-
japanese— MeCab -
chinese— jieba-style dictionary plus HMM -
icu— locale-aware Unicode segmentation -
splitByRegexp— keeps tokens such asC++andC#intact
Data Lakes
A number of data-lake integrations have been expanded.
BigQuery
A bigquery table function and BigQuery table engine are now available.
Authentication supports:
- OAuth token
- service account JSON
- refresh token
Snowflake Horizon
Snowflake Horizon catalog support is available for reading and writing Iceberg.
S3 Tables
The S3 Tables catalog now supports:
INSERT
Puffin
Puffin file format support has been added.
URL database engine
A URL database engine and s3_base setting complete the URL unification.
ClickHouse®-local's default database now uses it with a file:// base.
This means:
SELECT * FROM 'https://example.com/data.csv'
works directly.
AI Functions
The experimental:
aiSimilarityaiFilteraiRedact
functions were hardened in this release.
Insecure http endpoints to remote hosts are denied by default.
Outbound calls per query are bounded at:
1000
Provider error responses are also sanitised before logging.
Performance
ClickHouse® 26.8 contains 127 performance entries, with the majority enabled by default.
The improvements with the broadest reach include:
Aggregation
A new adaptive parallel GROUP BY allows each thread to aggregate into its own cache-resident hash table until it hits a threshold, then freezes it.
Other aggregation improvements include:
- bounded-heap pruning for
GROUP BY ... ORDER BY ... LIMIT - smaller hash-table cells for single-
Stringkeys - parallelised final merge of single-level tables
- aggregations without aggregate functions now use
HashSetrather thanHashMap
The latter can be up to 1.8x faster.
Reads
Lazy materialization for Parquet on object storage can significantly reduce I/O.
For example, on a 200 MB S3 file:
ORDER BY ... LIMIT 10
read:
3.3 MB
instead of:
171 MB
That represents:
- 51x less I/O
- 8x faster
read_in_order_use_virtual_row is enabled by default and reduces peak memory when reading in primary key order across many parts.
The Parquet V3 reader also gains dictionary-page-based row group skipping.
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.
This skips the hash scatter entirely.
The Important Trade-Off
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
Check for dictionaries using the removed library source:
SELECT name, source
FROM system.dictionaries
WHERE source ILIKE '%library%';
Check non-default settings you are carrying:
SELECT name, value, default
FROM system.settings
WHERE changed;
And:
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 / MaterializedPostgreSQL databases against hosts outside
remote_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 pins.
After Upgrading
Watch part counts and merge queue depth in:
system.parts
system.merges
for the max_insert_threads effect.
Also monitor:
system.errors
for:
UNACCEPTABLE_URL
ILLEGAL_COLUMN
UNKNOWN_ELEMENT_IN_CONFIG
BAD_ARGUMENTS
These cover most of the new rejections.
If you want to stage the transition:
SET compatibility = '26.7';
This 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 are:
1. max_insert_threads defaulting to auto
This changes part creation patterns and insert row order on every:
INSERT SELECT
2. Asynchronous metric consolidation
This can break dashboards silently rather than loudly.
3. Patch parts v2 format
This 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, and the introspection port — address real gaps.
And the 127 performance improvements, mostly enabled by default, represent a meaningful return on the upgrade work.
The key lesson is simple:
Treat ClickHouse® 26.8 as a real upgrade project, not just a version bump.
Review the defaults, test workload behaviour, validate observability, and plan the rollout carefully.
Read more... https://www.ch-ops.io/blog/clickhouse-268-lts-release-whats-new-and-why-it-matters
Top comments (0)