博客 / 工程

ClickHouse 24.12 版本发布

author avatar
ClickHouse 团队
2025 年 1 月 9 日 - 18 分钟阅读

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

ClickHouse 24.12 版本包含 16 个新功能 🦃 16 项性能优化 ⛸️ 36 个错误修复 🏕️

在此版本中,我们改进了 Enum 的可用性、Iceberg REST 目录和模式演变支持、反向表排序、使用 JSON 子列作为主键的能力、自动 JOIN 重排序等等!

新贡献者

与往常一样,我们特别欢迎 24.12 版本中的所有新贡献者!ClickHouse 的受欢迎程度很大程度上归功于社区的贡献。看到社区不断壮大总是令人感到荣幸。

以下是新贡献者的名字

Emmanuel Dias, Xavier Leune, Zawa_ll, Zaynulla, erickurbanov, jotosoares, zhangwanyun1, zwy991114, “JiaQi

提示:如果您好奇我们是如何生成此列表的……这里

您还可以查看演示文稿的幻灯片

Enum 可用性改进

由 ZhangLiStar 贡献

此版本还改进了使用 Enum 时的可用性。我们将借助 Reddit 评论数据集来探索这些改进。我们将创建一个只包含几列的表

    
1CREATE TABLE reddit
2    (
3        subreddit LowCardinality(String),
4        subreddit_type Enum(
5            'public' = 1, 'restricted' = 2, 'user' = 3, 
6            'archived' = 4, 'gold_restricted' = 5, 'private' = 6
7        ),
8    )
9    ENGINE = MergeTree
10    ORDER BY (subreddit);

我们可以像这样插入数据

1INSERT INTO reddit
2SELECT subreddit, subreddit_type
3FROM s3(        
4  'https://clickhouse-public-datasets.s3.eu-central-1.amazonaws.com/reddit/original/RC_2017-12.xz',
5  'JSONEachRow'
6);

假设我们要统计 subreddit_type 中类型包含字符串 e 的帖子数量。我们可以使用 LIKE 运算符编写以下查询

    
1SELECT
2    subreddit_type,
3    count() AS c
4FROM reddit
5WHERE subreddit_type LIKE '%restricted%'
6GROUP BY ALL
7ORDER BY c DESC;

如果我们在 24.12 之前的版本中运行此查询,我们将看到如下错误消息

Received exception:
Code: 43. DB::Exception: Illegal type Enum8('public' = 1, 'restricted' = 2, 'user' = 3, 'archived' = 4, 'gold_restricted' = 5, 'private' = 6) of argument of function like: In scope SELECT subreddit, count() AS c FROM reddit WHERE subreddit_type LIKE '%e%' GROUP BY subreddit ORDER BY c DESC LIMIT 20. (ILLEGAL_TYPE_OF_ARGUMENT)

如果我们在 24.12 版本中运行它,我们将得到以下结果

   ┌─subreddit_type─┬──────c─┐
1. │ restricted     │ 6982632.user39640 │
   └────────────────┴────────┘

等式和 IN 运算符现在也接受未知值。例如,以下查询返回类型为 Foopublic 的任何记录

SELECT count() AS c
FROM reddit
WHERE subreddit_type IN ('Foo', 'public')
GROUP BY ALL;

如果我们在 24.12 之前的版本中运行此查询,我们将看到如下错误消息

Received exception:
Code: 691. DB::Exception: Unknown element 'Foo' for enum: while converting 'Foo' to Enum8('public' = 1, 'restricted' = 2, 'user' = 3, 'archived' = 4, 'gold_restricted' = 5, 'private' = 6). (UNKNOWN_ELEMENT_OF_ENUM)

如果我们在 24.12 版本中运行它,我们将得到以下结果

   ┌────────c─┐
1.85235907-- 85.24 million
   └──────────┘

反向表排序

由 Amos Bird 贡献

此版本添加了一个新的 MergeTree 设置 allow_experimental_reverse_key,该设置支持在 MergeTree 排序键中进行降序排序。您可以在下面看到用法示例

