返回
RSS ClickHouse Blog AI 逐段翻译 发布 2026-09-10 08:00 收录于 09-13

ClickHouse 26.8 LTS 发布:后台查询与数据湖集成

DataHot 速览

ClickHouse 发布 26.8 LTS,包含 98 项新功能、128 项性能优化和 556 个 bug 修复。该版本引入后台查询、管道化 SQL、新文本分词器,并扩展数据湖集成。同时提升 Parquet、GROUP BY 和 join 查询性能。后台查询适合长时间运行的查询和数据摄入等场景。

为什么值得关注:ClickHouse 是实时分析与湖仓查询常用引擎,此次 LTS 更新涉及后台查询、数据湖集成和核心查询性能,数据平台与实时分析从业者应关注其升级价值。

本文目录 43 节
  1. 新贡献者
  2. 在后台运行查询
  3. 由 Miсhael Stetsyuk 贡献
  4. PostgreSQL 风格的正则表达式运算符
  5. 由 Alexey Milovidov 贡献
  6. 数组作为数组下标
  7. 由 folly 贡献
  8. 查询到 JSON 再到查询
  9. 由 Alexey Milovidov、Nikita Fomichev 贡献
  10. CREATE USER ... VALID FOR
  11. 由 Alexey Milovidov 贡献
  12. 流水线 SQL
  13. 由 Alexey Milovidov 贡献
  14. ClickHouse 作为流式 HTTP API
  15. 由 Alexey Milovidov 贡献
  16. system.user_query_log
  17. 由 Yue Ni、Alexey Milovidov 贡献
  18. 原子 POPULATE
  19. 由 Alexey Milovidov 贡献
  20. 日语和中文分词器支持
  21. 由 Robert Schulze、Amos Bird、Jimmy Aguilar Mena 贡献
  22. 日语
  23. 中文
  24. 新分词器:icu 和 splitByRegexp
  25. 由 Jimmy Aguilar Mena 贡献
  26. URL 数据库引擎
  27. 由 Alexey Milovidov 贡献
  28. bigquery 表函数和 BigQuery 表引擎
  29. 由 Alexey Milovidov 贡献
  30. S3 Tables
  31. 由 Konstantin Vedernikov 贡献
  32. Snowflake Horizon
  33. 由 Melvyn Peignon 贡献
  34. Puffin 文件格式
  35. 由 Konstantin Vedernikov 贡献
  36. 在 Iceberg 中预取清单文件
  37. 由 Asya Shneerson 和 Konstantin Vedernikov 贡献
  38. 原生 Parquet 改进
  39. 由 Alexey Milovidov 和 Vasily Chekalkin 贡献
  40. GROUP BY 性能改进
  41. 由 Nihal Z. Miaji、Dmitriy Terenichev、Konstantin Bogdanov、Harikrishnan Prabakaran 贡献
  42. 连接
  43. 由 Han Fei、Anton Popov、Robert Schulze、Alexey Milovidov、Vladimir Cherkasov、Alexander Gololobov 贡献

译文

AI 逐段翻译

又过了一个月,这意味着又到了发布新版本的时候了!

ClickHouse 26.8 版本包含 98 项新功能 🍇 128 项性能优化 🌠 556 项错误修复 🐝,并且是一个长期支持版本。

此版本带来了后台查询、流水线 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

提示:如果你好奇我们是如何生成这份名单的…… 这里

你也可以 查看演示文稿中的幻灯片

在后台运行查询

由 Miсhael Stetsyuk 贡献

现在可以在后台运行查询。查询将立即返回,然后一直运行到完成,无论你的连接发生什么情况。

此功能对于将数据摄取到 ClickHouse 或从中导出数据的长时间运行查询非常有用,允许它们在客户端断开连接时继续运行。

我们运行了一个 ClickHouse 服务器,其中 英国房产价格数据集 已被导入,然后使用以下查询复制了几次:

1ALTER TABLE uk_price_paid2ATTACH PARTITION ID 'all'3FROM uk_price_paid;

这样我们就得到了略多于 2.4 亿行:

1SELECTcount() FROM uk_price_paid;
1┌───count()─┐2│ 243619704 │3└───────────┘

接下来,我们可以运行以下查询,将 uk_price_paid 表中的数据写入本地 HTTP 服务器上的文件。

1INSERT INTOFUNCTION url('http://localhost:8080/uploads/london-sales.parquet', 'Parquet')2SELECT*FROM uk_price_paid3SETTINGS run_query_in_background =1;
1Query id: 59eabb9f-33f4-412b-b2f3-b090fe7b4bbf23Ok.450 rows inset. Elapsed: 0.001 sec.

我们可以查询 system.processes 来检查后台查询的进度:

1SELECT query_id, query,2      round(elapsed, 2) AS elapsed_seconds,3      read_rows, written_rows4FROM system.processes5WHERE query NOT ILIKE '%system.processes%'6ORDERBY elapsed DESC;
1Row 1:2──────3query_id:        59eabb9f-33f4-412b-b2f3-b090fe7b4bbf4query:           INSERT INTO FUNCTION url('http://localhost:8080/uploads/uk.parquet', 'Parquet')5SELECT * FROM uk_price_paid6SETTINGS run_query_in_background = 1;7elapsed_seconds: 8.528read_rows:       179243204 -- 179.24 million9written_rows:    179243204 -- 179.24 million

