ClickHouseInc

ClickHouse 26.1 版本发布说明

图片

本文字数:11903;估计阅读时间:30分钟

作者:ClickHouse Team

Image

又一个月过去了,这意味着我们迎来了新一轮版本发布!

发布概要

ClickHouse 26.1 版本带来了 25 项新特性 🧤、43 项性能优化 🛷,以及 176 个 bug 修复 ⛄

本次发布带来了对带有物化视图的异步插入去重支持、用于索引投影的全新语法、在所有函数中支持 Variant 等更多改进!

Image

新贡献者
特别欢迎 26.1 版本中的所有新贡献者!ClickHouse 社区的不断壮大令人振奋,我们始终感谢每一位为 ClickHouse 如今的流行与成功作出贡献的人。
以下是新贡献者的名字:

Aleksandr Tolkachev, Alex Soffronow Pagonidis, Alexey Bakharew, Andrew Slabko, Arsen Muk, Binnn-MX, Cole Smith, Daniel Muino, Fabian Ponce, Govind R Nair, Hechem Selmi, JIaQi, Jack Danger, JasonLi-cn, Jeremy Aguilon, Josh Carp, Julio Jordan, Karun, Karun Anantharaman, Kirill Kopnev, LeeChaeRok, MakarDev, Matt Klein, Michael Jarrett, Paresh Joshi, Revertionist, Sam Kaessner, Seva Potapov, Shaurya Mohan, Sümer Cip, Xuewei Wang, Yonatan-Dolan, alsugiliazova, gayanMatch, ggmolly, htuall, ita004, jetsetbrand, lijingxuan92, matanper, mostafa, pranavt84, punithns97, rainac1, speeedmaster, withlin

Image

带有物化视图的异步插入去重

由 Sema Checherinda 贡献

ClickHouse 通过写入彼此独立的数据分片来实现极高的插入吞吐量,在写入过程中无需全局同步,然后再由后台任务对这些分片进行合并。不过,创建和合并过多的小分片成本较高,因此为了获得最佳性能,插入操作需要进行批处理。
正如在最初介绍异步插入的博客文章中提到的:使用 ClickHouse 就像驾驶一辆 Formula One 赛车 🏎。动力始终充沛,但要获得最高的插入速度,你必须换到合适的档位。批处理既可以由客户端手动完成,也可以由 ClickHouse 通过异步插入自动完成,后者会在服务器端透明地进行批量处理。

为什么仅有批处理还不够

正如在最初介绍异步插入的博客文章中所指出的,在真实的数据摄取流程中,仅靠批处理仍然不够。插入操作还必须具备幂等性。网络故障、请求超时或节点崩溃,都可能导致我们无法确定某次插入是否已经成功写入持久化存储,从而迫使客户端对同一批数据进行重试。

用于幂等插入的自动去重

ClickHouse 长期以来支持对使用 *MergeTree 系列引擎的表进行幂等插入时的自动去重。启用后,ClickHouse 会为每一次插入分配一个去重标识符,该标识符可以基于插入数据的哈希值生成,也可以由客户端通过插入去重 Token 显式提供。这些标识符会在 Keeper 中进行跟踪,用于在重试时识别并忽略重复插入。借助这一机制,客户端侧的批量插入在发生重试时也能保持安全,不会产生重复数据。

用于异步插入的去重

异步插入建立在相同的机制之上,但去重发生在刷新阶段,而不是针对每一次客户端请求。多个独立的 INSERT 会首先被收集到内存缓冲区中。当缓冲区刷新时,ClickHouse 会为该批次中包含的每一个源插入计算一个去重哈希(或 Token)。

依赖型物化视图的问题(在 26.1 之前)