ENGINE = MergeTree 
ORDER BY (time DESC, key)
SETTINGS allow_experimental_reverse_key=1;

此表将按降序对 time 字段进行排序。

像这样对数据进行排序的功能对于 时间序列分析(尤其是 Top N 查询)非常有用。

JSON 子列作为表主键

由 Pavel Kruglov 贡献

提醒一下,ClickHouse 新的 强大的 JSON 实现以真正的列式方式存储每个唯一 JSON 路径的值

0_release_24_12.png

上图概述了 ClickHouse 如何将任何插入的 JSON 键路径存储(和读取)为原生子列,从而实现高数据压缩并保持在经典类型上看到的查询性能。

此版本现在支持使用 JSON 子列作为表的主键列

CREATE TABLE T
(
    data JSON()
)
ORDER BY (data.a, data.b);

这意味着,摄取的 JSON 文档(每个 表分区)在磁盘上按用作主键列的 JSON 子列 排序 存储。此外,ClickHouse 将为 主索引文件创建 加速 筛选主键列的查询

1_release_24_12.png

此外,使用 JSON 子列作为主键列 能够 为子列的 *.bin 数据文件实现最佳压缩率,前提是主键列按基数升序排列。

让我们看一个更具体的例子。

我们使用 AWS EC2 m6i.8xlarge 实例作为测试机器,配备 32 个 vCPU 和 128 GiB 主内存,并使用 Bluesky 数据集作为测试数据集

我们将 1 亿个 Bluesky 事件(每个事件一个 JSON 文档)加载到两个 ClickHouse 表中。

这是第一个不使用任何 JSON 子列作为主键列的表

CREATE TABLE bluesky_100m_raw
(
    data JSON()
)
ORDER BY ();

第二个表使用一些 JSON 子列作为主键列(另外还可选地为这些列添加一些 类型提示,以消除查询中的一些类型转换)

1CREATE TABLE bluesky_100m_primary_key
2(
3    data JSON(
4        kind LowCardinality(String), 
5        commit.operation LowCardinality(String), 
6        commit.collection LowCardinality(String), 
7        time_us UInt64
8    )
9)
10ORDER BY (
11    data.kind, 
12    data.commit.operation, 
13    data.commit.collection, 
14    fromUnixTimestamp64Micro(data.time_us)
15);

两个表都包含相同的 1 亿个 JSON 文档。

