ClickHouse 26.9: LIMIT AFTER/UNTIL, incremental refreshable views and ZSTD by default
26.9 adds boundary-based LIMIT, APPEND INCREMENTAL refreshes, DISTINCT spilling and scoped tokens, and changes the default compression from LZ4 to ZSTD(3) while removing the old analyzer, WINDOW VIEW and CatBoost.
Source: clickhouse.com
Why it matters: 26.9 changes two things that every installation has: the default compression is now ZSTD(3) instead of LZ4 for network traffic and for larger MergeTree parts, and the pre-analyzer query path can no longer be switched back on. Around that sit practical SQL additions (LIMIT ... AFTER / UNTIL, a spilling DISTINCT, incremental refreshable views) and a token mechanism that lets you hand an application a short-lived, scoped credential.
Sources: the official 26.9 changelog (released 2026-09-21) and the release post, which counts 56 new features, 135 performance optimizations and 464 bug fixes. PR numbers link to ClickHouse/ClickHouse. These features are newer than the 26.4 build used for this site’s tested examples, so the snippets below are quoted from the changelog and were not run here.
Features worth trying
-
LIMITby boundary condition (#99508). Following the stream order, so useORDER BYto define it. The changelog examples:SELECT number FROM numbers(10) ORDER BY number LIMIT 3 AFTER number >= 5; -- 5, 6, 7 SELECT number FROM numbers(10) ORDER BY number LIMIT UNTIL number >= 3; -- 0, 1, 2 SELECT number FROM numbers(10) ORDER BY number LIMIT AFTER number >= 2 UNTIL number >= 6; -- 2, 3, 4, 5 SELECT number FROM numbers(10) ORDER BY number LIMIT 2 AFTER number IN (2, 6) ALL; -- 2, 3, 6, 7It is the clean way to cut a session out of an ordered event stream, for example “the rows after a login until the next logout”, without a window-function detour.
-
Incremental refreshable materialized views with
REFRESH ... APPEND INCREMENTAL(#114152): only rows committed to the source table since the previous refresh are appended, instead of re-reading the whole source. Into anIcebergtarget the refresh is exactly-once, because the cursor is committed atomically inside the snapshot summary (#114823). This is the feature to look at if you run a full-refresh view that grows heavier every day on append-only data. -
DISTINCTcan spill to disk like aggregation and sorting do (#116569). Setmax_bytes_before_external_distinctormax_bytes_ratio_before_external_distinct; by default spilling starts once the query uses half the available memory under the applicable limit. High-cardinalityDISTINCTthat used to end in a memory-limit error now degrades to slower instead. -
CREATE TOKEN(#116957) generates a random secret, adds it to the current user as an additional authentication method and returns it with its expiry, for exampleCREATE TOKEN VALID FOR INTERVAL 30 DAY GRANTS (SELECT ON db.table). The token is tied to its user, limited in time and privileges, and stops working when the user is deleted; without an explicit lifetime it lives forcreate_token_default_ttl_seconds(30 minutes by default). It needs the newCREATE TOKENprivilege. TheGRANTS (...)clause is also available onCREATE USERandALTER USER ... ADD IDENTIFIED, limiting a session to the intersection with the listed grants. -
Smaller additions: SQL-standard linear regression aggregates
regr_slope,regr_intercept,regr_r2and friends (#119347); bracket syntax forJSONsubcolumns,json['key1']['key2'](#107039);MergeTreetable-size limitsmax_table_size_rowsandmax_table_size_bytes_*(#115069); themax_tablesdatabase setting;system.session_query_idsfor finding “the queries I just ran” (#110536); akeyValuePairstext-index tokenizer that answersmap['key'] = 'value'from the index (#115579); a jemalloc fragmentation profiler insystem.jemalloc_sampled_allocations(#115456);S3Queuemode = 'exclusive'for single-server ingestion without Keeper coordination (#110552); andBACKUP/RESTOREofWORKLOADandRESOURCEentities (#108431). -
Promoted:
Nullable(Tuple(...))is generally available and on by default (#119222), and theai*functions (aiGenerate,aiClassify,aiExtract,aiEmbed,aiSimilarityand others) are beta and no longer needallow_experimental_ai_functions(#116227).
Performance
min,maxandcountwithoutGROUP BYor filters can be answered directly from per-part column statistics, collected automatically for suitable columns through theauto_statistics_typesMergeTreesetting, without reading data (#115738).- Joins. An alternative row-major payload layout for hash joins speeds up output reconstruction (
enable_hash_join_row_store, on by default, #104884).query_plan_lift_predicate_across_joincopies equi-join filter conjuncts acrossMergeTreeinputs for better primary-key pruning; the changelog reports some queries 70 to 80 times faster (#108377).IEJoinnow picks its two key conditions by estimated selectivity. - Aggregation. The adaptive
GROUP BYfalls back to the ordinary algorithm when rare keys or aggregate arguments are too wide to copy profitably (#115755), and the old sharded aggregator is removed in its favor. - Text search.
LIKEandILIKEpatterns can use text indexes (#114728), andLIKE ... ESCAPEcan use skip indexes. Phrase search stores positions in packed blocks and reads only the blocks that cover candidate rows. - Random numbers and UUIDs.
rand,rand64,randomString,generateUUIDv4,generateUUIDv7and friends generate bytes with a reduced-round AES in counter mode (#116133). The native protocol batches end-of-query control messages into one socket write, saving a round trip per query for high-latency clients.
Plan before you upgrade
- Default compression is
ZSTD(3)(#108786, #119825). Client/server and server/server traffic uses it uniformly. ForMergeTreecolumn data the built-in default is size-aware:LZ4for parts below 100 MB andZSTD(3)at or above it, judged by the part size known at write time (for merges and mutations, the size of the source parts). Auxiliary files such aschecksums.txt,StripeLogstreams and the files ofSetandJoinengines switch toZSTD(3). Expect better ratios and a modest CPU increase; if your CPUs were already the bottleneck on large merges, benchmark before rolling out. Rollback:compatibility = '26.8'ornetwork_compression_method = 'LZ4'for the network,CODEC(LZ4)on columns or the server<compression>default forMergeTreedata. Reading stays correct either way because compressed frames are self-describing. - The analyzer cannot be disabled (#118629).
enable_analyzerandallow_experimental_analyzerare obsolete, setting them to0is rejected, andcompatibilityno longer reverts them. Check profiles,users.xmland client scripts for either name. - Removals that can stop a server from starting or break DDL.
WINDOW VIEWandWATCHare removed (#114747); drop window views before upgrading, because a server finding one in its metadata fails to start. The CatBoost integration is removed (catboostEvaluate,system.models,SYSTEM RELOAD MODEL); revokeSYSTEM RELOAD MODELfrom users and roles beforehand. TheWasmEdgeengine for WebAssembly UDFs is removed, and a server configured withwebassembly_udf_engine = 'wasmedge'will not start. Theread_resource/write_resourcedisk options are replaced byCREATE RESOURCE(#115286). - Privilege checks tightened.
BACKUP ... TO Disk(...)andRESTORE ... FROM Disk(...)now needWRITE ON DISK/READ ON DISK(#117406); reading aFilesource withrename_files_after_processingneedsWRITE ON FILEas well (#117895). Per-codec settingsenable_<codec>_codecreplaceallow_experimental_codecs, which no longer enables anything. - Result changes.
runningConcurrencyis now treated as non-deterministic to fix wrong results underLEFT JOINand lazy evaluation (#114403), so it can no longer appear in a sorting key, partition key or text-index preprocessor, and queries may be slower.GROUP BY ALLvalidation ofVariant/Dynamickeys can be turned off withvalidate_group_by_all_key_types = 0. Theinterfaceandhttp_methodcolumns ofsystem.query_logand related tables are nowEnum8.
If you do one thing before upgrading, search your metadata for window views and your configs for enable_analyzer and wasmedge: those are the items that fail loudly at startup or on the first query. Everything else is a performance or behavior change you can measure on a staging server using the compatibility setting.
Discussion
Comments live in GitHub Discussions. Sign in with GitHub to join in.