然而,在 ClickHouse 26.1 之前,当表上存在依赖的增量物化视图时,这一机制无法可靠工作。虽然源表可以正确地对重试的异步插入进行去重,但物化视图写入的经过转换的数据仍然可能被重复写入。这使得在依赖物化视图的数据管道中使用异步插入存在风险。
本次发布修复了这一问题。现在,异步插入及其依赖的物化视图都可以实现端到端去重。所有相关表在处理重试时都保持一致,从而确保即使在复杂的数据摄取流程中,异步插入也可以安全重试而不会产生重复数据。
为了更直观地说明这一点,下面的示例展示了在存在依赖物化视图的情况下,异步插入及其重试是如何实现端到端去重的。

示例:带有物化视图的去重异步插入

我们从一个用于存储原始值的简单基础表开始:
   CREATE TABLE events   (       value UInt64    )    ENGINE = MergeTree    ORDER BY value; 
物化视图的目标表用于存储插入到基础表中的所有原始值之和:
   CREATE TABLE events_mv_target   (       sum UInt64    )    ENGINE = SummingMergeTree    ORDER BY tuple(); 
该物化视图会响应所有插入到基础表的数据,并将新插入值的总和写入目标表。由于目标表使用的是 SummingMergeTree 引擎,物化视图写入的部分聚合结果会通过后台数据分片合并以增量方式进行汇总:
CREATE MATERIALIZED VIEW events_mvTO events_mv_targetASSELECT    sum(value) AS sumFROM events;
在上述 schema 创建完成后,下图展示了异步 INSERT 如何被缓冲、去重,并最终写入基础表和物化视图的目标表。首先是初始批次,然后是一次重试的情况。

图 1: 带有依赖物化视图的初始异步插入 

Image
① 三个彼此独立的 INSERT 首先被缓存在内存中,随后 ② 作为一个批次统一刷新。在刷新过程中,这些缓冲的插入会被合并为一个(或多个)内存块,其大小受 max_insert_block_size 限制。ClickHouse 会为每一个源 INSERT 计算一个去重哈希,并在整个插入处理流程中将这些哈希与对应数据一并传递。
生成的内存块首先进入 ③ 基础表处理阶段。在此阶段,每个去重哈希都会独立地与 Keeper 中记录的基础表去重状态进行比对。任何重复的子块都会被过滤掉,随后剩余数据按照主键排序,并以压缩形式写入为新的基础表数据分片。
同一个内存块及其去重哈希也会被传递到 ④ 物化视图处理阶段。系统会针对物化视图目标表单独执行去重,然后通过视图的 SELECT 查询对数据进行转换,并以压缩形式写入为物化视图目标表中的数据分片。

图 2: 带有依赖物化视图的重试 

Image
① 第二批异步 INSERT 被缓存在内存中,其中包含一个重试的 INSERT 和一个新的 INSERT。② 当缓冲区刷新时,ClickHouse 会为这两个 INSERT 计算去重哈希,并识别出其中的重试 INSERT 之前已经处理过。
在 ③ 基础表处理阶段,与重试 INSERT 对应的子块会根据 Keeper 中保存的去重状态被过滤掉,只有新增数据会被排序并写入为新的基础表数据分片。
在 ④ 物化视图处理阶段也会独立执行相同的过滤逻辑。在应用物化视图的转换逻辑之前,重试的子块会被丢弃,从而确保只有新增数据会参与最终的聚合计算。
图中展示后台合并仅用于说明整体流程;它们与异步插入和去重机制无关,而是 MergeTree 正常运行过程的一部分。
关键要点:在 ClickHouse 中,去重是以表为单位进行的。基础表和物化视图分别独立跟踪重复数据。因此,在发生重试时,所有相关表都会一致地过滤重复数据,物化视图也不会因为基础表已经处理过某条数据而出现跳过或重复写入的情况。

Image

索引投影的新语法

由 Amos Bird 贡献 