现在,我们在没有主键的表上运行一个查询(“人们何时在 BlueSky 上屏蔽他人”——改编自您可以在 ClickHouse SQL Playground 上运行的“人们何时使用 BlueSky?”查询

1SELECT
2    toHour(fromUnixTimestamp64Micro(data.time_us::UInt64)) AS hour_of_day,
3    count() AS block_events
4FROM bluesky_100m_raw
5WHERE (data.kind = 'commit') 
6AND (data.commit.operation = 'create') 
7AND (data.commit.collection = 'app.bsky.graph.block')
8GROUP BY hour_of_day
9ORDER BY hour_of_day ASC;
    ┌─hour_of_day─┬─block_events─┐
 1. │           0 │        89395 │
 2. │           1 │       143542 │
 3. │           2 │       154424 │
 4. │           3 │       162894 │
 5. │           4 │        65893 │
 6. │           5 │        39556 │
 7. │           6 │        34359 │
 8. │           7 │        35230 │
 9. │           8 │        30812 │
10. │           9 │        35620 │
11. │          10 │        31094 │
12. │          16 │        33359 │
13. │          17 │        65555 │
14. │          18 │        65135 │
15. │          19 │        65775 │
16. │          20 │        70096 │
17. │          21 │        65640 │
18. │          22 │        75840 │
19. │          23 │       143024 │
    └─────────────┴──────────────┘

19 rows in set. Elapsed: 0.607 sec. Processed 100.00 million rows, 10.21 GB (164.83 million rows/s., 16.83 GB/s.)
Peak memory usage: 337.52 MiB.

让我们在带有主键的表上运行相同的查询(请注意,查询筛选主键列的前缀)

1SELECT
2    toHour(fromUnixTimestamp64Micro(data.time_us)) AS hour_of_day,
3    count() AS block_events
4FROM bluesky_100m_primary_key
5WHERE (data.kind = 'commit') 
6AND (data.commit.operation = 'create') 
7AND (data.commit.collection = 'app.bsky.graph.block')
8GROUP BY hour_of_day
9ORDER BY hour_of_day ASC;
    ┌─hour_of_day─┬─block_events─┐
 1. │           0 │        89395 │
 2. │           1 │       143542 │
 3. │           2 │       154424 │
 4. │           3 │       162894 │
 5. │           4 │        65893 │
 6. │           5 │        39556 │
 7. │           6 │        34359 │
 8. │           7 │        35230 │
 9. │           8 │        30812 │
10. │           9 │        35620 │
11. │          10 │        31094 │
12. │          16 │        33359 │
13. │          17 │        65555 │
14. │          18 │        65135 │
15. │          19 │        65775 │
16. │          20 │        70096 │
17. │          21 │        65640 │
18. │          22 │        75840 │
19. │          23 │       143024 │
    └─────────────┴──────────────┘

19 rows in set. Elapsed: 0.011 sec. Processed 1.47 million rows, 16.16 MB (129.69 million rows/s., 1.43 GB/s.)
Peak memory usage: 2.18 MiB.

结果惊人:查询速度提高了 50 倍,内存使用量减少了 150 倍。

Iceberg REST 目录和模式演变支持

由 Daniil Ivanik 和 Kseniia Sumarokova 贡献

此版本引入了对查询 Apache Iceberg REST 目录的支持。目前,支持 Unity 和 Polaris 目录。我们首先使用 Iceberg 表引擎创建一个表

CREATE TABLE unity_demo
ENGINE = Iceberg('https://dbc-55555555-5555.cloud.databricks.com/api/2.1/unity-catalog/iceberg')
SETTINGS
  catalog_type = 'rest',
  catalog_credential = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:...',
  warehouse = 'unity',
  oauth_server_uri = 'https://dbc-55555555-5555.cloud.databricks.com/oidc/v1/token',
  auth_scope = 'all-apis,sql';

然后,我们可以查询目录底层表中的数据


SHOW TABLES FROM unity_demo;
SELECT * unity_demo."webinar.test";

Iceberg 表函数支持模式演变,包括随时间添加或删除的列、重命名的列以及在原始类型之间更改的数据类型。

默认情况下启用并行哈希连接

由 Nikita Taranov 贡献

每个 ClickHouse 版本都带来了 JOIN 改进,由于这是我们的特殊圣诞版本,因此它加载了满满一雪橇的 JOIN 增强功能!✨

24.11 版本发布帖子中,我们简要提到 并行哈希连接现在是 ClickHouse 的 默认连接策略。在这篇文章中,我们将通过一个具体的例子来演示此更改的性能改进。

我们使用 AWS EC2 m6i.8xlarge 实例作为测试机器,配备 32 个 vCPU 和 128 GiB 主内存。

我们使用比例因子为 100 的 TPC-H 数据集作为表连接的测试数据集,这意味着所有表中存储的数据总量为 100 GB。
我们通过 遵循文档中的说明创建并加载了 8 个表(模拟批发供应商的数据仓库)。

现在,我们使用 ClickHouse 之前的默认连接策略——哈希连接,运行 标准 TPC-H 基准查询集中的查询 3

1SELECT
2    l_orderkey,
3    sum(l_extendedprice * (1 - l_discount)) AS revenue,
4    o_orderdate,
5    o_shippriority
6FROM
7    customer,
8    orders,
9    lineitem
10WHERE
11    c_mktsegment = 'BUILDING'
12    AND c_custkey = o_custkey
13    AND l_orderkey = o_orderkey
14    AND o_orderdate < DATE '1995-03-15'
15    AND l_shipdate > DATE '1995-03-15'
16GROUP BY
17    l_orderkey,
18    o_orderdate,
19    o_shippriority
20ORDER BY
21    revenue DESC,
22    o_orderdate
23FORMAT Null
24SETTINGS join_algorithm='hash';
0 rows in set. Elapsed: 38.305 sec. Processed 765.04 million rows, 15.03 GB (19.97 million rows/s., 392.40 MB/s.)
Peak memory usage: 25.42 GiB.

接下来,我们使用 ClickHouse 新的默认连接策略——并行哈希连接,运行相同的查询

1SELECT
2    l_orderkey,
3    sum(l_extendedprice * (1 - l_discount)) AS revenue,
4    o_orderdate,
5    o_shippriority
6FROM
7    customer,
8    orders,
9    lineitem
10WHERE
11    c_mktsegment = 'BUILDING'
12    AND c_custkey = o_custkey
13    AND l_orderkey = o_orderkey
14    AND o_orderdate < DATE '1995-03-15'
15    AND l_shipdate > DATE '1995-03-15'
16GROUP BY
17    l_orderkey,
18    o_orderdate,
19    o_shippriority
20ORDER BY
21    revenue DESC,
22    o_orderdate
23FORMAT Null
24SETTINGS join_algorithm='default';
0 rows in set. Elapsed: 5.099 sec. Processed 765.04 million rows, 15.03 GB (150.04 million rows/s., 2.95 GB/s.)
Peak memory usage: 29.65 GiB.

使用并行哈希连接,查询速度提高了约 8 倍。

自动 JOIN 重排序

由 Vladimir Cherkasov 贡献

我们的圣诞版本的下一个 JOIN 改进是自动连接重排序。

提醒一下,ClickHouse 的 最快 连接算法(例如其新的默认算法 并行哈希连接)基于内存哈希表,其工作原理是:① 首先将连接查询右侧表中的数据加载到哈希表中(这也称为构建阶段),② 然后流式传输左侧表中的数据,并通过在哈希表中查找进行连接(这称为扫描阶段)

2_release_24_12.png

请注意,由于 ClickHouse 采用右侧表并在 RAM 中使用其数据创建哈希表,因此将较小的表放在 JOIN 的右侧更节省内存,并且通常速度更快。

同样,ClickHouse 基于外部排序的附加的非内存绑定 连接算法(例如 部分合并连接)也具有构建和扫描阶段。例如,部分合并连接首先构建右表的排序版本,然后扫描左表。因此,将较小的表放在 JOIN 的右侧通常速度更快。

ClickHouse 现在不再总是将连接的右表用于构建阶段,而是有一个新设置 - query_plan_join_swap_table - 用于确定连接的哪一侧应该是构建表。可能的值包括

  • auto(默认值):在此模式下,ClickHouse 将尝试选择行数最少的表用于构建阶段。这几乎对每个连接查询都有好处。
  • false:从不交换表(右表是构建表)。
  • true:始终交换表(左表是构建表)。

我们将使用 TPC-H 表上的另一个查询(请参阅上一节中的说明,了解如何创建和加载表,以及有关测试硬件的信息)来演示新 query_plan_join_swap_table 设置的 auto 模式,我们在其中连接 lineitem 表和 part 表。

首先,我们检查这两个表的大小

SELECT
    table,
    formatReadableQuantity(sum(rows)) AS rows,
    formatReadableSize(sum(bytes_on_disk)) AS size_on_disk
FROM system.parts
WHERE active AND (table IN ['lineitem', 'part'])
GROUP BY table
ORDER BY table ASC;
   ┌─table────┬─rows───────────┬─size_on_disk─┐
1. │ lineitem │ 600.04 million │ 26.69 GiB    │
2. │ part     │ 20.00 million  │ 896.47 MiB   │
   └──────────┴────────────────┴──────────────┘

如您所见,lineitem 表明显大于 part 表。

下一个查询连接 lineitem 表和 part 表,并将大得多的 lineitem 表放在连接的右侧

1SELECT 100.00 * sum(
2  CASE
3  WHEN p_type LIKE 'PROMO%'
4  THEN l_extendedprice * (1 - l_discount)
5  ELSE 0 END) / sum(l_extendedprice * (1 - l_discount)) AS promo_revenue
6FROM part, lineitem
7WHERE l_partkey = p_partkey;

我们使用设置为 false 的新 query_plan_join_swap_table 设置运行此查询,这意味着,像往常一样,右表是构建表,因此 ClickHouse 首先将来自非常大的 lineitem 表的数据加载到主内存中(并行加载到多个哈希表中,因为 并行哈希连接是默认连接算法

1SELECT 100.00 * sum(
2  CASE
3  WHEN p_type LIKE 'PROMO%'
4  THEN l_extendedprice * (1 - l_discount)
5  ELSE 0 END) / sum(l_extendedprice * (1 - l_discount)) AS promo_revenue
6FROM part, lineitem
7WHERE l_partkey = p_partkey
8SETTINGS query_plan_join_swap_table='false';
   ┌──────promo_revenue─┐
1.16.650141208349083 │
   └────────────────────┘

1 row in set. Elapsed: 55.687 sec. Processed 620.04 million rows, 12.67 GB (11.13 million rows/s., 227.57 MB/s.)
Peak memory usage: 24.39 GiB.

接下来,我们使用设置为 auto(默认值)的新 query_plan_join_swap_table 设置运行相同的查询。现在,ClickHouse 将使用表大小的估计值来确定连接的哪一侧应该是构建表。因此,ClickHouse 首先将来自小得多的 part 表的数据加载到主内存中的哈希表中,然后再流式传输和连接来自 lineitem 表的数据

1SELECT 100.00 * sum(
2  CASE
3  WHEN p_type LIKE 'PROMO%'
4  THEN l_extendedprice * (1 - l_discount)
5  ELSE 0 END) / sum(l_extendedprice * (1 - l_discount)) AS promo_revenue
6FROM part, lineitem
7WHERE l_partkey = p_partkey
8SETTINGS query_plan_join_swap_table='auto';
   ┌──────promo_revenue─┐
1.16.650141208349083 │
   └────────────────────┘

1 row in set. Elapsed: 9.447 sec. Processed 620.04 million rows, 12.67 GB (65.63 million rows/s., 1.34 GB/s.)
Peak memory usage: 4.72 GiB.

如您所见,查询速度提高了 5 倍以上,内存使用量减少了 5 倍。

JOIN 表达式的优化

由 János Benjamin Antal 贡献

对于具有一系列由 OR 分隔的条件的连接,如本抽象示例中所示……

JOIN ... ON (a=b AND x) OR (a=b AND y) OR (a=b AND z)

…ClickHouse 每个条件使用哈希表(当使用 基于哈希表的连接算法之一时)。

减少哈希表数量并允许更好的谓词下推的一种方法是从上面示例 JOIN 的 ON 子句中提取公共表达式

JOIN ...ON a=b AND (x OR y OR z)

可以通过将新的 optimize_extract_common_expressions 设置设置为 1 来启用此行为。由于此设置目前是实验性的,因此默认值目前为 0

我们使用 TPC-H 表上的另一个查询(请参阅上一节,了解有关创建和加载表的说明,以及有关所用硬件的信息)来演示此新设置。

我们运行以下连接查询,该查询具有一系列由 OR 分隔的条件,并将 optimize_extract_common_expressions 设置为 0(禁用该设置)

1SELECT
2  sum(l_extendedprice * (1 - l_discount)) AS revenue
3FROM
4  lineitem, part
5WHERE
6(
7        p_partkey = l_partkey
8    AND p_brand = 'Brand#12'
9    AND p_container in ('SM CASE', 'SM BOX','SM PACK', 'SM PKG')
10    AND l_quantity >= 1 AND l_quantity <= 1 + 10
11    AND p_size BETWEEN 1 AND 5
12    AND l_shipmode in ('AIR', 'AIR REG')
13    AND l_shipinstruct = 'DELIVER IN PERSON'
14)
15OR
16(
17        p_partkey = l_partkey
18    AND p_brand = 'Brand#23'
19    AND p_container in ('MED BAG', 'MED BOX', 'MED PKG', 'MED PACK')
20    AND l_quantity >= 10 AND l_quantity <= 10 + 10
21    AND p_size BETWEEN 1 AND 10
22    AND l_shipmode in ('AIR', 'AIR REG')
23    AND l_shipinstruct = 'DELIVER IN PERSON'
24)
25OR
26(
27        p_partkey = l_partkey
28    AND p_brand = 'Brand#34'
29    AND p_container in ('LG CASE', 'LG BOX', 'LG PACK', 'LG PKG')
30    AND l_quantity >= 20 AND l_quantity <= 20 + 10
31    AND p_size BETWEEN 1 AND 15
32    AND l_shipmode in ('AIR', 'AIR REG')
33    AND l_shipinstruct = 'DELIVER IN PERSON'
34)
35SETTINGS optimize_extract_common_expressions = 0;

在我们的测试机器上,此查询在 30 分钟后进度为 3%...因此我们中止了查询,并运行了启用 optimize_extract_common_expressions 设置的相同查询

1SELECT
2  sum(l_extendedprice * (1 - l_discount)) AS revenue
3FROM
4  lineitem, part
5WHERE
6(
7        p_partkey = l_partkey
8    AND p_brand = 'Brand#12'
9    AND p_container in ('SM CASE', 'SM BOX','SM PACK', 'SM PKG')
10    AND l_quantity >= 1 AND l_quantity <= 1 + 10
11    AND p_size BETWEEN 1 AND 5
12    AND l_shipmode in ('AIR', 'AIR REG')
13    AND l_shipinstruct = 'DELIVER IN PERSON'
14)
15OR
16(
17        p_partkey = l_partkey
18    AND p_brand = 'Brand#23'
19    AND p_container in ('MED BAG', 'MED BOX', 'MED PKG', 'MED PACK')
20    AND l_quantity >= 10 AND l_quantity <= 10 + 10
21    AND p_size BETWEEN 1 AND 10
22    AND l_shipmode in ('AIR', 'AIR REG')
23    AND l_shipinstruct = 'DELIVER IN PERSON'
24)
25OR
26(
27        p_partkey = l_partkey
28    AND p_brand = 'Brand#34'
29    AND p_container in ('LG CASE', 'LG BOX', 'LG PACK', 'LG PKG')
30    AND l_quantity >= 20 AND l_quantity <= 20 + 10
31    AND p_size BETWEEN 1 AND 15
32    AND l_shipmode in ('AIR', 'AIR REG')
33    AND l_shipinstruct = 'DELIVER IN PERSON'
34)
35SETTINGS optimize_extract_common_expressions = 1;
   ┌───────revenue─┐
1.298937728.882-- 298.94 million
   └───────────────┘

1 row in set. Elapsed: 3.021 sec. Processed 620.04 million rows, 38.21 GB (205.24 million rows/s., 12.65 GB/s.)
Peak memory usage: 2.79 GiB.

现在,查询在 3 秒内返回了结果。

默认支持非等值 JOIN

由 Vladimir Cherkasov 贡献

自 24.05 版本以来,ClickHouse 对 JOIN 的 ON 子句中的非等值条件提供了实验性支持

-- Equi join
SELECT t1.*, t2.* FROM t1 JOIN t2 ON t1.key = t2.key;

-- Non-equi joins
SELECT t1.*, t2.* FROM t1 JOIN t2 ON t1.key != t2.key;
SELECT t1.*, t2.* FROM t1 JOIN t2 ON t1.key > t2.key

在此版本中,此支持不再是实验性的,并且默认启用。

请继续关注今年接下来的版本,这些版本将如 承诺的那样,带来更多 JOIN 改进!

分享这篇文章

订阅我们的新闻资讯

随时了解功能发布、产品路线图、支持和云产品!
正在加载表单...
关注我们
X imageSlack imageGitHub image
Telegram imageMeetup imageRss image