查询完成后,我们可以查询 system.query_log 来查看它花了多长时间:

1SELECT event_time, type, query_duration_ms,2      read_rows, result_rows3FROM system.query_log4WHERE query_id ='59eabb9f-33f4-412b-b2f3-b090fe7b4bbf'5ORDERBY event_time;
1┌──────────event_time─┬─type────────┬─query_duration_ms─┬─read_rows─┬─result_rows─┐2│ 2026-09-03 12:32:05 │ QueryStart  │                 0 │         0 │           0 │3│ 2026-09-03 12:32:17 │ QueryFinish │             11798 │ 243619704 │   243619704 │4└─────────────────────┴─────────────┴───────────────────┴───────────┴─────────────┘

PostgreSQL 风格的正则表达式运算符

由 Alexey Milovidov 贡献

ClickHouse 现在支持以下 PostgreSQL 风格的正则运算符:

  • ~ 在字符串匹配正则表达式时返回 1,使用区分大小写匹配。
  • ~* 在字符串匹配正则表达式时返回 1,使用不区分大小写匹配。
  • !~ 在字符串不匹配正则表达式时返回 1,使用区分大小写匹配。
  • !~* 在字符串不匹配正则表达式时返回 1,使用不区分大小写匹配。

以下查询展示了如何使用所有这些运算符来匹配术语 Baker Street 的各个部分:

1SELECT2'Baker Street'~'street$'AS matchesCaseSensitively,3'Baker Street'~*'street$'AS matchesCaseInsensitively,4'Baker Street'!~'street$'AS doesNotMatchCaseSensitively,5'Baker Street'!~*'street$'AS doesNotMatchCaseInsensitively6FORMAT Vertical;
1Row 1:2──────3matchesCaseSensitively:        04matchesCaseInsensitively:      15doesNotMatchCaseSensitively:   16doesNotMatchCaseInsensitively: 0
一个小免责声明: 这些运算符附带了实现它们的 Alexey 的免责声明:他并不喜欢它们,认为它们让 SQL 看起来太像 Bash 或 Perl 了。PostgreSQL 兼容性在这一轮中胜出。同样的变更意味着 psql 命令,例如 \d\dt\dv 现在可以通过 PostgreSQL 线路协议连接到 ClickHouse 时正常工作。然而,\d <table> 仍然使用一些 ClickHouse 尚未支持的 PostgreSQL 语法。

数组作为数组下标

由 folly 贡献

下标运算符现在接受一个索引数组,并返回所有给定位置上的元素。这使得可以在单个表达式中收集、重新排序或采样数组元素,在与 arraySort 和 topK 风格函数一起使用时非常方便。

让我们看一个简单的例子:

1WITH arrayMap(x -> rand(x), range(1, 10)) AS random_values2SELECT random_values[[1, 3, 5]];
1┌─arrayElement(ran⋯ues, [1, 3, 5])─┐2│ [344408953,1157293782,407846805] │3└──────────────────────────────────┘

一个更现实的用法是从月度时间序列中选择季度检查点。以下查询构建每个地区月度中位价格的有序数组,然后在一个表达式中选择一月、四月、七月和十月:

