又一个月过去了,这意味着又到了发布新版本的时候了!
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 │ 698263 │
2. │ user │ 39640 │
└────────────────┴────────┘
等式和 IN 运算符现在也接受未知值。例如,以下查询返回类型为 Foo
或 public
的任何记录
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 路径的值
上图概述了 ClickHouse 如何将任何插入的 JSON 键路径存储(和读取)为原生子列,从而实现高数据压缩并保持在经典类型上看到的查询性能。
此版本现在支持使用 JSON 子列作为表的主键列
CREATE TABLE T
(
data JSON()
)
ORDER BY (data.a, data.b);
这意味着,摄取的 JSON 文档(每个 表分区)在磁盘上按用作主键列的 JSON 子列 排序 存储。此外,ClickHouse 将为 主索引文件创建 加速 筛选主键列的查询
此外,使用 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 的 最快 连接算法(例如其新的默认算法 并行哈希连接)基于内存哈希表,其工作原理是:① 首先将连接查询右侧表中的数据加载到哈希表中(这也称为构建阶段),② 然后流式传输左侧表中的数据,并通过在哈希表中查找进行连接(这称为扫描阶段)
请注意,由于 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 改进!