在 ClickHouse 25.6 和 25.11 版本中,我们让投影真正具备了类似二级索引的行为特性。投影不再需要存储完整的数据副本,而是只保存排序键以及一个指向基础表的 _part_offset 指针,从而大幅降低存储开销。
在下面的 schema 中,我们通过这种方式创建了 by_time 和 by_town 两个投影:
   CREATE OR REPLACE TABLE uk.uk_price_paid_with_proj    (        price UInt32,        ...        PROJECTION by_time (            SELECT _part_offset ORDER BY date        ),        PROJECTION by_town (            SELECT _part_offset ORDER BY town        )    )    ENGINE = MergeTree    ORDER BY (postcode1, postcode2, addr1, addr2); 
在 ClickHouse 26.1 中,我们为索引投影引入了新的定义语法。现在可以改为如下方式定义 by_time 和 by_town 投影:
CREATE OR REPLACE TABLE uk.uk_price_paid_with_proj(    price UInt32,    ...    PROJECTION by_time INDEX date TYPE basic,        PROJECTION by_town INDEX town TYPE basic)ENGINE = MergeTreeORDER BY (postcode1, postcode2, addr1, addr2);

Image

LowCardinality 列上的 DISTINCT 性能提升

由 Nihal Z. Miaji 贡献

ClickHouse 一直将 LowCardinality 列作为重点优化对象。在最近的一个版本中,我们通过对聚合合并阶段进行并行化改造,大幅提升了 LowCardinality 键上的 GROUP BY 性能,消除了小规模固定键聚合场景下长期存在的瓶颈。(负责该优化的工程师也撰写了一篇博客分享他的优化历程)
在 ClickHouse 26.1 中,DISTINCT 也和 GROUP BY 一样,开始受益于针对 LowCardinality 的专项优化。
这一优化最初作为一份“圣诞工程礼物”推出,更多细节可以参考配套文章。
从整体原理来看:当 DISTINCT 查询作用于 LowCardinality 列时,ClickHouse 现在可以直接基于列的字典表示进行处理,而无需将完整值展开。这样可以减少 CPU 计算开销,提升缓存局部性表现,并显著加快在低基数列上的 DISTINCT 查询速度。
综合来看,这些优化让 GROUP BY 和 DISTINCT 在 LowCardinality 数据上的性能都得到明显提升。这类数据在分析型工作负载中非常常见,例如状态码、类别、区域或枚举等维度字段。

Image

MergeTree 表与 Keeper 的诊断与可观测性增强 
ClickHouse 在设计之初就强调可观测性,通过 SQL 来观测自身运行状态。从系统表和日志,到专门的内部诊断函数,查询执行、后台合并、数据分片、副本状态以及协调机制等内部行为,都可以直接通过在 ClickHouse 上运行 SQL 进行检查。
ClickHouse 26.1 延续了这一理念,新增了多项相关工具。本次发布引入了新的系统表,扩展了现有系统表,并新增了一个表函数,以增强对 MergeTree 内部状态和 Keeper 运行情况的可见性。这些改进让性能问题排查、索引使用分析以及大规模集群运维变得更加高效和可靠。

新的系统表:zookeeper_info 由 Smita Kulkarni 贡献 

新的系统表 zookeeper_info 允许你直接查看 Keeper 集群的运行状态,提供包括集群规模、延迟情况、是否为 Leader、数据量等在内的多项信息。
SELECT * FROM system.zookeeper_info;
Row 1:──────zookeeper_cluster_name:   zookeeperhost:                     localhostport:                     9181index:                    0is_connected:             1is_readonly:              0version:                  v26.2.1.90-testing-a44e1...avg_latency:              1max_latency:              100min_latency:              0packets_received:         4598packets_sent:             4738

Keeper 的 Web UI 与 HTTP 接口 由 Alexander Tolkachev 和 Artem Brustovetskii 贡献 

除了前文提到的新系统表外,ClickHouse 26.1 还为 Keeper 引入了一个内置的 Web 仪表板,用于监控、健康检查以及存储管理。通过该仪表板,你可以浏览 Keeper 中存储的数据,查看节点内容,甚至可以直接在 UI 中编辑节点数据。
你可以在版本发布的网络研讨会上观看 Alexey 对该仪表板的演示。
除了可视化仪表板外,现在还提供了一个 API,允许运维人员通过浏览器或 HTTP 客户端查看集群状态、执行命令以及管理 Keeper 存储。