1WITH monthlyPrices AS2(3SELECT district, toStartOfMonth(date) ASmonth,4           round(median(price)) AS medianPrice5FROM uk_price_paid6WHERE town ='LONDON'7AND district IN ('CAMDEN', 'CITY OF WESTMINSTER', 'KENSINGTON AND CHELSEA')8ANDdate>='2024-01-01'ANDdate<'2025-01-01'9GROUPBY district, month10)11SELECT district,12       arraySort(groupArray((month, medianPrice)))[[1, 4, 7, 10]] AS quarterlyPrices13FROM monthlyPrices14GROUPBY district15HAVINGcount() =1216ORDERBY district;
1Row 1:2──────3district:        CAMDEN4quarterlyPrices: [('2024-01-01',760000),('2024-04-01',731000),('2024-07-01',762500),('2024-10-01',805000)]56Row 2:7──────8district:        CITY OF WESTMINSTER9quarterlyPrices: [('2024-01-01',1215000),('2024-04-01',1037500),('2024-07-01',935000),('2024-10-01',875000)]1011Row 3:12──────13district:        KENSINGTON AND CHELSEA14quarterlyPrices: [('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() 来演示这个函数,因为输出极其冗长:

1SELECT parseQueryToJSON($sql$2SELECTcount() FROM uk_price_paid3$sql$)::JSON4FORMAT Vertical;
1{2"type":"SelectWithUnionQuery",3"union_mode":"UNION_DEFAULT",4"list_of_selects":{5"type":"ExpressionList",6"children":[7{8"type":"SelectQuery",9"select":{10"type":"ExpressionList",11"children":[12{"type":"Function","name":"count",13"arguments":{"type":"ExpressionList"}}14]15},16"tables":{17"type":"TablesInSelectQuery",18"children":[19{"type":"TablesInSelectQueryElement",20"table_expression":{21"type":"TableExpression",22"database_and_table_name":{23"type":"TableIdentifier","name":"uk_price_paid"}}}24]25}26}27]28}29}

该 JSON 将查询描述为一棵由类型化节点组成的树:标识符、字面量、函数和子句。

因为它只是普通的 JSON,任何带有 JSON 库的语言都可以分析、验证、重写或生成查询,而无需嵌入 SQL 解析器。

该转换是双向的:formatQueryFromJSON 将树转换回 SQL,因此你始终可以读回你所构建的内容:

1SELECT formatQueryFromJSON(parseQueryToJSON($sql$2SELECTcount() FROM uk_price_paid3$sql$));
1┌─formatQueryFromJ⋯_price_paid\n'))─┐2│ SELECT count() FROM uk_price_paid │3└───────────────────────────────────┘

ClickHouse 26.8 还添加了一个实验性方言 clickhouse_json,它允许你发送 JSON AST 而不是 SQL。它受 enable_json_ast_dialect 设置控制,然后像任何其他方言一样选择:

1SET enable_json_ast_dialect =1, dialect ='clickhouse_json';

如果我们粘贴之前创建的 SELECT count() FROM uk_price_paid 的 AST,我们会得到:

1┌───count()─┐2│ 243619704 │3└───────────┘

我们并不期望你开始将查询编写为 JSON 语法树!此功能更多是面向机器而非人类。

任何生成查询的东西都可以生成 JSON 语法树而不是 SQL,这意味着不需要字符串拼接,不再有可怕的转义错误,也许最重要的是,没有 SQL 注入面。

CREATE USER ... VALID FOR

由 Alexey Milovidov 贡献

在 ClickHouse 26.8 之前,已经可以使用 VALID UNTIL 语法创建有时间限制的用户:

1CREATEUSER mark2IDENTIFIED WITH no_password3VALID UNTIL '2026-10-04';

ClickHouse 26.8 添加了 VALID FOR INTERVAL语法,它从当前时间开始计算截止时间。因此,要让用户从现在起两周内有效,我们会运行以下查询:

1CREATEUSER mark2IDENTIFIED WITH no_password3VALID FORINTERVAL2 WEEKS;

然后我们可以检查该用户的有效期到什么时候:

1SHOWCREATEUSER mark2FORMAT LineAsString;

此查询的输出将全部在一行上,因此我们为了方便阅读进行了手动格式化:

1CREATE USER mark 2IDENTIFIED WITH no_password 3VALID UNTIL '2026-09-18 10:06:29'

我们在修改用户时也可以使用此语法。要让用户改为从现在起一周内有效,我们会运行以下查询:

1ALTERUSER mark VALID FORINTERVAL1 WEEK;
1CREATE USER mark 2IDENTIFIED WITH no_password 3VALID UNTIL '2026-09-11 10:08:34'

流水线 SQL

由 Alexey Milovidov 贡献

ClickHouse 26.8 引入了流水线 SQL,它让你可以自上而下地编写查询,每一步都紧随前一步。

你可以在 ClickHouse 26.8 中的流水线 SQL 博客文章中了解更多信息。

ClickHouse 作为流式 HTTP API

由 Alexey Milovidov 贡献

ClickHouse 26.8 还引入了多项功能,使得围绕你的表和常见查询添加轻量级 HTTP API 变得相当容易。

你可以在 ClickHouse 作为流式 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 来禁用它,从而让我们演示之前的行为。

首先,让我们创建一个包含 100,000 行的源表:

1CREATE TABLE src (id UInt64) 2ORDERBY id;3INSERT INTO src 4SELECT number FROM numbers(100000);

以及一个目标表:

1CREATE TABLE dst (id UInt64) 2ORDERBY id;

接下来,我们将让一个查询插入 50 行,另一个查询创建一个填充 dst 表的物化视图。

要让某一行落入空隙,它的 INSERT 必须在 POPULATE 获取源表快照时仍处于进行中。大多数插入完成得太快,无法做到这一点,因此我们使用 sleepEachRow 将 50 行分散到约两秒半内,并在该插入仍在运行时启动 CREATE

我们还同时使用了 POPULATETO,这在 26.8 之前是语法错误。该视图从 dst 表中已有数据回填现有的 src

1./clickhouse client -q "2  INSERT INTO src SELECT number + 100000 FROM numbers(50)3  WHERE sleepEachRow(0.05) = 0;" &45sleep 0.567./clickhouse client -m -q "8  SET materialized_views_populate_atomically = 0;9  CREATE MATERIALIZED VIEW mv TO dst10  POPULATE AS SELECT id FROM src;"1112wait

运行完成后,我们可以将源表与目标表进行比较:

1SELECT (SELECTcount() FROM src) AS sourceRows,2       (SELECTcount() FROM dst) AS dstRows,3       (SELECT uniqExact(id) FROM dst) AS dstDistinct,4       sourceRows - dstRows AS lost,5       dstRows - dstDistinct AS duplicated6FORMAT PrettyCompact;
1┌─sourceRows─┬─dstRows─┬─dstDistinct─┬─lost─┬─duplicated─┐2│     100050 │  100000 │      100000 │   50 │          0 │3└────────────┴─────────┴─────────────┴──────┴────────────┘

所有 50 条记录都在源表中,但目标表中一条也没有。该插入在 mv 存在之前就已开始,因此它没有推送到该视图,而且它在填充快照获取之后才提交,因此回填也没有看到它。

正好是 50 条,因为插入在查询开始时一次性决定哪些视图将接收其数据,而不是按行或按块决定。所以整个插入要么到达,要么不到达。

如果我们删除 mvsrcdst,然后重新创建 srcdst,我们可以创建物化视图,但这次将 materialized_views_populate_atomically 设置为 1

1./clickhouse client -q "2  INSERT INTO src SELECT number + 100000 FROM numbers(50)3  WHERE sleepEachRow(0.05) = 0;" &45sleep 0.567./clickhouse client -m -q "8  SET materialized_views_populate_atomically = 1;9  CREATE MATERIALIZED VIEW mv TO dst10  POPULATE AS SELECT id FROM src;"1112wait

这次,当我们统计每个表中的记录数时,将得到以下输出:

1┌─sourceRows─┬─dstRows─┬─dstDistinct─┬─lost─┬─duplicated─┐2│     100050 │  100050 │      100050 │    0 │          0 │3└────────────┴─────────┴─────────────┴──────┴────────────┘

日语和中文分词器支持

由 Robert Schulze、Amos Bird、Jimmy Aguilar Mena 贡献

ClickHouse 中的默认分词器(由 tokenshasAllTokenshasAnyTokens 函数使用)是 splitByNonAlpha,它按空白和标点字符将字符串拆分为子字符串数组。与英语和其他印欧语言不同,中文和日文文本单词之间没有空格,因此需要独特的分词器。

26.8 通过两个专门构建的分词器解决了这个问题:

  • japanese - 日语分词器依赖于 MeCab 形态分析器,需要在服务器配置中指定外部 MeCab 词典
  • chinese - 一个 jieba 风格的分词器,结合词典与 HMM(隐马尔可夫模型)来切分未知序列,例如将 ClickHouse是一个快速的开源数据库 切分为 ClickHouse、是、一个、快速、的、开源、数据库,而不是一个整体或单个字符。

日语

要使用日语分词器,你首先需要配置一个词典。在此示例中,我们将使用 UniDic。从网站下载 .zip 归档文件后,你需要获取该归档文件的 SHA256,ClickHouse 会在加载词典前用它来验证词典:

1sha256sum /var/lib/clickhouse/unidic-cwj-202512.zip
1d94216b589d15d05c408ed59abc5259086703ebbac14e225b5314e4cd106c4db

/etc/clickhouse-server/config.d/tokenizers.xml 中添加一个新的分词器,以定义在服务器启动时合并到主 config.xml 文件中的自定义配置。

1<clickhouse>2<tokenizer>3<japanese>4<dictionary_location>file:///var/lib/clickhouse/unidic-cwj-202512.zip</dictionary_location>5<dictionary_sha>d94216b589d15d05c408ed59abc5259086703ebbac14e225b5314e4cd106c4db</dictionary_sha>6</japanese>7</tokenizer>8</clickhouse>

注意

你也可以通过 HTTP/HTTPS URL 或在 S3 兼容存储中指定词典

如果你的服务器已在运行,请确保重启它以让配置生效。现在你可以创建文本索引并像这样查询它:

1CREATE TABLE reviews2(3    id UInt64,4    text String,5    INDEX text_idx text TYPE text(tokenizer ='japanese')6)7ENGINE = MergeTree8ORDERBY id;910INSERT INTO reviews VALUES11(1, '渋谷の新しい寿司屋で美味しいうにを食べた'),      -- "ate" (食べた)12(2, '大阪のラーメンは最高だった、また食べたい'),      -- "want to eat" (食べたい)13(3, '京都で抹茶アイスを食べながら散歩した'),          -- "while eating" (食べながら)14(4, '新幹線に乗って富士山を見に行った');              -- no mention of eating1516SELECT id, text17FROM reviews18WHERE hasAnyTokens(text, ['食べ'], 'japanese')19ORDERBY id;
idtext
1渋谷の新しい寿司屋で美味しいうにを食べた
2大阪のラーメンは最高だった、また食べたい
3京都で抹茶アイスを食べながら散歩した

3 rows in set. Elapsed: 0.007 sec.

在上面的示例中,食べた食べたい食べながら 都是动词 食べる(“吃”)的不同形式,但分词器将每一个都切分为词干 食べ 加上一个单独的活用助词。因此,对 食べ 进行单 token 搜索会正确地检索出第 1–3 行,并跳过第 4 行。

如需了解更多详情,请阅读 日语分词器文档

中文

与日语分词器需要下载外部词典归档文件并在服务器 XML 配置中进行配置不同,中文分词器不需要任何服务器配置设置。其嵌入式词典和隐马尔可夫模型数据(源自 cppjieba)直接内置于 ClickHouse 中。

这个chinese分词器允许你指定一个粒度参数。默认情况下,它被设置为 coarse_grained,但如果你传入 fine_grained,你就可以索引重叠的子词,以提高搜索召回率,代价是索引更大。

下面的查询创建了两个表。第一个表的索引使用 chinese 分词器,并采用默认的 coarse_grained 粒度参数,第二个表的索引使用 fine_grained 参数。

向两个表中插入了四个字符串,它们要么包含单词 大学(“university”)独立出现(第 1 行),要么嵌入在更长的复合词中(第 2 行和第 3 行),要么完全不包含(第 4 行)。

1CREATE TABLE table_coarse2(3    key UInt64,4    str String,5    INDEX text_idx str TYPE text(tokenizer = chinese) -- default: coarse_grained6)7ENGINE = MergeTree ORDERBY key;89CREATE TABLE table_fine10(11    key UInt64,12    str String,13    INDEX text_idx str TYPE text(tokenizer = chinese('fine_grained'))14)15ENGINE = MergeTree ORDERBY key;1617INSERT INTO table_coarse VALUES18    (1, '他考上了大学,很开心'), -- standalone 大学19    (2, '北京邮电大学的通信工程专业很强'), -- compound 北京邮电大学20    (3, '我毕业于北京大学计算机系'), -- compound 北京大学21    (4, '今天天气不错,适合散步'); -- no mention2223INSERT INTO table_fine SELECT*FROM table_coarse;

下面的查询使用 hasAllTokens 函数来搜索 大学(“university”),首先在索引中使用默认 coarse_grained 参数的 chinese 索引的表上搜索,然后在第二个表上使用 fine_grained 参数进行搜索。

对于第一个表,只返回 他考上了大学,很开心(“He got into university and was very happy.”),也就是独立提及 大学 的句子。对于第二个表,独立提及 大学 的句子以及复合词提及的句子都会被返回:

1SELECT2    key,3    str4FROM table_coarse5WHERE hasAllTokens(str, '大学')6ORDERBY key ASC;

Query ID: 7e348059-9cd1-4f72-8026-8d76266050e4

keystr
1他考上了大学,很开心

1 row in set. Elapsed: 0.002 sec.

1SELECT2    key,3    str4FROM table_fine5WHERE hasAllTokens(str, '大学')6ORDERBY key ASC;

Query ID: 4da23c89-8a10-4099-9637-bd2a14f6aa1b

keystr
1他考上了大学,很开心
2北京邮电大学的通信工程专业很强
3我毕业于北京大学计算机系

3 rows in set. Elapsed: 0.002 sec.

这是因为 chinese 分词器在粗粒度模式下将 北京大学(“Peking University”)和 北京邮电大学(“Beijing University of Posts and Telecommunications”)视为单个词典词元,因此在粗粒度分词下,仅搜索子词元“大学”永远不会匹配到这些行,尽管人类阅读句子时会知道这两个句子都与“university”相关匹配。fine_grained 模式正是会额外将这些复合词拆分成其重叠子词的设置,例如 北京(“Beijing”)、邮电(“Post and Telecommunications”)或 大学(“University”),所以它才会返回第 2 行和第 3 行。

新分词器:icusplitByRegexp

由 Jimmy Aguilar Mena 贡献

虽然 Jieba 是为简体和繁体中文设计的,而 MeCab 主要面向日语——ICU(International Components for Unicode)是一个通用的、基于规则的多语言库,使用标准边界分析算法,但我们可能会合理地问:如何为其他在词与词之间不加空格的语言(如泰语、老挝语、高棉语或缅甸语)对文本进行分词?

26.8 版本为 ClickHouse 新增了一个 icu(locale) 分词器,它使用该库的 Unicode 分词将字符串拆分为词元。对于不在词与词之间加空格的文字系统,ICU 会应用基于词典的分词,确保文本被拆分为有意义的多字符词,而不是单个字符。

在 26.8 之前,如果你尝试使用像 asciiCJK 这样的非 ASCII 分词器来对泰语、老挝语、高棉语或缅甸语文本进行分词,就会遇到单字符碎片化问题,因为 asciiCJK 会将每个非 ASCII 字符视为其自己的词元。以泰语单词 บ้าน(“house”)为例:

1SELECT tokens('บ้าน', 'asciiCJK');
1┌─tokens('บ้าน', 'asciiCJK')─┐2│ ['บ','้','า','น']          │3└───────────────────────────┘

是辅音, 是声调符号, 是元音符号,因此分词把这个词拆成了四个无意义的片段。hasAllTokens 只检查每个查询片段是否存在于该行的某处,因此两个完全无关的真实泰语单词可以各自贡献一个片段,并在对与完全不相关的句子(如“The horse is on the mountain”)搜索“house”时产生误报:

1-- "ม้าอยู่บนภูเขา" = "The horse is on the mountain" (no usage of "house")2SELECT tokens('ม้าอยู่บนภูเขา', 'asciiCJK');
1┌─tokens('ม้าอยู่บนภูเขา', 'asciiCJK')──────────────────────┐2│ ['ม','้','า','อ','ย','ู','่','บ','น','ภ','ู','เ','ข','า'] │3└───────────────────────────────────────────────────────┘
1SELECT hasAllTokens('ม้าอยู่บนภูเขา', 'บ้าน', 'asciiCJK');
1┌─hasAllTokens⋯'asciiCJK')─┐2│                        1 │3└──────────────────────────┘4-- false positive

将其与使用 icu 分词器且将 locale 设置为 th 时发生的情况进行比较:

1SELECT tokens('ม้าอยู่บนภูเขา', 'icu', 'th');
1┌─tokens('ม้าอยู่⋯icu', 'th')─┐2│ ['ม้า','อยู่','บน','ภูเขา']  │3└──────────────────────────┘4-- "horse", "is/located", "on", "mountain"
1SELECT hasAllTokens('ม้าอยู่บนภูเขา', 'บ้าน', 'icu(''th'')');
1┌─hasAllTokens⋯u(\'th\')')─┐2│                        0 │3└──────────────────────────┘

提示

你可以查询 system.collations 表,以获取所有受支持的 ICU 区域设置的列表:

对于需要更多控制的情况,26.8 引入了 splitByRegexp 分词器,它允许你使用正则表达式作为分隔符将文本拆分为词元。

ClickHouse 的默认分词器 splitByNonAlpha 会在每个非字母数字的 ASCII 字符处分隔,这意味着像 C++C#F# 这样的文本都会坍缩为同一个裸字母:

1SELECT2    tokens('C++', 'splitByNonAlpha'),3    tokens('C#', 'splitByNonAlpha'),4    tokens('F#', 'splitByNonAlpha')5FORMAT Vertical;
1tokens('C++'⋯yNonAlpha'): ['C']2tokens('C#',⋯yNonAlpha'): ['C']3tokens('F#',⋯yNonAlpha'): ['F']

在实践中,这意味着搜索“C#”可能会返回像“I am an expert in C++”这样的误报:

1SELECT hasAllTokens('I am an expert in C++', 'C#', 'splitByNonAlpha');
1┌─hasAllTokens⋯yNonAlpha')─┐2│                        1 │3└──────────────────────────┘

此外,hasToken 会拒绝包含分隔符字符的查询片段,因此使用 C++C# 字面词与 splitByNonAlpha 一起搜索时完全无法进行。

使用 splitByRegexp,你可以显式定义分隔符模式,使字符 #+ 被视为单词的一部分:

1CREATE TABLE hold_my_beer2(3id UInt64,4description String,5INDEX idx description TYPE text(tokenizer = splitByRegexp('[^\p{L}\p{N}#+]+'))6)7ENGINE = MergeTree ORDERBY id;89INSERT INTO hold_my_beer VALUES10    (1, 'I am an expert in C++'),11    (2, 'I am an expert in C#'),12    (3, 'I use Arch');1314SELECT id, description FROM hold_my_beer WHERE hasAllTokens(description, 'C++') OR hasAllTokens(description, 'C#');
1┌─id─┬─description───────────┐2│  1 │ I am an expert in C++ │3│  2 │ I am an expert in C#  │4└────┴───────────────────────┘

URL 数据库引擎

由 Alexey Milovidov 贡献

在上个月的 26.7 发布博客文章中,我们介绍了 url 表函数和 URL 表引擎现在如何根据指定的 URL schema 分派到正确的后端,除 HTTP 外还支持文件路径、S3、GCS、Azure 和 HDFS。

26.8 引入了一个新的 URL 数据库引擎,它允许你指定一个 URL 前缀,并将远程服务器上的任意路径作为表进行查询。

在上个月的文章中,我们的示例展示了如何使用 url 表引擎直接查询 s3 存储桶:

1SELECTcount(), avg(star_rating) FROM url('s3://datasets-documentation/amazon_reviews/amazon_reviews_2015.snappy.parquet');

现在你可以指定 s3://datasets-documentation/amazon_reviews 作为前缀,并将存储桶中的每个文件作为单独的表进行查询:

1CREATE DATABASE datasets2ENGINE = URL('https://datasets-documentation.s3.eu-west-3.amazonaws.com/amazon_reviews/');3USE datasets;45SELECTcount() FROM'amazon_reviews_2015.snappy.parquet';
1┌──count()─┐2│ 41905631 │ -- 41.91 million3└──────────┘
1SELECTcount() 2FROM'amazon_reviews_2014.snappy.parquet';
1┌──count()─┐2│ 44127569 │ -- 44.13 million3└──────────┘

clickhouse-local 中,默认数据库现在也叠加在 URL 之上,让你可以方便地在默认数据库中查询本地文件、URL、S3 数据或常规表:

1SELECT*FROM'hits.tsv';2SELECT*FROM'https://example.com/hits.tsv';3SELECT*FROM's3://mybucket/hits.tsv';4SELECT*FROMtable;

bigquery 表函数和 BigQuery 表引擎

由 Alexey Milovidov 贡献

对于那些尚未将工作负载从 BigQuery 迁移到 ClickHouse 的用户,26.8 通过引入 bigquery 表函数和 BigQuery 表引擎,使迁移比以往任何时候都更容易。

让我们使用一个现有的 Stack Overflow 帖子 BigQuery 项目(设置步骤)来看看它是如何工作的。

我们将使用 bigquery 表函数先查看数据形态。为此,我们需要向 bigquery 表函数传入一个服务密钥。

创建一个命名集合,并将下面的 your_service_key 替换为从 Google 控制台获取的 .json 服务密钥内容:

1<clickhouse>2<named_collections>3<bigquery_credentials>4<project>bigquery-clickhouse</project>5<dataset>stackoverflow</dataset>6<table>badges</table>7<service_account_key><![CDATA[your_service_key]]></service_account_key>8</bigquery_credentials>9</named_collections>10</clickhouse>

确认你的命名集合存在:

1SELECT*2FROM system.named_collections;
1┌─name────────┬─collection───────────────────────────────────────────────────────────────────┬─source─┬─create_query─┐2│ my_bigquery │ {'dataset':'[HIDDEN]','project':'[HIDDEN]','service_account_key':'[HIDDEN]'} │ CONFIG │              │3└─────────────┴──────────────────────────────────────────────────────────────────────────────┴────────┴──────────────┘

现在使用已命名的集合作为 bigquery 表函数的参数,以查看数据形状:

1DESCRIBETABLE bigquery(bigquery_credentials);
1Query id: dd1e8285-b286-4892-8770-eeed49ef691923┌─name─────┬─type───────────────────────────┬─default_type─┬─default_expression─┬─comment─┬─codec_expression─┬─ttl_expression─┐4│ Id       │ Nullable(Int64)                │              │                    │         │                  │                │5│ UserId   │ Nullable(Int64)                │              │                    │         │                  │                │6│ Name     │ Nullable(String)               │              │                    │         │                  │                │7│ Date     │ Nullable(DateTime64(6, 'UTC')) │              │                    │         │                  │                │8│ Class    │ Nullable(Int64)                │              │                    │         │                  │                │9│ TagBased │ Nullable(Int64)                │              │                    │         │                  │                │10└──────────┴────────────────────────────────┴──────────────┴────────────────────┴─────────┴──────────────────┴────────────────┘11126 rows inset. Elapsed: 0.602 sec.

现在你可以轻松地在本地创建相同的表并插入数据:

1CREATE TABLE bq_badges2(3    Id       Int64,4    UserId   Int64,5    Name     LowCardinality(String),6Date     DateTime64(6, 'UTC'),7    Class    Int64,8    TagBased Int649)10ENGINE = MergeTree11ORDERBY (Id);1213INSERT INTO bq_badges SELECT*FROM bigquery(bigquery_credentials) LIMIT 5; -- I don't want to max out my credit card1415SELECT*FROM bq_badges;
1┌───────Id─┬───UserId─┬─Name────────┬───────────────────────Date─┬─Class─┬─TagBased─┐2│  3336768 │  1033808 │ Copy Editor │ 2012-05-03 22:19:43.187000 │     1 │        0 │3│  6687355 │    19299 │ .net        │ 2013-06-15 03:03:52.690000 │     1 │        1 │4│ 12885730 │  3892259 │ Copy Editor │ 2015-01-31 14:43:59.373000 │     1 │        0 │5│ 39830489 │ 10659482 │ Copy Editor │ 2020-12-09 11:16:11.410000 │     1 │        0 │6│ 40413130 │  1386551 │ Copy Editor │ 2021-01-29 18:00:54.783000 │     1 │        0 │7└──────────┴──────────┴─────────────┴────────────────────────────┴───────┴──────────┘895 rows inset. Elapsed: 0.005 sec.

S3 Tables

由 Konstantin Vedernikov 贡献

ClickHouse 26.8 新增了对 Amazon S3 Tables(AWS 的 Iceberg 表托管服务)的写入支持。除了查询现有表之外,你现在还可以插入数据并创建新表。

首先,让我们启用 Iceberg 目录集成和写入:

1SET allow_database_iceberg =1;2SET allow_insert_into_iceberg =1;

在配置好用于访问你的表存储桶的 AWS 凭证后,我们可以使用 DataLakeCatalog 数据库引擎进行连接。请将下面的区域和仓库 ARN 替换为你自己的:

1CREATE DATABASE tables2ENGINE = DataLakeCatalog(3'https://s3tables.us-east-1.amazonaws.com/iceberg'4)5SETTINGS6    catalog_type ='s3tables',7    region ='us-east-1',8    warehouse ='arn:aws:s3tables:us-east-1:123456789012:bucket/analytics';

然后我们可以列出可用的表并查询其中一个。例如,如果你的存储桶在 events 命名空间中包含一个 ns 表:

1SHOW TABLES FROM tables;23SELECT*4FROM tables.`ns.events`5LIMIT 10;

我们现在还可以写入该表,提供与其模式匹配的值:

1-- Replace … with values matching your table's columns.2INSERT INTO tables.`ns.events` VALUES (…);

Snowflake Horizon

由 Melvyn Peignon 贡献

ClickHouse 26.8 新增了通过 Snowflake Horizon 目录读写 Iceberg 表的支持。ClickHouse 直接从对象存储读取底层数据文件。

让我们启用 Iceberg 目录集成和写入,然后连接到目录。请将下面的账户端点、数据库名称、个人访问令牌和 Snowflake 角色替换为你自己的。该角色需要能够访问你想要使用的 Iceberg 表。

1SET allow_database_iceberg =1;2SET allow_insert_into_iceberg =1;34CREATE DATABASE horizon5ENGINE = DataLakeCatalog(6'https://<org>-<account>.snowflakecomputing.com/polaris/api/catalog'7)8SETTINGS9    catalog_type ='horizon',10    warehouse ='ICEBERG_DB',11    catalog_credential ='<PAT>',12    auth_scope ='session:role:<ROLE>',13    vended_credentials =1;

然后我们可以列出可用的表并查询其中一个。例如,如果你的目录在 trades 命名空间中包含一个 schema 表:

1SHOW TABLES FROM horizon;23SELECT*4FROM horizon.`schema.trades`5LIMIT 10;

要插入数据,请提供与表模式匹配的值:

1-- Replace … with values matching your table's columns.2INSERT INTO horizon.`schema.trades` VALUES (…);

ClickHouse 通过 Horizon 目录提交更改。

Puffin 文件格式

由 Konstantin Vedernikov 贡献

ClickHouse 26.8 新增了对 Puffin 文件格式的支持,Apache Iceberg 使用该格式来存储统计信息和删除向量。

让我们使用 ClickHouse 测试套件中的一个小样本文件来看看它是如何工作的:

1SELECT referenced_data_file, deleted_rows2FROM url(3'https://raw.githubusercontent.com/ClickHouse/ClickHouse/693dee22dda0d7ff23343c326754ccfc24f3237f/tests/queries/0_stateless/data_puffin/file_properties_ok.puffin',4'Puffin'5);
1┌─referenced_data_file───────────┬─deleted_rows─┐2│ /data/table/part-00000.parquet │ [2,5]        │3└───────────────────────────────┴──────────────┘

这告诉我们,所引用的 Parquet 文件中第 2 行和第 5 行的位置被标记为已删除。我们不需要访问那个 Parquet 文件——删除信息存储在 Puffin 文件本身中。这有助于解释为什么数据文件中存在的行在查询 Iceberg 表时不会出现。

我们还可以使用 PuffinMetadata 格式来检查 blob 的类型和属性:

1SELECT blob_type, properties2FROM url(3'https://raw.githubusercontent.com/ClickHouse/ClickHouse/693dee22dda0d7ff23343c326754ccfc24f3237f/tests/queries/0_stateless/data_puffin/file_properties_ok.puffin',4'PuffinMetadata'5);

对于此文件,blob 类型为 deletion-vector-v1,其属性包括一个值为 cardinality2,与上面两个已删除的行位置相匹配。

在 Iceberg 中预取清单文件

由 Asya Shneerson 和 Konstantin Vedernikov 贡献

在从 Iceberg 表读取数据之前,ClickHouse 会读取描述其数据和删除文件的清单文件。获取和处理这些元数据可能会增加明显的启动时间,尤其是当它需要对对象存储发出许多请求时。

在 26.8 中,ClickHouse 在解析当前清单文件的同时预取下一个清单文件,将存储读取与 CPU 工作重叠起来。

删除清单也会被并发读取和解码。由于这些必须在读取数据文件之前处理,这有助于包含大量删除文件的表上的查询更快启动。iceberg_delete_manifest_decode_concurrency 设置控制一次解码多少个删除清单,默认值为 4

此版本还修复了数据湖目录的 S3 存储桶区域缓存,避免重复请求以确定存储桶的区域。

原生 Parquet 改进

由 Alexey Milovidov 和 Vasily Chekalkin 贡献

ClickHouse 26.8 为 Parquet 读取带来了多项改进,帮助查询跳过不必要的数据。

对于带有 ORDER BYLIMIT 的查询,ClickHouse 可以先读取排序和过滤所需的列,然后仅对通过限制的行获取其余列。此优化默认启用。

让我们在一个公共 Parquet 数据集中找到最近的十条事件,使用 WatchID 来打破平局:

1SELECT URL, Title2FROM s3(3'https://clickhouse-public-datasets.s3.amazonaws.com/hits_compatible/hits.parquet',4    NOSIGN5)6ORDERBY EventTime DESC, WatchID DESC7LIMIT 108SETTINGS 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 条件下跳过数据。让我们找出匹配特定手机型号的行:

1SELECTcount()2FROM s3(3'https://clickhouse-public-datasets.s3.amazonaws.com/hits_compatible/hits.parquet',4    NOSIGN5)6WHERE MobilePhoneModel ='GT-C3262';
1┌─count()─┐2│     200 │3└─────────┘

当列块完全字典编码时,如果请求的值不在字典中,ClickHouse 可以跳过其行组。即使最小/最大统计信息无法排除匹配且布隆过滤器不可用,这也有帮助。

在启用字典过滤的情况下,此查询处理了 12 个数据页,而禁用时处理了 226 个。两个查询都返回了 200input_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 BYORDER BY LIMIT 融合。26.8 对任何读取顺序都能做到这一点,剪除不可能出现在结果中的组。
  • GROUP BY 使用单字符串键时,现在使用小得多的哈希表单元格。
  • 并行 GROUP BY 查询中的最终合并步骤现在使用多线程,防止其成为单线程瓶颈。

我们将在另一篇博文中更详细地介绍这些优化。

连接

由 Han Fei、Anton Popov、Robert Schulze、Alexey Milovidov、Vladimir Cherkasov、Alexander Gololobov 贡献

与每个版本一样,我们在连接方面也有更多改进,包括:

  • 列统计信息现在在 INSERT默认情况下用于小表,使得所有 TPC-H 基准测试整体提升了 29%。
  • 条件为两个不等式的 JOIN 过去会作为带过滤的 CROSS JOIN 来执行。现在它们将改用基于排序的 IEJoin 算法。
  • 一种新的合并连接算法 parallel_full_sorting_merge,可在所有核心上运行。输入按键的哈希分片为独立的各分片合并连接。
  • 用于分布式查询计划的基于成本的优化器。新的基于成本的优化器使用基数估计来选择分布式连接、聚合、排序和数据移动的执行方式。

我们还会在另一篇文章中更详细地介绍这些功能。

这篇内容对你有用吗?

反馈只用于改善内容筛选,不等同于收藏

分享这条资讯
分享海报
保存图片
iOS 也可以长按图片保存