Another month goes by, which means itβs time for another release!
The ClickHouse 26.4 release contains 39 new features π· 45 performance optimizations π 238 bug fixes π
This release sees more features become SQL compatible, faster COUNT DISTINCT, even prettier EXPLAIN, and more!
New contributors
A special welcome to all the new contributors in 26.4!Β The growth of ClickHouse's community is humbling, and we are always grateful for the contributions that have made ClickHouse so popular.
Below are the names of the new contributors:
Alexander Kuleshov, Alsu, Anton Frost, Aruj Bansal, Asya, ClickGap AI Bot, Denys Melnyk, Diego Gomes Tome, Dustin Healy, Evgeny Kuzin, Farid Adam, Francisco Garcia Florez, Gagan Dhakrey, Gleb Popov, Groene AI, Ivan Mantova, Jaap Elst, Jack Knudson, James Cunningham, JingYanchao, K, Kc Balusu, Matheus Nerone, Michael Russell, MukundaKatta, Nikita Semenov, Pavel Kravtsov, Peng, RenzoMXD, Sergey Veletskiy, Takumi Hara, Timothy Kurniawan, Wenyu Chen, XiaoBinMu, Yuri Fedoseev, ashrithb, asyablue22, dwagner-decix, egor romanov, groeneai, liuguangliang, manerone, nerve-bot, simonhammes, sourcelliu, xiaobin
Hint: if youβre curious how we generate this listβ¦ here.
You can also view the slides from the presentation.
SQL compatibility: VALUES as table expression, EXTRACT, SET TIME ZONE
The 26.4 release sees more features become compatible with standard SQL syntax. Weβll look at just a few of them, but you can see the presentation slide deckΒ for more.
VALUES as a table expression
Contributed by Desel72
First up, VALUES. Before this release, you could call it like this:
SELECT *
FROM VALUES((1, 'a'), (2, 'b'), (3, 'c'));ββc1ββ¬βc2ββ
1. β 1 β a β
2. β 2 β b β
3. β 3 β c β
ββββββ΄βββββWhereas now, we can also call it as a table expression, as shown below:
SELECT *
FROM (VALUES (1, 'a'), (2, 'b'), (3, 'c'));ββc1ββ¬βc2ββ
1. β 1 β a β
2. β 2 β b β
3. β 3 β c β
ββββββ΄βββββWe can now also alias columns, which is useful when using VALUESΒ inΒ a join query. For example, instead of the following:
SELECT
c.c2,
o.c2
FROM VALUES((1, 'Alice'), (2, 'Bob')) AS c
INNER JOIN VALUES((1, 250), (2, 100), (1, 75)) AS o
ON c.c1 = o.c1;ββc2βββββ¬βo.c2ββ
1. β Alice β 250 β
2. β Alice β 75 β
3. β Bob β 100 β
βββββββββ΄βββββββWe can name the columns, which makes the query easier to understand:
SELECT c.name, o.amount
FROM (VALUES (1, 'Alice'), (2, 'Bob')) AS c(id, name)
JOIN (VALUES (1, 250), (2, 100), (1, 75)) AS o(customer_id, amount)
ON c.id = o.customer_id;ββnameβββ¬βamountββ
1. β Alice β 250 β
2. β Alice β 75 β
3. β Bob β 100 β
βββββββββ΄βββββββββEXTRACT
Contributed by Alexey Milovidov
The EXTRACTΒ operator (used when working with dates) now supports PostgreSQL-style units, as shown in the following query:
SELECT
EXTRACT(EPOCH FROM now()) AS epoch,
EXTRACT(DOW FROM today()) AS dayOfWeek,
EXTRACT(DOY FROM today()) AS dayOfYear,
EXTRACT(ISODOW FROM today()) AS isoDOW,
EXTRACT(ISOYEAR FROM today()) AS isoYear,
EXTRACT(WEEK FROM today()) AS isoWeek,
EXTRACT(CENTURY FROM today()) AS century,
EXTRACT(DECADE FROM today()) AS decade,
EXTRACT(MILLENNIUM FROM today()) AS millennium;Row 1:
ββββββ
epoch: 1777992683
dayOfWeek: 2
dayOfYear: 125
isoDOW: 2
isoYear: 2026
isoWeek: 19
century: 21
decade: 202
millennium: 3SET TIME ZONE
Contributed by phulv94
There is also a new SQL standard alias for setting the time zone. First, letβs check my current time zone:
SELECT timezone(), formatDateTime(now(), '%Y-%m-%d %H:%M:%S %z');ββtimezone()βββββ¬βformatDateTimβ―H:%M:%S %z')ββ
1. β Europe/London β 2026-05-05 15:May:47 +0100 β
βββββββββββββββββ΄βββββββββββββββββββββββββββββAnd now, weβll set it to be Amsterdam instead:
SET TIME ZONE 'Europe/Amsterdam';And if we re-run the above query:
ββtimezone()ββββββββ¬βformatDateTimβ―H:%M:%S %z')ββ
1. β Europe/Amsterdam β 2026-05-05 16:May:59 +0200 β
ββββββββββββββββββββ΄βββββββββββββββββββββββββββββOther compatibility improvements
And thatβs not all - there is also now support for NATURAL JOIN, OVERLAYΒ is drop-in compatible, compound INTERVAL literalsΒ are supported, and more!
LIKE uses text index
Contributed by Elmi Ahmadov
From ClickHouse 25.4, when a LIKE/ILIKEΒ query pattern is %<alpha-numeric-characters-without-spaces>%Β and the text index tokenizer is splitByNonAlpha, ClickHouse uses the inverted index to speed up those queries. It does this by scanning the inverted index dictionary rather than performing a full-table scan to find the matching pattern.
Letβs have a look at how this works with our trusty HackerNews dataset, first using clickhousectlΒ to get ClickHouse 26.4 running on my laptop:
chctl local install 26.4And then weβll start it up:
chctl local server start --version 26.4And connect using the ClickHouse client:
chctl local client --name default -mnNext, weβll create our HackerNews table:
CREATE TABLE hackernews
(
`id` Int64,
`deleted` Int64,
`type` String,
`by` String,
`time` DateTime64(9),
`text` String,
`dead` Int64,
`parent` Int64,
`poll` Int64,
`kids` Array(Int64),
`url` String,
`score` Int64,
`title` String,
`parts` Array(Int64),
`descendants` Int64
GRANULARITY 128
)
ORDER BY time;Weβll then insert the data:
INSERT INTO hackernews
SELECT *
FROM url('https://datasets-documentation.s3.eu-west-3.amazonaws.com/hackernews/hacknernews.csv.gz', 'CSVWithNames')And next, weβll add a text index on the textΒ column using the splitByNonAlphaΒ tokenizer:
ALTER TABLE hackernews
ADD INDEX text_tokens_idx text
TYPE text(tokenizer='splitByNonAlpha')
GRANULARITY 1;And materialize that index:
ALTER TABLE hackernews
(MATERIALIZE INDEX text_tokens_idx)
SETTINGS mutations_sync = 1;This optimization is already enabled in 26.4, but can be controlled using the use_text_index_like_evaluation_by_dictionary_scanΒ setting. The following query counts how many Hacker News posts mentioned Kubernetes:
SELECT count()
FROM hackernews
WHERE text LIKE '%Kubernetes%'
SETTINGS use_text_index_like_evaluation_by_dictionary_scan=0;ββcount()ββ
1. β 20070 β
βββββββββββ
1 row in set. Elapsed: 0.832 sec. Processed 18.25 million rows, 6.29 GB (21.93 million rows/s., 7.56 GB/s.)
Peak memory usage: 88.18 MiB.
1 row in set. Elapsed: 0.624 sec. Processed 18.25 million rows, 6.29 GB (29.23 million rows/s., 10.08 GB/s.)
Peak memory usage: 87.93 MiB.
1 row in set. Elapsed: 0.638 sec. Processed 18.25 million rows, 6.29 GB (28.60 million rows/s., 9.86 GB/s.)
Peak memory usage: 86.01 MiB.And now using the optimization:
SELECT count()
FROM hackernews
WHERE text LIKE '%Kubernetes%'
SETTINGS use_text_index_like_evaluation_by_dictionary_scan=1;ββcount()ββ
1. β 20070 β
βββββββββββ
1 row in set. Elapsed: 0.208 sec. Processed 18.25 million rows, 18.25 MB (87.53 million rows/s., 87.53 MB/s.)
Peak memory usage: 2.07 MiB.
1 row in set. Elapsed: 0.225 sec. Processed 18.25 million rows, 18.25 MB (80.98 million rows/s., 80.98 MB/s.)
Peak memory usage: 2.07 MiB.
1 row in set. Elapsed: 0.234 sec. Processed 18.25 million rows, 18.25 MB (77.83 million rows/s., 77.83 MB/s.)
Peak memory usage: 2.07 MiB.The number of rows processed is the same, but the query using the optimization has a best runtime of 208 milliseconds, compared to 624 milliseconds, a little over 3 times faster.
If we compare the query plans, we can see that the one using the optimization scans more than 1,000 fewer granules.
No use of inverted index:
ββexplainββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1. β Output: count() β
2. β β
3. β Aggregating β
4. β βββFilter ((WHERE + Change column names to column identifiers)) β
5. β βββReadFromMergeTree (default.hackernews) β
6. β Indexes: β
7. β PrimaryKey β
8. β Condition: true β
9. β Parts: 6/6 β
10. β Granules: 3533/3533 β
11. β Skip β
12. β Name: text_tokens_idx β
13. β Description: text GRANULARITY 100000000 β
14. β Condition: (mode: All; tokens: []) β
15. β Parts: 6/6 β
16. β Granules: 3533/3533 β
17. β Ranges: 6 β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββUses inverted index:
ββexplainβββββββββββββββββββββββββββββββββββββββββββββββ
1. β Output: count() β
2. β β
3. β Aggregating β
4. β βββFilter β
5. β βββReadFromMergeTree (default.hackernews) β
6. β Indexes: β
7. β PrimaryKey β
8. β Condition: true β
9. β Parts: 6/6 β
10. β Granules: 3533/3533 β
11. β Skip β
12. β Name: text_tokens_idx β
13. β Description: text GRANULARITY 100000000 β
14. β Condition: (mode: All; tokens: []) β
15. β Parts: 6/6 β
16. β Granules: 2247/3533 β
17. β Ranges: 190 β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββFaster COUNT DISTINCT
Contributed by Jiebin Sun
There are a couple of improvements to uniqExactΒ (used by COUNT(DISTINCT ...)) on high-core-count machines:
- ClickHouse no longer spawns redundant threads during the merge phase. uniqExact uses a two-level hash table with 256 buckets, but previously, ClickHouse would spawn up to
max_threadsΒ threadsΒ regardless, and many of them would have nothing to do and exit immediately. - When merging N intermediate hash tables (one per aggregation thread), the thread pool was initialized N times, causing
O(N Γ threads)Β total thread spawns and severe lock contention. Now, all N hash tables are merged in a single pass - each thread processes one bucket across all hash tables at once, reducing thread pool initializations fromO(N)Β toO(1).
In some of our benchmarks, we saw speedups of 3 to 15 times on a 288-core machine.
This, however, is very much an optimization for machines with many cores - I tried it out on the HackerNews dataset on my Mac M2 Max (which has 12 cores) and didnβt see any improvement!
Even prettier EXPLAIN
Contributed by Kirill Kopnev
EXPLAIN PLAN pretty=1Β now prints expressions in a human-readable form, shows top-level output columns and per-step output columns, and labels JOINs with estimated row counts and locality.
Letβs see how this works with the following query:
EXPLAIN pretty = 1
SELECT by, count()
FROM hackernews
WHERE (text LIKE '%OpenAI%') AND (text LIKE '%Google%')
GROUP BY ALL
ORDER BY count() DESC, by
LIMIT 10;26.3
ββexplainβββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1. β Expression (Project names) β
2. β βββLimit (preliminary LIMIT) β
3. β βββSorting (Sorting for ORDER BY) β
4. β βββExpression ((Before ORDER BY + Projection)) β
5. β βββAggregating β
6. β βββExpression (Before GROUP BY) β
7. β βββExpression ((WHERE + Change column names to column identifiers)) β
8. β βββReadFromMergeTree (default.hackernews) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ26.4
ββexplainββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1. β Output: by, count() β
2. β β
3. β Expression (Project names) β
4. β βββLimit (preliminary LIMIT) β
5. β βββSorting (Sorting for ORDER BY) β
6. β βββExpression ((Before ORDER BY + Projection)) β
7. β βββAggregating β
8. β βββExpression (Before GROUP BY) β
9. β βββExpression β
10. β βββReadFromMergeTree (default.hackernews) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββJSONAllValues + text index
Contributed by Anton Popov
ClickHouse 26.4 adds the JSONAllValues, which returns every leaf value of a JSON column as Array(String). We can create a text index on top of this, enabling more efficient filtering on JSON subcolumns.
Letβs have a look at how this works with help from the StatsBomb dataset. Letβs get a subset of the data on our machine by running the following:
git clone --filter=blob:none --sparse https://github.com/statsbomb/open-data.git
cd open-data
git sparse-checkout set data/eventsWeβll create the following table using clickhouse-local:
CREATE TABLE events (
match_id UInt32,
json JSON(id String, index UInt32),
INDEX vals JSONAllValues(json) TYPE text(tokenizer = 'ngrams') GRANULARITY 1
)
ENGINE = MergeTree
ORDER BY (match_id, json.index);And then insert the data:
INSERT INTO events
SELECT
toUInt32(replaceRegexpOne(_file, '\\.json$', '')) AS match_id,
json
FROM file('open-data/data/events/*.json', JSONAsObject);12188949 rows in set. Elapsed: 1275.404 sec. Processed 12.19 million rows, 10.48 GB (9.56 thousand rows/s., 8.22 MB/s.)
Peak memory usage: 1.87 GiB.Just for our understanding of how the index works, letβs have a look at what the JSONAllValuesΒ function returns:
SELECT JSONAllValues(json) FROM events LIMIT 1
FORMAT Vertical;JSONAllValues(json): ['[36.4,21.7]','1.013174','000000b5-8156-429d-9088-e62a6ac2ea0d','2529','[36.8,20]','60','2','4','From Throw In','10958','Chris Smalling','5','Left Center Back','123','39','Manchester United','[\'5fbbde9b-74ab-48e9-9873-ef956db384de\',\'fd43cc18-c37b-438a-8a40-a8bb50e59469\']','18','39','Manchester United','00:15:18.727','43','Carry']The dataset has just over 12 million records, which isnβt really enough to see the impact of the index, so weβll duplicate the data a bunch of times:
ALTER TABLE events
ATTACH PARTITION ID 'all'
FROM events;0 rows in set. Elapsed: 3.892 sec.
0 rows in set. Elapsed: 7.957 sec.
0 rows in set. Elapsed: 15.894 sec.
0 rows in set. Elapsed: 33.655 sec.
0 rows in set. Elapsed: 68.870 sec.And now weβve got a lot more records:
SELECT count()
FROM events;ββββcount()ββ
1. β 390046368 β -- 390.05 million
βββββββββββββThe following query returns the number of rows related to Lionel Messi:
SELECT count()
FROM events
WHERE json.player.name = 'Lionel AndrΓ©s Messi Cuccittini'
SETTINGS use_skip_indexes = 1;We can disable the text index by setting use_skip_indexes = 0. Running this query gives us the following result:
ββcount()ββ
1. β 4268960 β -- 4.27 million
βββββββββββWeβll run it three times without the index:
1 row in set. Elapsed: 1.505 sec. Processed 390.05 million rows, 13.87 GB (259.20 million rows/s., 9.22 GB/s.)
Peak memory usage: 48.23 MiB.
1 row in set. Elapsed: 1.666 sec. Processed 390.05 million rows, 13.87 GB (234.12 million rows/s., 8.32 GB/s.)
Peak memory usage: 48.23 MiB.
1 row in set. Elapsed: 1.668 sec. Processed 390.05 million rows, 13.87 GB (233.88 million rows/s., 8.32 GB/s.)
Peak memory usage: 48.23 MiB.And three times with the index:
1 row in set. Elapsed: 1.139 sec. Processed 80.64 million rows, 3.23 GB (70.80 million rows/s., 2.84 GB/s.)
Peak memory usage: 69.25 MiB.
1 row in set. Elapsed: 1.096 sec. Processed 80.64 million rows, 3.23 GB (73.61 million rows/s., 2.95 GB/s.)
Peak memory usage: 68.93 MiB.
1 row in set. Elapsed: 1.087 sec. Processed 80.64 million rows, 3.23 GB (74.21 million rows/s., 2.97 GB/s.)
Peak memory usage: 74.13 MiB.From the processed rows, we can see that the index reduces the amount of data to scan by almost 5 times. The best time without the index is 1,505 milliseconds, compared to 1,087 milliseconds with the index, an improvement of around 50%.
Get started today
Interested in seeing how ClickHouse works on your data? Get started with ClickHouse Cloud in minutes and receive $300 in free credits.
Sign up