system.parts 中的文件信息 由 Gayan Match 贡献

ClickHouse 将数据以不可变的数据分片形式存储:每个分片都是磁盘上的一个目录,包含压缩后的列文件、索引以及元数据。
系统表 system.parts 用于查看当前存在的所有数据分片。在 ClickHouse 26.1 中,该表新增了一个 files 列,使你能够了解每个分片包含的文件数量。这有助于分析磁盘布局、schema 复杂度,以及插入、查询和合并等行为特征。
SELECT name, rows, marks, bytes, files FROM system.partsWHERE database = 'default' AND table = 'github_events';
┌─name─────────────┬───────rows─┬─marks──┬──────────bytes─┬─files─┐│ all_0_0_0_288     │ 4430017383 │ 576807 │ 332974330897   │   145 ││ all_1_1_0_288     │     853559 │    108 │     77871986   │   196 ││ all_2_2_0_288     │   22523075 │   2783 │   1618742341   │   196 ││ all_3_3_0_288     │     855252 │    107 │     42151604   │   202 ││ all_4_4_0_288     │  612539497 │  75810 │  45082755883   │   188 ││ all_5_5_0_288     │ 1082739624 │ 138264 │ 123406140554   │   145 ││ all_6_6_0_288     │ 1546160205 │ 191296 │ 101227190998   │   198 ││ all_7_2820_6_288  │ 2225987644 │ 278473 │ 157837202094   │   192 │└───────────────────┴────────────┴────────┴────────────────┴───────┘

mergeTreeAnalyzeIndexes 表函数 由 Azat Khuzhin 贡献

现有的 mergeTreeIndex 表函数可用于查看 MergeTree 表主索引和 marks 文件的内容,也就是直接查看这些磁盘文件中的数据结构。
在 ClickHouse 26.1 中,我们新增了一个配套的表函数 mergeTreeAnalyzeIndexes,用于展示这些索引在查询执行过程中是如何实际发挥作用的。针对指定查询,它会返回在应用主索引和数据跳过索引之后,每个数据分片中需要扫描的精确行范围。
借助这一功能,你可以清晰地看到 ClickHouse 在查询执行期间是如何裁剪数据的:哪些分片被访问,哪些数据范围在索引过滤后被保留,最终实际读取了哪些数据。这为分析和调试索引效果提供了非常有力的工具。结合系统表、Web UI 和 HTTP API,用户可以根据自己的偏好选择通过 SQL、浏览器界面或程序化方式来查看和管理 Keeper。
SELECT * FROM mergeTreeAnalyzeIndexes(    default, github_events, repo_name = 'ClickHouse/ClickHouse');
SELECT * FROM mergeTreeAnalyzeIndexes(    default, github_events, repo_name = 'ClickHouse/ClickHouse')

表(ASCII):

