ClickHouse 26.4 版本发布说明:SQL兼容性提升,COUNT DISTINCT更快了!
本文字数:10732;估计阅读时间:27分钟
作者:ClickHouse Team
Meetup活动
ClickHouse 上海第3届 Meetup 报名倒计时3天,详见文末海报!
又是新一月,这意味着又到了发布新版本的时候了!
ClickHouse 26.4 版本包含 39 项新功能 🌷、45 项性能优化 🐇 和 238 个 bug 修复 🐝。
本次发布带来了更多与 SQL 兼容的功能、更快的 COUNT DISTINCT、更美观的 EXPLAIN 等改进!
热烈欢迎所有参与 26.4 版本贡献的新成员!ClickHouse 社区的蓬勃发展令人备受鼓舞,我们由衷感谢所有贡献者,正是他们的付出让 ClickHouse 广受欢迎。
以下是新贡献者的姓名:
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
小提示:如果您好奇我们如何生成此列表,详情请点击此处(https://gist.github.com/gingerwizard/5a9a87a39ba93b422d8640d811e269e9)。
查看媒体链接:https://youtu.be/9lSVy7k2EoI
您也可以查看演示文稿的幻灯片(https://presentations.clickhouse.com/2026-release-26.4/)。
26.4 版本引入了更多与标准 SQL 语法兼容的功能。我们将仅介绍其中几个,您可以查阅演示文稿幻灯片以了解更多详情(https://presentations.clickhouse.com/2026-release-26.4/?full#16)。
查看媒体链接:https://youtu.be/VE2DHww-Z_k
贡献者:Desel72
首先我们来看 VALUES。在此版本之前,其调用方式如下:
SELECT *
FROM VALUES((1, 'a'), (2, 'b'), (3, 'c'));
┌─c1─┬─c2─┐1. │ 1 │ a │2. │ 2 │ b │3. │ 3 │ c │└────┴────┘
而现在,我们也可以将其作为表表达式调用,示例如下:
SELECT *
FROM (VALUES (1, 'a'), (2, 'b'), (3, 'c'));
┌─c1─┬─c2─┐1. │ 1 │ a │2. │ 2 │ b │3. │ 3 │ c │└────┴────┘
我们现在还可以为列设置别名,这在使用 VALUES 进行 join 查询时非常有用。例如,我们可以避免使用以下写法:
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 │└───────┴──────┘
通过为列命名,我们可以让查询更易于理解:
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 │└───────┴────────┘
贡献者:Alexey Milovidov
EXTRACT 运算符(在处理日期时使用)现在支持 PostgreSQL 风格的单位,如下面查询所示:
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: 3
由 phulv94 贡献
还有一个新的 SQL 标准别名用于设置时区。首先,让我们检查我当前的区域:
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 │└───────────────┴────────────────────────────┘
现在,我们将其设置为 Amsterdam:
SET TIME ZONE 'Europe/Amsterdam';
如果我们重新运行上面的查询:
┌─timezone()───────┬─formatDateTim⋯H:%M:%S %z')─┐1. │ Europe/Amsterdam │ 2026-05-05 16:May:59 +0200 │└──────────────────┴────────────────────────────┘
其他兼容性改进
不仅如此,现在还支持 NATURAL JOIN,OVERLAY 可直接兼容,复合 INTERVAL 字面量 也受支持,等等!
由 Elmi Ahmadov 贡献
从 ClickHouse 25.4 开始,当 LIKE / ILIKE 查询模式为 不含空格的字母数字字符 且文本索引分词器为splitByNonAlpha 时,ClickHouse 会使用倒排索引 来加速这些查询。它通过扫描倒排索引字典而非执行全表扫描来查找匹配模式。
让我们看看这在我们的信赖的 HackerNews 数据集上如何工作,首先使用 clickhousectl 让 ClickHouse 26.4 在我的笔记本电脑上运行起来:
chctl local install 26.4
然后我们启动它:
chctl local server start --version 26.4
并使用 ClickHouse 客户端连接:
chctl local client --name default -mn
接下来,我们将创建我们的 HackerNews 表:
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;
然后我们将插入数据:
INSERT INTO hackernews
SELECT *
FROM url('https://datasets-documentation.s3.eu-west-3.amazonaws.com/hackernews/hacknernews.csv.gz', 'CSVWithNames')
接下来,我们将在 text 列上使用 splitByNonAlpha 分词器添加一个文本索引:
ALTER TABLE hackernews
ADD INDEX text_tokens_idx text
TYPE text(tokenizer='splitByNonAlpha')
GRANULARITY 1;
并物化该索引:
ALTER TABLE hackernews
(MATERIALIZE INDEX text_tokens_idx)
SETTINGS mutations_sync = 1;
此优化已在 26.4 中启用,但可以使用 use_text_index_like_evaluation_by_dictionary_scan 设置进行控制。以下查询统计了有多少 Hacker News 帖子提到了 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.
现在使用优化后:
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.
处理的行数相同,但使用此优化的查询的最佳运行时间为 208 毫秒,相比 624 毫秒,速度提升了三倍多。
如果我们比较查询计划,可以看到使用优化的查询扫描的颗粒数减少了 1000 多个。
未使⽤倒排索引:
┌─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 │
└─────────────────────────────────────────────────────────────────┘
使⽤倒排索引:
┌─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 │
└──────────────────────────────────────────────────────┘
Jiebin Sun 贡献
在高核数机器上,uniqExact (由 COUNT(DISTINCT ...) 使用) 有以下几项改进:
• ClickHouse 在合并阶段不再创建冗余线程。 uniqExact 使用带有 256 个桶的两级哈希表,但在此之前,ClickHouse 无论如何都会创建多达 max_threads 个线程,其中许多线程会处于空闲状态并立即退出。
• 合并 N 个中间哈希表(每个聚合线程对应一个)时,线程池会初始化 N 次,这导致总共 O(N × threads) 次线程创建和严重的锁竞争。现在,所有 N 个哈希表都在单次遍历中完成合并——每个线程同时处理所有哈希表中的一个桶,从而将线程池初始化次数从 O(N) 减少到 O(1) 。
在我们的一些基准测试中,一台 288 核机器上的性能提升达到 3 到 15 倍。
然而,这主要是一项针对多核机器的优化——我在我的 Mac M2 Max (拥有 12 个核心) 上使用 HackerNews 数据集进行了尝试,但并未观察到任何改进!
Kirill Kopnev 贡献
EXPLAIN PLAN pretty=1 现在能以人类可读的形式打印表达式,显示顶层输出列和每步输出列,并为 JOIN 操作标注预估的行数和局部性。
让我们通过以下查询,来看看它是如何工作的:
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) │
└─────────────────────────────────────────────────────────────┘
Anton Popov 贡献
ClickHouse 26.4 Values 函数,它会将 JSON 列中的每个叶子值作为 Array(String) 类型返回。基于此,我们可以创建一个文本索引,从而实现对 JSON 子列更高效的过滤。
让我们借助 StatsBomb 数据集 来看看它是如何工作的。通过运行以下命令,我们将在本地机器上获取一个数据子集:
git clone --filter=blob:none --sparse https://github.com/statsbomb/open-data.git
cd open-data
git sparse-checkout set data/events
我们将使用 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);
然后插入数据:
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.
为了更好地理解这个索引是如何工作的,我们来看一下 JSONAllValues 函数会返回什么:
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']
这个数据集只有 1200 多万条记录,还不太足以体现索引带来的影响,所以我们先把数据重复复制几轮:
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.
现在,记录数就多多了:
SELECT count()
FROM events;
┌───count()─┐
1. │ 390046368 │ -- 390.05 million
└───────────┘
以下查询返回与 Lionel Messi 相关的行数:
SELECT count()
FROM events
WHERE json.player.name = 'Lionel Andrés Messi Cuccittini'
SETTINGS use_skip_indexes = 1;
我们可以通过设置 use_skip_indexes = 0 来禁用文本索引。运行此查询将得到以下结果:
┌─count()─┐
1. │ 4268960 │ -- 4.27 million
└─────────┘
我们将在不使用索引的情况下运行该查询三次:
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.
并在使用索引的情况下运行该查询三次:
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.
从处理的行数来看,索引将需要扫描的数据量减少了近5倍。在不使用索引的情况下,最佳时间为1,505毫秒;而使用索引时,最佳时间为1,087毫秒,查询性能提升了约50%。
好消息:ClickHouse Shanghai User Group第 3 届 Meetup 报名倒计时,将于2026年5月16日在上海市浦东新区世纪大道1568号中建大厦33层 Optiver上海举行,扫码免费报名
(温馨提示:因场地登记需要,请实名报名。)
/END/
试用阿里云 ClickHouse企业版
轻松节省30%云资源成本?阿里云数据库ClickHouse 云原生架构全新升级,首次购买ClickHouse企业版计算和存储资源组合,首月消费不超过99.58元(包含最大16CCU+450G OSS用量)了解详情:https://t.aliyun.com/Kz5Z0q9G
征稿启示
面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:[email protected]