
本文字数:23451;估计阅读时间:59分钟
作者:ClickHouse

编者按: 本文译自 ClickHouse 原博客。 原文围绕「ClickHouse 26.8 以长期支持版本形式交付功能扩展与性能提升。」展开。本次更新在功能与性能两条线上均有实质推进,长期支持定位也降低了生产环境的升级顾虑。数据湖集成与 join 性能的改进值得关注。
又过了一个月,又到了发布新版本的时候!
ClickHouse 26.8 版本包含 98 项新功能 🍇、128 项性能优化 🌠、556 个 bug 修复 🐝,并且是长期支持版本。
本次发布带来后台查询、流水线 SQL、新的文本分词器、扩展的数据湖集成,以及 Parquet、GROUP BY 和 join 的性能改进。
新贡献者
特别欢迎 26.8 的所有新贡献者!ClickHouse 社区的增长令人惊叹,我们始终感谢那些让 ClickHouse 如此受欢迎的贡献。
以下为新贡献者名单:
Aashish Kohli, AbdullahKaya, Alex Budkar, Alex Kalmakov, Alexey Elkin, Amog Iska, Amogh-Bharadwaj, Andrey Tsarevskiy, Andy Bradshaw, Avinash Kamath, Bartok, Bartok9, Chris Lu, Christian Bianchi, ClickGap, Daniel Q. Kim, David Dallakyan, Dean Chen, Dergousov Maksim, Diego Gomes Tomé, Dmitrii Tunikov, Eduardo Gómez, Emil Sadek, George MacRorie, Hamid, Hank Cui, Haowen Feng (from Dev Box), Ilia Demianenko, Ivan Shelestov, James Sanders, Jose Muñoz, KD2YCU, Kirill Shcherbatov, Kirill Shokhin, Konstantin Plis, Kseniia, Nikolai Ovchinnikov, Nishant, Nishant Agarwal, Nuno Adrego, Perfloop Agent, Rahul Malik, RamiDarwiche, Raymond Lee, Ria Khatoniar, Ria-K912, Rohith Pariki, RohithPariki, Schum, Sean Reid, Sergey Chernov, Shawn Chen, Stanislav, Steve Lerner, Tod Trevillian, UberDever, Utkal Singh, Valery Petrov, VighneshPath, Vladimir Chemeris, William Hatcher, Yecine Megdiche, Yiyang Shao, ZachEddy, Zeynel Koca, a.akhondi, addshore, amirreza1307, avinash, deusgaudio, francisconeves-clickhouse, gelsonbagetti, gudauu, kalyanamdewri, locadex-agent[bot], pavol kutaj, quantrail-admin, vahid, vahid sohrabloo, yisamlee, yiyang-shao, zainulabidin302, Éco
提示:如果你好奇我们如何生成这份名单……看这里(https://gist.github.com/gingerwizard/5a9a87a39ba93b422d8640d811e269e9)。
查看媒体链接:https://www.youtube.com/watch?v=5A9gRYE0v2M
你还可以查看演示文稿的幻灯片(https://presentations.clickhouse.com/2026-release-26.8/)。
在后台运行查询
由 Miсhael Stetsyuk 贡献
现在可以在后台运行查询。查询会立即返回,然后持续运行直到完成,无论连接发生什么。
这个功能适合向 ClickHouse 写入数据或从中导出数据的长时间查询,即使客户端断开连接,这些查询也能继续运行。
我们有一台 ClickHouse 服务器正在运行,已导入 UK property prices dataset,然后用下面的查询复制了几次:
ALTER TABLE uk_price_paid
ATTACH PARTITION ID 'all'
FROM uk_price_paid;
这样行数就略超过 2.4 亿:
SELECT count() FROM uk_price_paid;
┌───count()─┐
│ 243619704 │
└───────────┘
接下来,我们可以运行下面的查询,把 uk_price_paid 表中的数据写入本地 HTTP 服务器上的一个文件。
INSERT INTO FUNCTION url('http://localhost:8080/uploads/london-sales.parquet', 'Parquet')
SELECT * FROM uk_price_paid
SETTINGS run_query_in_background = 1;
Query id: 59eabb9f-33f4-412b-b2f3-b090fe7b4bbf
Ok.
0 rows in set. Elapsed: 0.001 sec.
我们可以查询 system.processes,查看后台查询的进度:
SELECT query_id, query,
round(elapsed, 2) AS elapsed_seconds,
read_rows, written_rows
FROM system.processes
WHERE query NOT ILIKE '%system.processes%'
ORDER BY elapsed DESC;
Row 1:
──────
query_id: 59eabb9f-33f4-412b-b2f3-b090fe7b4bbf
query: INSERT INTO FUNCTION url('http://localhost:8080/uploads/uk.parquet', 'Parquet')
SELECT * FROM uk_price_paid
SETTINGS run_query_in_background = 1;
elapsed_seconds: 8.52
read_rows: 179243204 -- 179.24 million
written_rows: 179243204 -- 179.24 million
查询完成后,可以查询 system.query_log,看看它花了多长时间:
SELECT event_time, type, query_duration_ms,
read_rows, result_rows
FROM system.query_log
WHERE query_id = '59eabb9f-33f4-412b-b2f3-b090fe7b4bbf'
ORDER BY event_time;
┌──────────event_time─┬─type────────┬─query_duration_ms─┬─read_rows─┬─result_rows─┐
│ 2026-09-03 12:32:05 │ QueryStart │ 0 │ 0 │ 0 │
│ 2026-09-03 12:32:17 │ QueryFinish │ 11798 │ 243619704 │ 243619704 │
└─────────────────────┴─────────────┴───────────────────┴───────────┴─────────────┘
PostgreSQL 风格的正则表达式运算符
由 Alexey Milovidov 贡献
ClickHouse 现在支持以下 PostgreSQL 风格的正则表达式运算符:
• ~ 在字符串匹配正则表达式时返回 1,区分大小写。
• ~* 在字符串匹配正则表达式时返回 1,不区分大小写。
• !~ 在字符串不匹配正则表达式时返回 1,区分大小写。
• !~* 在字符串不匹配正则表达式时返回 1,不区分大小写。
下面的查询演示如何用所有这些运算符匹配 Baker Street 的各个部分:
SELECT
'Baker Street' ~ 'street$' AS matchesCaseSensitively,
'Baker Street' ~* 'street$' AS matchesCaseInsensitively,
'Baker Street' !~ 'street$' AS doesNotMatchCaseSensitively,
'Baker Street' !~* 'street$' AS doesNotMatchCaseInsensitively
FORMAT Vertical;
Row 1:
──────
matchesCaseSensitively: 0
matchesCaseInsensitively: 1
doesNotMatchCaseSensitively: 1
doesNotMatchCaseInsensitively: 0
一点免责声明: 这些运算符的实现者 Alexey 附带了一句提醒:他本人并不喜欢它们,觉得它们会让 SQL 看起来太像 Bash 或 Perl。这一轮,PostgreSQL 兼容性胜出。同一项改动还意味着,通过 PostgreSQL 线协议连接 ClickHouse 时,
\d、\dt、\dv等psql命令现在可以用了。不过,\d <table>仍依赖一些 ClickHouse 尚未支持的 PostgreSQL 语法。
数组作为数组下标
由 folly 贡献
下标运算符现在可以接受一组索引,并返回所有指定位置上的元素。这样一来,就能在单个表达式里收集、重排或抽样数组元素;与 arraySort 和 topK 类函数一起用时很方便。
来看一个简单的例子:
WITH arrayMap(x -> rand(x), range(1, 10)) AS random_values
SELECT random_values[[1, 3, 5]];
┌─arrayElement(ran⋯ues, [1, 3, 5])─┐
│ [344408953,1157293782,407846805] │
└──────────────────────────────────┘
更实际的用法是从月度时间序列中选取季度检查点。下面的查询先为每个区域的月度中位价格构建有序数组,然后用一个表达式选出 1 月、4 月、7 月和 10 月:
WITH monthlyPrices AS
(
SELECT district, toStartOfMonth(date) AS month,
round(median(price)) AS medianPrice
FROM uk_price_paid
WHERE town = 'LONDON'
AND district IN ('CAMDEN', 'CITY OF WESTMINSTER', 'KENSINGTON AND CHELSEA')
AND date >= '2024-01-01' AND date < '2025-01-01'
GROUP BY district, month
)
SELECT district,
arraySort(groupArray((month, medianPrice)))[[1, 4, 7, 10]] AS quarterlyPrices
FROM monthlyPrices
GROUP BY district
HAVING count() = 12
ORDER BY district;
Row 1:
──────
district: CAMDEN
quarterlyPrices: [('2024-01-01',760000),('2024-04-01',731000),('2024-07-01',762500),('2024-10-01',805000)]
Row 2:
──────
district: CITY OF WESTMINSTER
quarterlyPrices: [('2024-01-01',1215000),('2024-04-01',1037500),('2024-07-01',935000),('2024-10-01',875000)]
Row 3:
──────
district: KENSINGTON AND CHELSEA
quarterlyPrices: [('2024-01-01',1205000),('2024-04-01',1187500),('2024-07-01',1100000),('2024-10-01',1200000)]
查询转 JSON 再转查询
贡献者:Alexey Milovidov、Nikita Fomichev
现在,既可以把查询转换成 JSON 格式的抽象语法树,也可以用 JSON 格式的抽象语法树来查询 ClickHouse。
parseQueryToJSON 函数返回 JSON 格式的抽象语法树。我们用一个简单的 count() 来演示这个函数,因为输出非常冗长:
SELECT parseQueryToJSON($sql$
SELECT count() FROM uk_price_paid
$sql$)::JSON
FORMAT Vertical;
{
"type": "SelectWithUnionQuery",
"union_mode": "UNION_DEFAULT",
"list_of_selects": {
"type": "ExpressionList",
"children": [
{
"type": "SelectQuery",
"select": {
"type": "ExpressionList",
"children": [
{"type": "Function", "name": "count",
"arguments": {"type": "ExpressionList"}}
]
},
"tables": {
"type": "TablesInSelectQuery",
"children": [
{"type": "TablesInSelectQueryElement",
"table_expression": {
"type": "TableExpression",
"database_and_table_name": {
"type": "TableIdentifier", "name": "uk_price_paid"}}}
]
}
}
]
}
}
JSON 把查询描述为一棵带类型的节点树:标识符、字面量、函数和子句。
由于它就是普通的 JSON,任何有 JSON 库的语言都能分析、验证、重写或生成查询,无需嵌入 SQL 解析器。
转换是双向的:formatQueryFromJSON 能把树转回 SQL,所以你随时可以读回自己构建的内容:
SELECT formatQueryFromJSON(parseQueryToJSON($sql$
SELECT count() FROM uk_price_paid
$sql$));
┌─formatQueryFromJ⋯_price_paid\n'))─┐
│ SELECT count() FROM uk_price_paid │
└───────────────────────────────────┘
ClickHouse 26.8 还新增了一个实验性方言 clickhouse_json,允许你发送 JSON AST 而不是 SQL。它由 enable_json_ast_dialect 设置控制,然后像其他方言一样选择:
SET enable_json_ast_dialect = 1, dialect = 'clickhouse_json';
如果我们粘贴之前创建的 SELECT count() FROM uk_price_paid 的 AST,会得到:
┌───count()─┐
│ 243619704 │
└───────────┘
我们并不指望你开始用 JSON 语法树写查询!这个功能更多是给机器用的,而不是给人用的。
任何生成查询的东西都可以生成 JSON 语法树,而不是 SQL。这意味着不再需要拼接字符串,不再有可怕的转义 bug,也许最重要的是,没有 SQL 注入面。
CREATE USER... VALID FOR
贡献者:Alexey Milovidov
在 ClickHouse 26.8 之前,已经可以用 VALID UNTIL 语法创建有时间限制的用户:
CREATE USER mark
IDENTIFIED WITH no_password
VALID UNTIL '2026-10-04';
ClickHouse 26.8 新增了 VALID FOR INTERVAL 语法,它从当前时间开始计算截止时间。所以,要让用户从此刻起两周内有效,可以运行以下查询:
CREATE USER mark
IDENTIFIED WITH no_password
VALID FOR INTERVAL 2 WEEKS;
然后我们可以检查这个用户的有效期到什么时候:
SHOW CREATE USER mark
FORMAT LineAsString;
这个查询的输出会全部在一行里,所以我们手动格式化了一下,方便阅读:
CREATE USER mark
IDENTIFIED WITH no_password
VALID UNTIL '2026-09-18 10:06:29'
在修改用户时也可以使用这个语法。要让用户从此刻起一周内有效,可以运行以下查询:
ALTER USER mark VALID FOR INTERVAL 1 WEEK;
CREATE USER mark
IDENTIFIED WITH no_password
VALID UNTIL '2026-09-11 10:08:34'
流水线 SQL
贡献者:Alexey Milovidov
ClickHouse 26.8 引入了流水线 SQL,让你可以自上而下地写查询,每一步都接在前一步之后。
更多内容参见 Pipelined SQL in ClickHouse 26.8 博客文章(https://clickhouse.com/blog/pipelined-sql-26.8)。
将 ClickHouse 用作流式 HTTP API
由 Alexey Milovidov 贡献
ClickHouse 26.8 还引入了若干特性,让你能相当方便地为自己的表和常用查询套上一层轻量级 HTTP API。
更多内容参见 ClickHouse as a streaming HTTP API 博客文章(https://clickhouse.com/blog/clickhouse-streaming-http-api)。
system.user_query_log
由 Yue Ni、Alexey Milovidov 贡献
ClickHouse 26.8 还引入了一张新的系统表 system.user_query_log,其中只包含当前用户自己的查询。
也就是说,每个用户无需访问 system.query_log 表,就能查看自己的查询历史。
如果你的用户没有该表的访问权限,查询 system.query_log 仍会返回权限不足异常。
原子 POPULATE
由 Alexey Milovidov 贡献
增量物化视图相当于一个触发器:数据块插入表中时,就对它执行一次查询。
创建增量物化视图并在创建时填充数据时,曾存在一个竞态条件,会漏掉填充期间插入的记录。ClickHouse 26.8 已修复该问题。
调用 CREATE MATERIALIZED VIEW ... POPULATE 时,视图会订阅源表上的新插入,同时抓取现有数据的快照——这两步在源表短暂的排他锁下一起完成,因此在填充期间并发插入的每一行都能被投递且只投递一次。
该功能默认开启,把 materialized_views_populate_atomically 设为 0 即可关闭,这样就能重现之前的行为。
首先,创建一张包含 10 万行的源表:
CREATE TABLE src (id UInt64)
ORDER BY id;
INSERT INTO src
SELECT number FROM numbers(100000);
再创建一张目标表:
CREATE TABLE dst (id UInt64)
ORDER BY id;
接下来,我们让一个查询插入 50 行,同时让另一个查询创建物化视图来填充 dst 表。
要想让某一行掉进空档,它的 INSERT 必须在 POPULATE 取源表快照时仍处于执行中。多数插入结束得太快,来不及被卡住,所以我们用 sleepEachRow 把 50 行摊到大约两秒半的时间里,并在这条插入还在运行时启动 CREATE。
我们还把 POPULATE 和 TO 一起使用,这在 26.8 之前属于语法错误。该视图会用 src 中已有的数据回填现有的 dst 表。
./clickhouse client -q "
INSERT INTO src SELECT number + 100000 FROM numbers(50)
WHERE sleepEachRow(0.05) = 0;" &
sleep 0.5
./clickhouse client -m -q "
SET materialized_views_populate_atomically = 0;
CREATE MATERIALIZED VIEW mv TO dst
POPULATE AS SELECT id FROM src;"
wait
运行结束后,我们可以把源表和目标表对比一下:
SELECT (SELECT count() FROM src) AS sourceRows,
(SELECT count() FROM dst) AS dstRows,
(SELECT uniqExact(id) FROM dst) AS dstDistinct,
sourceRows - dstRows AS lost,
dstRows - dstDistinct AS duplicated
FORMAT PrettyCompact;
┌─sourceRows─┬─dstRows─┬─dstDistinct─┬─lost─┬─duplicated─┐
│ 100050 │ 100000 │ 100000 │ 50 │ 0 │
└────────────┴─────────┴─────────────┴──────┴────────────┘
50 条记录全在源表里,目标表里一条都没有。这条插入在 mv 存在之前就开始了,所以没有向它推送;而它提交时,填充所用的快照已经拍完,所以回填也没看到它。
恰好是 50 条,是因为插入只在查询开始时决定一次数据要送往哪些视图,而不是逐行或逐块判断。所以整批插入要么全都到达,要么全都不到达。
如果我们删掉 mv、src 和 dst,再重建 src 和 dst,就可以创建物化视图,只是这次把 materialized_views_populate_atomically 设为 1:
./clickhouse client -q "
INSERT INTO src SELECT number + 100000 FROM numbers(50)
WHERE sleepEachRow(0.05) = 0;" &
sleep 0.5
./clickhouse client -m -q "
SET materialized_views_populate_atomically = 1;
CREATE MATERIALIZED VIEW mv TO dst
POPULATE AS SELECT id FROM src;"
wait
这次统计每张表的记录数,会得到如下输出:
┌─sourceRows─┬─dstRows─┬─dstDistinct─┬─lost─┬─duplicated─┐
│ 100050 │ 100050 │ 100050 │ 0 │ 0 │
└────────────┴─────────┴─────────────┴──────┴────────────┘
日语和中文分词器支持
贡献者:Robert Schulze、Amos Bird、Jimmy Aguilar Mena
ClickHouse 的默认分词器是 splitByNonAlpha(tokens、hasAllTokens 和 hasAnyTokens 函数都用它),它按空白和标点字符把字符串切分成子串数组。与英语等印欧语言不同,中文和日文文本的词之间没有空格,因此需要专门的分词器。
26.8 用两个专门打造的分词器解决了这个问题:
• japanese :日语分词器基于 MeCab 形态分析器,需要在服务端配置中指定外部 MeCab 词典
• chinese :jieba 风格的分词器,把词典与 HMM(隐马尔可夫模型)结合起来切分未知序列,例如把 ClickHouse是一个快速的开源数据库 切成 ClickHouse、是、一个、快速、的、开源、数据库,而不是整块或单个字符。
Japanese
要使用日语分词器,首先需要配置一部词典。这里我们使用 UniDic。从网站下载 .zip 压缩包后,还需要算出它的 SHA256,ClickHouse 会在加载前用它校验词典:
sha256sum /var/lib/clickhouse/unidic-cwj-202512.zip
d94216b589d15d05c408ed59abc5259086703ebbac14e225b5314e4cd106c4db
在 /etc/clickhouse-server/config.d/tokenizers.xml 中新增一个 tokenizer,定义自定义配置;服务器启动时,这份配置会合并进主 config.xml 文件。
<clickhouse>
<tokenizer>
<japanese>
<dictionary_location>file:///var/lib/clickhouse/unidic-cwj-202512.zip</dictionary_location>
<dictionary_sha>d94216b589d15d05c408ed59abc5259086703ebbac14e225b5314e4cd106c4db</dictionary_sha>
</japanese>
</tokenizer>
</clickhouse>
注意
也可以通过 HTTP/HTTPS URL 或 S3 兼容存储指定词典
如果服务器已经在运行,记得重启,让配置生效。现在可以创建文本索引并这样查询:
CREATE TABLE reviews
(
id UInt64,
text String,
INDEX text_idx text TYPE text(tokenizer = 'japanese')
)
ENGINE = MergeTree
ORDER BY id;
INSERT INTO reviews VALUES
(1, '渋谷の新しい寿司屋で美味しいうにを食べた'), -- "ate" (食べた)
(2, '大阪のラーメンは最高だった、また食べたい'), -- "want to eat" (食べたい)
(3, '京都で抹茶アイスを食べながら散歩した'), -- "while eating" (食べながら)
(4, '新幹線に乗って富士山を見に行った'); -- no mention of eating
SELECT id, text
FROM reviews
WHERE hasAnyTokens(text, ['食べ'], 'japanese')
ORDER BY id;
| id | text |
|---|---|
| 1 | 渋谷の新しい寿司屋で美味しいうにを食べた |
| 2 | 大阪のラーメンは最高だった、また食べたい |
| 3 | 京都で抹茶アイスを食べながら散歩した |
返回 3 行。耗时:0.007 秒。
上例中,食べた、食べたい、食べながら 都是动词 食べる(“吃”)的不同形态,而分词器会把它们统统切分成词干 食べ 加一个独立的活用助词。因此只搜 食べ 这一个 token,就能正确命中第 1–3 行,跳过第 4 行。
更多细节请参阅日文分词器文档(https://clickhouse.com/docs/reference/engines/table-engines/mergetree-family/textindexes#japanese-tokenizer-dictionary)
中文
日文分词器需要下载外部分词典压缩包并在服务器 XML 配置里设置,中文分词器则不需要任何服务器配置。它内置的词典和隐马尔可夫模型数据(源自 cppjieba)直接编译进了 ClickHouse。
chinese 分词器允许指定粒度参数。默认值为 coarse_grained;如果传入 fine_grained,就能索引重叠的子词,提高搜索召回率,代价是索引更大。
下面的查询创建两张表。第一张表的索引使用 chinese 分词器,粒度参数为默认的 coarse_grained;第二张使用 fine_grained。
两张表都插入了四条字符串,其中 大学(“university”)要么单独出现(第 1 行),要么嵌在更长的复合词中(第 2、3 行),要么完全不出现(第 4 行)。
CREATE TABLE table_coarse
(
key UInt64,
str String,
INDEX text_idx str TYPE text(tokenizer = chinese) -- default: coarse_grained
)
ENGINE = MergeTree ORDER BY key;
CREATE TABLE table_fine
(
key UInt64,
str String,
INDEX text_idx str TYPE text(tokenizer = chinese('fine_grained'))
)
ENGINE = MergeTree ORDER BY key;
INSERT INTO table_coarse VALUES
(1, '他考上了大学,很开心'), -- standalone 大学
(2, '北京邮电大学的通信工程专业很强'), -- compound 北京邮电大学
(3, '我毕业于北京大学计算机系'), -- compound 北京大学
(4, '今天天气不错,适合散步'); -- no mention
INSERT INTO table_fine SELECT * FROM table_coarse;
下面的查询用 hasAllTokens 函数搜索 大学(“university”):先在 chinese 索引使用默认 coarse_grained 参数的表上查询,再在 fine_grained 参数的表上查询。
第一个表只返回了 他考上了大学,很开心 这一句,因为其中单独出现了 大学。第二个表既返回了单独出现 大学 的句子,也返回了包含复合词的句子:
SELECT
key,
str
FROM table_coarse
WHERE hasAllTokens(str, '大学')
ORDER BY key ASC;
Query ID: 7e348059-9cd1-4f72-8026-8d76266050e4
| key | str |
|---|---|
| 1 | 他考上了大学,很开心 |
1 row in set. Elapsed: 0.002 sec.
SELECT
key,
str
FROM table_fine
WHERE hasAllTokens(str, '大学')
ORDER BY key ASC;
Query ID: 4da23c89-8a10-4099-9637-bd2a14f6aa1b
| key | str |
|---|---|
| 1 | 他考上了大学,很开心 |
| 2 | 北京邮电大学的通信工程专业很强 |
| 3 | 我毕业于北京大学计算机系 |
3 rows in set. Elapsed: 0.002 sec.
这是因为 chinese 分词器在粗粒度模式下把 北京大学 和 北京邮电大学 当作词典中的单个词元,因此只搜索子词元「大学」时,粗粒度分词永远匹配不到这两行——尽管人读到句子时都知道这两句都与「大学」相关。fine_grained 模式则专门把这些复合词进一步拆成相互重叠的子词,如 北京、邮电 或 大学,所以只有它才能匹配到第 2 行和第 3 行。
新增分词器:icu 与 splitByRegexp
由 Jimmy Aguilar Mena 贡献
Jieba 面向简体和繁体中文,MeCab 主要用于日语,而 ICU(International Components for Unicode)是一个通用的、基于规则的多语言库,采用标准边界分析算法。那么对于泰语、老挝语、高棉语或缅甸语这类词与词之间不写空格的文字,又该如何分词呢?
26.8 版为 ClickHouse 新增了 icu(locale) 分词器,利用该库的 Unicode 分词功能把字符串切分成词元。对于词间不写空格的文字,ICU 采用基于词典的分词,确保文本被切成有意义的多字词,而不是单个字符。
在 26.8 之前,如果要用 asciiCJK 这类非 ASCII 分词器处理泰语、老挝语、高棉语或缅甸语文本,会遇到单字符碎片化的问题,因为 asciiCJK 把每个非 ASCII 字符都当作独立词元。以泰语词 บ้าน(“房屋”)为例:
SELECT tokens('บ้าน', 'asciiCJK');
┌─tokens('บ้าน', 'asciiCJK')─┐
│ ['บ','้','า','น'] │
└───────────────────────────┘
บ 和 น 是辅音字母,้ 是声调符号,า 是元音符号,所以分词把这个词切成了四个无意义的片段。hasAllTokens 只检查每个搜索片段是否出现在该行的某处,因此两个完全无关的真实泰语单词可以各自贡献一个片段,在搜索 "house" 时对完全无关的句子 "The horse is on the mountain" 产生误报:
-- "ม้าอยู่บนภูเขา" = "The horse is on the mountain" (no usage of "house")
SELECT tokens('ม้าอยู่บนภูเขา', 'asciiCJK');
┌─tokens('ม้าอยู่บนภูเขา', 'asciiCJK')──────────────────────┐
│ ['ม','้','า','อ','ย','ู','่','บ','น','ภ','ู','เ','ข','า'] │
└───────────────────────────────────────────────────────┘
SELECT hasAllTokens('ม้าอยู่บนภูเขา', 'บ้าน', 'asciiCJK');
┌─hasAllTokens⋯'asciiCJK')─┐
│ 1 │
└──────────────────────────┘
-- false positive
对比一下使用 icu 分词器并将 locale 设为 th 时会发生什么:
SELECT tokens('ม้าอยู่บนภูเขา', 'icu', 'th');
┌─tokens('ม้าอยู่⋯icu', 'th')─┐
│ ['ม้า','อยู่','บน','ภูเขา'] │
└──────────────────────────┘
-- "horse", "is/located", "on", "mountain"
SELECT hasAllTokens('ม้าอยู่บนภูเขา', 'บ้าน', 'icu(''th'')');
┌─hasAllTokens⋯u(\'th\')')─┐
│ 0 │
└──────────────────────────┘
提示
可以查询 system.collations 表,获取所有受支持的 ICU locale 列表:
对于需要更多控制的场景,26.8 引入了 splitByRegexp 分词器,可以用正则表达式作为分隔符把文本切分成 token。
ClickHouse 的默认分词器 splitByNonAlpha 会按每个非字母数字的 ASCII 字符切分,这意味着像 C++、C# 和 F# 这样的文本都会退化成单个字母:
SELECT
tokens('C++', 'splitByNonAlpha'),
tokens('C#', 'splitByNonAlpha'),
tokens('F#', 'splitByNonAlpha')
FORMAT Vertical;
tokens('C++'⋯yNonAlpha'): ['C']
tokens('C#',⋯yNonAlpha'): ['C']
tokens('F#',⋯yNonAlpha'): ['F']
实际中,这意味着搜索 "C#" 可能返回像 "I am an expert in C++" 这样的误报:
SELECT hasAllTokens('I am an expert in C++', 'C#', 'splitByNonAlpha');
┌─hasAllTokens⋯yNonAlpha')─┐
│ 1 │
└──────────────────────────┘
此外,hasToken 会拒绝包含分隔符的搜索词,所以用 splitByNonAlpha 根本无法搜索字面词项 C++ 或 C#。
使用 splitByRegexp,可以显式定义分隔符模式,让字符 # 和 + 被视为单词的一部分:
CREATE TABLE hold_my_beer
(
id UInt64,
description String,
INDEX idx description TYPE text(tokenizer = splitByRegexp('[^\p{L}\p{N}#+]+'))
)
ENGINE = MergeTree ORDER BY id;
INSERT INTO hold_my_beer VALUES
(1, 'I am an expert in C++'),
(2, 'I am an expert in C#'),
(3, 'I use Arch');
SELECT id, description FROM hold_my_beer WHERE hasAllTokens(description, 'C++') OR hasAllTokens(description, 'C#');
┌─id─┬─description───────────┐
│ 1 │ I am an expert in C++ │
│ 2 │ I am an expert in C# │
└────┴───────────────────────┘
URL 数据库引擎
由 Alexey Milovidov 贡献
在上个月的 26.7 发布博客文章 中,我们介绍了 url 表函数和 URL 表引擎现在如何根据指定的 URL schema 分派到正确的后端,除了 HTTP,还支持文件路径、S3、GCS、Azure 和 HDFS。
26.8 引入了新的 URL数据库引擎,可以指定 URL 前缀,并把远程服务器上的任意路径作为表来查询。
在上个月的文章中,我们的示例展示了如何用 url 表引擎直接查询 s3 存储桶:
SELECT count(), avg(star_rating) FROM url('s3://datasets-documentation/amazon_reviews/amazon_reviews_2015.snappy.parquet');
现在可以指定 s3://datasets-documentation/amazon_reviews 作为前缀,并把存储桶中的每个文件作为单独的表来查询:
CREATE DATABASE datasets
ENGINE = URL('https://datasets-documentation.s3.eu-west-3.amazonaws.com/amazon_reviews/');
USE datasets;
SELECT count() FROM 'amazon_reviews_2015.snappy.parquet';
┌──count()─┐
│ 41905631 │ -- 41.91 million
└──────────┘
SELECT count()
FROM 'amazon_reviews_2014.snappy.parquet';
┌──count()─┐
│ 44127569 │ -- 44.13 million
└──────────┘
在 clickhouse-local 中,默认数据库现在也叠加在 URL 之上,方便你查询本地文件、URL、S3 数据,或默认数据库中的普通表:
SELECT * FROM 'hits.tsv';
SELECT * FROM 'https://example.com/hits.tsv';
SELECT * FROM 's3://mybucket/hits.tsv';
SELECT * FROM table;
bigquery 表函数和 BigQuery 表引擎
贡献者:Alexey Milovidov
对于还没把工作负载从 BigQuery 迁移到 ClickHouse 的用户,26.8 引入了 bigquery 表函数和 BigQuery 表引擎,让迁移比以往更容易。
我们用一个现成的 Stack Overflow 帖子 BigQuery 项目来看看它如何工作(设置步骤)(https://pastila.nl/?003f30a0/ab5c593a768497695e7e0c9957cf118d#2FvhCYiX4UryqoULfQ/vfA==GCM)。
先使用 bigquery 表函数看看数据形态。为此需要向 bigquery 表函数传入服务密钥。
创建一个命名集合,将下面的 your_service_key 替换为从 Google 控制台获取的 .json 服务密钥内容:
<clickhouse>
<named_collections>
<bigquery_credentials>
<project>bigquery-clickhouse</project>
<dataset>stackoverflow</dataset>
<table>badges</table>
<service_account_key><![CDATA[your_service_key]]></service_account_key>
</bigquery_credentials>
</named_collections>
</clickhouse>
确认命名集合已存在:
SELECT *
FROM system.named_collections;
┌─name────────┬─collection───────────────────────────────────────────────────────────────────┬─source─┬─create_query─┐
│ my_bigquery │ {'dataset':'[HIDDEN]','project':'[HIDDEN]','service_account_key':'[HIDDEN]'} │ CONFIG │ │
└─────────────┴──────────────────────────────────────────────────────────────────────────────┴────────┴──────────────┘
现在把命名集合作为 bigquery 表函数的参数,查看数据形态:
DESCRIBE TABLE bigquery(bigquery_credentials);
Query id: dd1e8285-b286-4892-8770-eeed49ef6919
┌─name─────┬─type───────────────────────────┬─default_type─┬─default_expression─┬─comment─┬─codec_expression─┬─ttl_expression─┐
│ Id │ Nullable(Int64) │ │ │ │ │ │
│ UserId │ Nullable(Int64) │ │ │ │ │ │
│ Name │ Nullable(String) │ │ │ │ │ │
│ Date │ Nullable(DateTime64(6, 'UTC')) │ │ │ │ │ │
│ Class │ Nullable(Int64) │ │ │ │ │ │
│ TagBased │ Nullable(Int64) │ │ │ │ │ │
└──────────┴────────────────────────────────┴──────────────┴────────────────────┴─────────┴──────────────────┴────────────────┘
6 rows in set. Elapsed: 0.602 sec.
现在可以轻松在本地创建同样的表并插入数据:
CREATE TABLE bq_badges
(
Id Int64,
UserId Int64,
Name LowCardinality(String),
Date DateTime64(6, 'UTC'),
Class Int64,
TagBased Int64
)
ENGINE = MergeTree
ORDER BY (Id);
INSERT INTO bq_badges SELECT * FROM bigquery(bigquery_credentials) LIMIT 5; -- I don't want to max out my credit card
SELECT * FROM bq_badges;
┌───────Id─┬───UserId─┬─Name────────┬───────────────────────Date─┬─Class─┬─TagBased─┐
│ 3336768 │ 1033808 │ Copy Editor │ 2012-05-03 22:19:43.187000 │ 1 │ 0 │
│ 6687355 │ 19299 │ .net │ 2013-06-15 03:03:52.690000 │ 1 │ 1 │
│ 12885730 │ 3892259 │ Copy Editor │ 2015-01-31 14:43:59.373000 │ 1 │ 0 │
│ 39830489 │ 10659482 │ Copy Editor │ 2020-12-09 11:16:11.410000 │ 1 │ 0 │
│ 40413130 │ 1386551 │ Copy Editor │ 2021-01-29 18:00:54.783000 │ 1 │ 0 │
└──────────┴──────────┴─────────────┴────────────────────────────┴───────┴──────────┘
5 rows in set. Elapsed: 0.005 sec.
S3 Tables
贡献者:Konstantin Vedernikov
ClickHouse 26.8 为 Amazon S3 Tables 增加了写入支持,这是 AWS 面向 Iceberg 表的托管服务。除了查询现有表,现在还可以插入数据、创建新表。
首先,启用 Iceberg catalog 集成和写入:
SET allow_database_iceberg = 1;
SET allow_insert_into_iceberg = 1;
配置好可访问表存储桶的 AWS 凭证后,就可以使用 DataLakeCatalog 数据库引擎连接。把下面的区域和 warehouse ARN 替换成你自己的:
CREATE DATABASE tables
ENGINE = DataLakeCatalog(
'https://s3tables.us-east-1.amazonaws.com/iceberg'
)
SETTINGS
catalog_type = 's3tables',
region = 'us-east-1',
warehouse = 'arn:aws:s3tables:us-east-1:123456789012:bucket/analytics';
然后可以列出可用的表并查询其中一个。例如,如果你的存储桶在 ns 命名空间下有一个 events 表:
SHOW TABLES FROM tables;
SELECT *
FROM tables.`ns.events`
LIMIT 10;
现在也可以向该表写入数据,提供与其 schema 匹配的值:
-- Replace … with values matching your table's columns.
INSERT INTO tables.`ns.events` VALUES (…);
Snowflake Horizon
贡献者:Melvyn Peignon
ClickHouse 26.8 新增通过 Snowflake Horizon catalog 读写 Iceberg 表的支持。ClickHouse 直接从对象存储读取底层数据文件。
我们先启用 Iceberg catalog 集成和写入功能,再连接到该 catalog。请把下面的账户端点、数据库名、个人访问令牌和 Snowflake 角色换成你自己的。该角色需要有权访问你要使用的 Iceberg 表。
SET allow_database_iceberg = 1;
SET allow_insert_into_iceberg = 1;
CREATE DATABASE horizon
ENGINE = DataLakeCatalog(
'https://<org>-<account>.snowflakecomputing.com/polaris/api/catalog'
)
SETTINGS
catalog_type = 'horizon',
warehouse = 'ICEBERG_DB',
catalog_credential = '<PAT>',
auth_scope = 'session:role:<ROLE>',
vended_credentials = 1;
接下来可以列出可用的表,并查询其中一个。例如,如果你的 catalog 在 schema 命名空间下有一个 trades 表:
SHOW TABLES FROM horizon;
SELECT *
FROM horizon.`schema.trades`
LIMIT 10;
要插入数据,请提供与该表 schema 匹配的值:
-- Replace … with values matching your table's columns.
INSERT INTO horizon.`schema.trades` VALUES (…);
ClickHouse 通过 Horizon catalog 提交这些更改。
Puffin 文件格式
由 Konstantin Vedernikov 贡献
ClickHouse 26.8 新增对 Puffin 文件格式的支持,Apache Iceberg 用它来存储统计信息和删除向量。
下面用 ClickHouse 测试套件中的一个小样本文件看看它是怎么工作的:
SELECT referenced_data_file, deleted_rows
FROM url(
'https://raw.githubusercontent.com/ClickHouse/ClickHouse/693dee22dda0d7ff23343c326754ccfc24f3237f/tests/queries/0_stateless/data_puffin/file_properties_ok.puffin',
'Puffin'
);
┌─referenced_data_file───────────┬─deleted_rows─┐
│ /data/table/part-00000.parquet │ [2,5] │
└───────────────────────────────┴──────────────┘
这说明所引用的 Parquet 文件中第 2 行和第 5 行被标记为已删除。我们无需访问那个 Parquet 文件——删除信息就存放在 Puffin 文件自身里。这有助于解释为什么数据文件中存在的行,在查询 Iceberg 表时却不出现。
也可以用 PuffinMetadata 格式查看该 blob 的类型和属性:
SELECT blob_type, properties
FROM url(
'https://raw.githubusercontent.com/ClickHouse/ClickHouse/693dee22dda0d7ff23343c326754ccfc24f3237f/tests/queries/0_stateless/data_puffin/file_properties_ok.puffin',
'PuffinMetadata'
);
对这个文件来说,blob 类型是 deletion-vector-v1,其属性中的 cardinality 为 2,与上面两个被删除的行位置一致。
Iceberg 中预取 manifest 文件
由 Asya Shneerson 和 Konstantin Vedernikov 贡献
从 Iceberg 表读取数据之前,ClickHouse 要先读取描述其数据文件和删除文件的 manifest 文件。抓取并处理这些元数据会明显增加启动耗时,尤其是在需要向对象存储发起大量请求时。
在 26.8 中,ClickHouse 会在解析当前 manifest 文件的同时预取下一个,让存储读取与 CPU 计算重叠进行。
删除 manifest 也是并发读取和解码的。由于这些文件必须在读取数据文件之前处理完,这一改动能加快含大量删除文件的表的查询启动速度。iceberg_delete_manifest_decode_concurrency 设置控制同时解码多少个删除 manifest,默认值为 4。
本次发布还修复了数据湖 catalog 的 S3 bucket 区域缓存,避免反复发请求来确定 bucket 所在区域。
原生 Parquet 改进
由 Alexey Milovidov 和 Vasily Chekalkin 贡献
ClickHouse 26.8 对 Parquet 读取做了多项改进,帮助查询跳过不必要的数据。
对于带有 ORDER BY 和 LIMIT 的查询,ClickHouse 可以先读取排序和过滤所需的列,然后只为通过 LIMIT 筛选后保留下来的行获取其余列。这项优化默认开启。
下面从公开 Parquet 数据集中找出最新的十个事件,用 WatchID 打破并列:
SELECT URL, Title
FROM s3(
'https://clickhouse-public-datasets.s3.amazonaws.com/hits_compatible/hits.parquet',
NOSIGN
)
ORDER BY EventTime DESC, WatchID DESC
LIMIT 10
SETTINGS query_plan_optimize_lazy_materialization_for_object_storage = 1;
我们可以把它与关闭惰性读取的情况对比:将 query_plan_optimize_lazy_materialization_for_object_storage 设为 0。在 ClickHouse 26.8.2.7 上反复测试时,关闭该优化后查询从 S3 读取约 6.6–6.7 GB,开启后读取 1.5 GB,返回的是同样的十行。
基于字典的过滤也能帮助 ClickHouse 在等值和 IN 条件下跳过数据。下面查找匹配某一款手机型号的行:
SELECT count()
FROM s3(
'https://clickhouse-public-datasets.s3.amazonaws.com/hits_compatible/hits.parquet',
NOSIGN
)
WHERE MobilePhoneModel = 'GT-C3262';
┌─count()─┐
│ 200 │
└─────────┘
当某个列块完全采用字典编码时,如果请求的值不在字典中,ClickHouse 就可以跳过该行组。即使 min/max 统计信息无法排除匹配,并且布隆过滤器不可用,这项优化仍然有用。
开启字典过滤后,该查询处理了 12 个数据页;关闭时则处理 226 个。两次查询都返回 200。input_format_parquet_dictionary_filter_push_down 设置默认为 1048576(即字典页上限为 1 MiB);将其设为 0 会关闭这项优化。
GeoParquet 查询也能受益于空间剪枝。ClickHouse 利用边界框信息跳过无关的行组和页,并在读取行时应用空间谓词。
GROUP BY 性能改进
由 Nihal Z. Miaji、Dmitriy Terenichev、Konstantin Bogdanov、Harikrishnan Prabakaran 贡献
26.8 版本还对 GROUP BY 做了一系列性能改进,包括:
• 一种新的并行 GROUP BY 算法,自适应地结合了合并聚合器和拆分聚合器所用的方法。
• 26.7 版本为按键排序的表将 GROUP BY 与 ORDER BY LIMIT 融合。26.8 对任意读取顺序都支持这样做,并剪掉不可能出现在结果中的分组。
• 以单个字符串为键的 GROUP BY 现在使用小得多的哈希表单元。
• 并行 GROUP BY 查询的最后合并步骤现在使用多线程,避免成为单线程瓶颈。
我们将在另一篇文章中更详细地介绍这些优化。
联接
由 Han Fei, Anton Popov, Robert Schulze, Alexey Milovidov, Vladimir Cherkasov, Alexander Gololobov 贡献
和每次发布一样,这次我们在 join 方面也有不少改进:
• 对于小表,列统计信息现在默认在 INSERT 时构建,全部 TPC-H 基准测试的成绩因此提升了 29%。
• 连接条件为两个不等式的 JOIN 过去会按带过滤的 CROSS JOIN 执行,现在改用基于排序的 IEJoin 算法。
• 新增 merge join 算法 parallel_full_sorting_merge ,可以利用所有核心。输入按 key 的哈希分片,各分片独立执行 merge join。
• 面向分布式查询计划的基于代价的优化器。该优化器借助基数估计,决定分布式 join、聚合、排序和数据搬移该如何执行。
这些功能的更多细节,我们会另写一篇文章介绍。
关于我们
ClickHouse(clickhouse.com) 是面向 AI 时代打造的高性能实时分析数据库,能够以极致性能处理海量数据分析任务。凭借高并发、低延迟和云原生架构,ClickHouse 广泛应用于可观测性、数据仓库、实时分析及 AI 数据基础设施等场景。我们致力于帮助企业在公有云平台上构建安全、弹性且高性价比的实时分析与 AI 数据平台,加速释放数据价值,推动智能化创新与数字化转型。目前,Trip.com、DiDi、Meta、Sony、Netflix、Deutsche Bank、Sierra、Cloudflare 等全球领先企业均在使用 ClickHouse 支撑其关键业务和数据分析平台。
262



被折叠的 条评论
为什么被折叠?