┌─part_name──────────┬─ranges──────────────────────────────────────────────────┐│ all_0_0_0_288      │ [(101,102),(2149,2150),(6938,6940),(81644,...           ││ all_1_1_0_288      │ [(0,1),(10,11),(12,15),(18,19),(20,22),(27...           ││ all_2_2_0_288      │ [(0,1),(8,9),(32,33),(295,296),(300,301),(...           ││ all_3_3_0_288      │ [(0,2),(9,13),(15,18),(21,22),(26,27),(98,...           ││ all_4_4_0_288      │ [(21,22),(309,310),(1041,1043),(10038,1003...           ││ all_5_5_0_288      │ [(55,56),(854,855),(2423,2424),(22336,2233...           ││ all_6_6_0_288      │ [(64,65),(893,894),(2721,2723),(24902,2490...           ││ all_7_2820_6_288   │ [(12,13),(207,208),(2688,2691),(32469,3247...           │└────────────────────┴─────────────────────────────────────────────────────────┘

Image

reverseBySeparator

由 Xuewei Wang 贡献

ClickHouse 26.1 还引入了 reverseBySeparator 函数。该函数可以在不将字符串拆分为数组的情况下,对一个以分隔符分隔的序列进行反转。
例如:
SELECT reverseBySeparator('benchmark.clickhouse.com', '.') AS x
┌─x────────────────────────┐│ com.clickhouse.benchmark │└──────────────────────────┘
在此之前,我们可能需要结合 arrayStringConcat、reverse 和 splitByChar 等函数来实现相同的效果:
SELECT arrayStringConcat(  reverse(    splitByChar('.','benchmark.clickhouse.com')  ),   '.') AS x;

Image

Variant 在所有函数中的支持 

由 Bharat Nallan 贡献 

在 ClickHouse 24.1 中引入的 Variant 类型,现在已经可以在所有函数中使用。
来看一个在 26.1 中可以正常执行、但在之前版本中无法运行的查询示例。
SELECT length('ClickHouse'::Variant(String, UInt32));
这个查询在过去会报出如下错误:
Received exception:Code: 43. DB::Exception: Illegal type Variant(String, UInt32) of argument of functionlength: InscopeSELECTlength(CAST('ClickHouse', 'Variant(String, UInt32)')). (ILLEGAL_TYPE_OF_ARGUMENT)
而现在则可以返回字符串 ClickHouse 的长度:
┌─length(CAST(⋯ UInt32)'))─┐│                       10 │└──────────────────────────┘
再举一个例子,假设我们有如下表并插入了一些数据:
CREATE TABLE test (  v Variant(UInt32, String, Array(String)));INSERT INTO test VALUES('ClickHouse'),(42),(10),(['We', 'Love', 'Clickhouse']);
如果我们希望返回值大于 10 的行:
SELECT *FROM testWHERE v > 10;
在 ClickHouse 26.1 之前,会出现如下错误:
Received exception:Code: 43. DB::Exception: Illegal types of arguments (`Variant(Array(String), String, UInt32)`, `UInt8`) of function `greater`: In scope SELECT * FROM test WHERE v > 10. (ILLEGAL_TYPE_OF_ARGUMENT)
而在 26.1 中,则会得到如下结果:
┌─v──┐│ 42 │└────┘

Image

文本索引改进
本次发布对文本索引进行了多项改进。该功能在 ClickHouse 25.12 中已进入 beta 阶段。
首先,文本索引现在支持 sparseGrams 分词器,该功能由 Anton Popov 和 Konstantin Vedernikov 贡献。
sparseGrams 函数本身是在 ClickHouse 25.5 中引入的。它会在给定字符串中查找所有长度至少为 n 的子字符串,并要求该子字符串两端的 (n-1)-gram 的哈希值严格大于子字符串内部任意 (n-1)-gram 的哈希值。该函数使用 CRC32 作为哈希函数。
在搜索阶段,查询引擎可以优先选择搜索字符串中最长的 n-gram,并忽略那些已被覆盖的较短 n-gram,从而使用更少、更具体且更稀有的 token 来执行搜索。
我们通过 Hacker News 数据集来看看这一机制的效果。首先,使用 splitByNonAlpha 分词器创建一个表:
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,    INDEX inv_idx(text) TYPE text(        tokenizer = 'splitByNonAlpha'    )    GRANULARITY 128)ENGINE = MergeTreeORDER BY time;
然后,再创建一个使用 sparseGrams 分词器的表:
CREATE TABLE hackernews_sparseGrams(    `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,    INDEX inv_idx(text) TYPE text(        tokenizer = sparseGrams(3, 20, 5),        preprocessor = lower(text)    )    GRANULARITY 128)ENGINE = MergeTreeORDER BY time;
在一台 Apple M2 Max 上,向 hackernews 表插入约 2800 万条记录所需时间如下:
28737557 rows in set. Elapsed: 120.358 sec. Processed 28.74 million rows, 4.98 GB (238.77 thousand rows/s., 41.36 MB/s.)Peak memory usage: 1.36 GiB.
而向 hackernews_sparseGrams 表插入相同数据所需时间如下:
28737557 rows in set. Elapsed: 1162.247 sec. Processed 28.74 million rows, 4.98 GB (24.73 thousand rows/s., 4.28 MB/s.)Peak memory usage: 4.37 GiB.
我们还可以比较不同文本索引占用的存储空间:
SELECT table,       formatReadableSize(sum(data_compressed_bytes)) AS data,       formatReadableSize(sum(secondary_indices_compressed_bytes)) AS secondaryIndicesFROM system.partsWHERE table LIKE 'hackernews%'GROUP BY ALL;
┌─table──────────────────┬─data─────┬─secondaryIndices─┐│ hackernews             │ 6.77 GiB │ 2.00 GiB         ││ hackernews_sparseGrams │ 6.77 GiB │ 16.19 GiB        │└────────────────────────┴──────────┴──────────────────┘
使用 sparse grams 的表,其二级索引占用了 14 GiB;相比之下,另一个表的索引占用要小得多。该表的总存储空间为 23 GiB,而另一个表不到 9 GiB,可以看出存储开销显著增加。
让我们在数据集中查询,找出发布关于关系型数据库内容最多的用户。首先在 hackernews 表上执行:
SELECT by, count()FROM hackernewsWHERE text LIKE '%relational database%'GROUP BY ALLORDER BY count() DESCLIMIT 10;
10 rows in set. Elapsed: 1.482 sec. Processed 28.74 million rows, 9.77 GB (19.39 million rows/s., 6.59 GB/s.)Peak memory usage: 169.01 MiB.10 rows in set. Elapsed: 1.454 sec. Processed 28.74 million rows, 9.77 GB (19.76 million rows/s., 6.72 GB/s.)Peak memory usage: 168.84 MiB.10 rows in set. Elapsed: 1.392 sec. Processed 28.64 million rows, 9.74 GB (20.58 million rows/s., 7.00 GB/s.)Peak memory usage: 169.69 MiB.
该查询的最短执行时间为 0.883 秒,性能提升约 36%。
此外,文本索引现在还可以应用于 Strings 或 FixedStrings 类型的数组。例如,我们可以创建如下表:
CREATE TABLE tab(    key UInt64,    val Array(String),    INDEX idx(val) TYPE text(        tokenizer = 'splitByNonAlpha', preprocessor = lower(val)));
随后即可编写如下查询,并利用该索引:
SELECT count() FROM tab WHERE hasAllTokens(val, 'clickhouse');

Image

QBit 进入 beta 阶段

由 Raufs Dunamalijevs 贡献

QBit 是一种面向向量嵌入的专用数据类型,支持在运行时动态调节搜索精度。该类型在 ClickHouse 25.10 中首次引入。
在 ClickHouse 26.1 中,QBit 已正式进入 beta 阶段。

Image

开源 Kubernetes Operator

由 Grigory Pervakov 贡献

最后,多年来我们不断收到社区关于开源 Kubernetes Operator 的请求。
现在,这一 Operator 已正式开源,支持自动化集群部署、垂直与水平扩缩容、配置管理等多项能力。
更多细节可以参阅博客文章《Introducing the Official ClickHouse Kubernetes Operator: Seamless Analytics at Scale》(https://clickhouse.com/blog/clickhouse-kubernetes-operator)。

/END/

试用阿里云 ClickHouse企业版

轻松节省30%云资源成本?阿里云数据库ClickHouse 云原生架构全新升级,首次购买ClickHouse企业版计算和存储资源组合,首月消费不超过99.58元(包含最大16CCU+450G OSS用量)了解详情:https://t.aliyun.com/Kz5Z0q9G

图片
图片

征稿启示

面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:[email protected]

图片图片