欢迎阅读我们 2024 年的第一篇发布文章,尽管它实际上是关于 2023 年底发布的版本!ClickHouse 23.12 版本包含 21 个新功能、18 项性能优化和 37 个错误修复。
我们将在本文中介绍一小部分新功能,但此版本还包括以下功能:ORDER BY ALL
、从数字生成短唯一标识符 (SQID)、使用基于傅里叶变换的新函数 seriesPeriodDetectFFT 查找信号频率、支持 SHA-512/256、别名列上的索引、通过 APPLY DELETED MASK
在轻量级删除操作后清理已删除记录、降低哈希连接的内存使用量以及加快合并表的计数速度。
在集成方面,我们还改进了 ClickHouse 的 PowerBI、Metabase、dbt、Apache Beam 和 Kafka 连接器。
新增贡献者
一如既往,我们向所有 23.12 版本的新贡献者 表示热烈的欢迎!ClickHouse 的受欢迎程度在很大程度上归功于社区的贡献。看到社区不断壮大,我们感到非常欣慰。
以下是新增贡献者的姓名列表:
Andrei Fedotov、Chen Lixiang、Gagan Goel、James Nock、Natalya Chizhonkova、Ryan Jacobs、Sergey Suvorov、Shani Elharrar、Zhuo Qiu、andrewzolotukhin、hdhoang 和 skyoct。
如果您在这里看到自己的名字,请与我们联系……但我们也会在 Twitter 等平台上找到您。
您还可以查看 演示文稿的幻灯片。
可刷新物化视图
贡献者:Michael Kolupaev、Michael Guzov
ClickHouse 新用户经常会探索物化视图来解决各种数据和查询问题,从加速聚合查询到在插入时进行数据转换任务。此时,这些用户也常常遇到一个常见的困惑来源——他们期望 ClickHouse 中的物化视图与他们在其他数据库中使用的物化视图类似,而它们实际上只是一种在插入新行时执行的查询触发器!更准确地说,当行作为块插入 ClickHouse 时(通常至少包含 1000 行),物化视图定义的查询将在该块上执行,并将结果存储在不同的目标表中。我们同事 Mark 在最近的一段视频中简洁地描述了此过程
此功能非常强大,并且与 ClickHouse 中的大多数功能一样,经过精心设计以实现扩展性,并随着新数据的插入增量更新视图。但是,在某些用例中,不需要或不适用此增量过程。某些问题要么与增量方法不兼容,要么不需要实时更新,定期重建更合适。例如,您可能希望定期对完整数据集上的视图进行完整重新计算,因为它使用复杂的连接,这与增量方法不兼容。
在 23.12 版本中,我们很高兴地宣布添加可刷新物化视图作为一项实验性功能,以解决这些用例!除了允许视图包含定期执行的查询并将结果设置为目标表之外,此功能还可用于在 ClickHouse 中执行 cron 任务,例如,定期导出到或从外部数据源导出。
此重要功能值得单独撰写一篇博文(敬请期待!),尤其是在它可以解决的问题数量方面。
例如,为了介绍语法,让我们考虑一个可能难以使用传统的增量物化视图甚至经典视图来解决的问题。
考虑我们用于 与 dbt 集成 的示例。它包含一个小型 IMDB 数据集,具有以下关系模式。此数据集源自 关系数据集存储库。
假设您已在 ClickHouse 中创建并填充了这些表,如 我们的文档 中所述,可以使用以下查询计算每个演员的摘要,并按电影出现次数排序。
SELECT
id,
any(actor_name) AS name,
uniqExact(movie_id) AS num_movies,
avg(rank) AS avg_rank,
uniqExact(genre) AS unique_genres,
uniqExact(director_name) AS uniq_directors,
max(created_at) AS updated_at
FROM
(
SELECT
imdb.actors.id AS id,
concat(imdb.actors.first_name, ' ', imdb.actors.last_name) AS actor_name,
imdb.movies.id AS movie_id,
imdb.movies.rank AS rank,
genre,
concat(imdb.directors.first_name, ' ', imdb.directors.last_name) AS director_name,
created_at
FROM imdb.actors
INNER JOIN imdb.roles ON imdb.roles.actor_id = imdb.actors.id
LEFT JOIN imdb.movies ON imdb.movies.id = imdb.roles.movie_id
LEFT JOIN imdb.genres ON imdb.genres.movie_id = imdb.movies.id
LEFT JOIN imdb.movie_directors ON imdb.movie_directors.movie_id = imdb.movies.id
LEFT JOIN imdb.directors ON imdb.directors.id = imdb.movie_directors.director_id
)
GROUP BY id
ORDER BY num_movies DESC
LIMIT 5
┌─────id─┬─name─────────┬─num_movies─┬───────────avg_rank─┬─unique_genres─┬─uniq_directors─┬──────────updated_at─┐
│ 45332 │ Mel Blanc │ 909 │ 5.7884792542982515 │ 19 │ 148 │ 2024-01-08 15:44:31 │
│ 621468 │ Bess Flowers │ 672 │ 5.540605094212635 │ 20 │ 301 │ 2024-01-08 15:44:31 │
│ 283127 │ Tom London │ 549 │ 2.8057034230202023 │ 18 │ 208 │ 2024-01-08 15:44:31 │
│ 41669 │ Adoor Bhasi │ 544 │ 0 │ 4 │ 121 │ 2024-01-08 15:44:31 │
│ 89951 │ Edmund Cobb │ 544 │ 2.72430730046193 │ 17 │ 203 │ 2024-01-08 15:44:31 │
└────────┴──────────────┴────────────┴────────────────────┴───────────────┴────────────────┴─────────────────────┘
5 rows in set. Elapsed: 1.207 sec. Processed 5.49 million rows, 88.27 MB (4.55 million rows/s., 73.10 MB/s.)
Peak memory usage: 1.44 GiB.
诚然,这不是最慢的查询,但让我们假设用户需要此查询更快且计算成本更低才能用于应用程序。假设此数据集也受到持续更新的影响——电影不断发布,新的演员和导演也随之出现。
此处的普通视图无济于事,并且将其转换为增量物化视图将具有挑战性:只有连接左侧表上的更改将被反映出来,这需要多个链式视图并且复杂性显著。
使用 23.12 版本,我们可以创建一个可刷新物化视图,该视图将定期运行上述查询并以原子方式替换目标表中的结果。虽然它的更新不会像增量视图那样实时,但对于不太可能频繁更新的数据集来说,它可能已经足够了。
让我们首先为结果创建目标表
CREATE TABLE imdb.actor_summary
(
`id` UInt32,
`name` String,
`num_movies` UInt16,
`avg_rank` Float32,
`unique_genres` UInt16,
`uniq_directors` UInt16,
`updated_at` DateTime
)
ENGINE = MergeTree
ORDER BY num_movies
创建可刷新物化视图使用与增量相同的语法,只是我们引入了一个 REFRESH
子句,指定应执行查询的周期。请注意,我们删除了查询的限制以存储完整的结果。此视图类型对 SELECT
子句没有任何限制。
//enable experimental feature
SET allow_experimental_refreshable_materialized_view = 1
CREATE MATERIALIZED VIEW imdb.actor_summary_mv
REFRESH EVERY 1 MINUTE TO imdb.actor_summary AS
SELECT
id,
any(actor_name) AS name,
uniqExact(movie_id) AS num_movies,
avg(rank) AS avg_rank,
uniqExact(genre) AS unique_genres,
uniqExact(director_name) AS uniq_directors,
max(created_at) AS updated_at
FROM
(
SELECT
imdb.actors.id AS id,
concat(imdb.actors.first_name, ' ', imdb.actors.last_name) AS actor_name,
imdb.movies.id AS movie_id,
imdb.movies.rank AS rank,
genre,
concat(imdb.directors.first_name, ' ', imdb.directors.last_name) AS director_name,
created_at
FROM imdb.actors
INNER JOIN imdb.roles ON imdb.roles.actor_id = imdb.actors.id
LEFT JOIN imdb.movies ON imdb.movies.id = imdb.roles.movie_id
LEFT JOIN imdb.genres ON imdb.genres.movie_id = imdb.movies.id
LEFT JOIN imdb.movie_directors ON imdb.movie_directors.movie_id = imdb.movies.id
LEFT JOIN imdb.directors ON imdb.directors.id = imdb.movie_directors.director_id
)
GROUP BY id
ORDER BY num_movies DESC
该视图将立即执行,并在之后每分钟执行一次(根据配置),以确保反映源表的更新。我们之前获取演员摘要的查询在语法上变得更简单,速度也更快!
SELECT *
FROM imdb.actor_summary
ORDER BY num_movies DESC
LIMIT 5
┌─────id─┬─name─────────┬─num_movies─┬──avg_rank─┬─unique_genres─┬─uniq_directors─┬──────────updated_at─┐
│ 45332 │ Mel Blanc │ 909 │ 5.7884793 │ 19 │ 148 │ 2024-01-09 10:12:57 │
│ 621468 │ Bess Flowers │ 672 │ 5.540605 │ 20 │ 301 │ 2024-01-09 10:12:57 │
│ 283127 │ Tom London │ 549 │ 2.8057034 │ 18 │ 208 │ 2024-01-09 10:12:57 │
│ 356804 │ Bud Osborne │ 544 │ 1.9575342 │ 16 │ 157 │ 2024-01-09 10:12:57 │
│ 41669 │ Adoor Bhasi │ 544 │ 0 │ 4 │ 121 │ 2024-01-09 10:12:57 │
└────────┴──────────────┴────────────┴───────────┴───────────────┴────────────────┴─────────────────────┘
5 rows in set. Elapsed: 0.003 sec. Processed 6.71 thousand rows, 275.62 KB (2.30 million rows/s., 94.35 MB/s.)
Peak memory usage: 1.19 MiB.
假设我们将一个新演员“Clicky McClickHouse”添加到我们的源数据中,他碰巧出演了很多电影!
INSERT INTO imdb.actors VALUES (845466, 'Clicky', 'McClickHouse', 'M');
INSERT INTO imdb.roles SELECT
845466 AS actor_id,
id AS movie_id,
'Himself' AS role,
now() AS created_at
FROM imdb.movies
LIMIT 10000, 910
0 rows in set. Elapsed: 0.006 sec. Processed 10.91 thousand rows, 43.64 KB (1.84 million rows/s., 7.36 MB/s.)
Peak memory usage: 231.79 KiB.
不到 60 秒后,我们的目标表更新以反映 Clicky 丰富的表演经历
SELECT *
FROM imdb.actor_summary
ORDER BY num_movies DESC
LIMIT 5
┌─────id─┬─name────────────────┬─num_movies─┬──avg_rank─┬unique_genres─┬─uniq_directors─┬──────────updated_at─┐
│ 845466 │ Clicky McClickHouse │ 910 │ 1.4687939 │ 21 │ 662 │ 2024-01-09 10:45:04 │
│ 45332 │ Mel Blanc │ 909 │ 5.7884793 │ 19 │ 148 │ 2024-01-09 10:12:57 │
│ 621468 │ Bess Flowers │ 672 │ 5.540605 │ 20 │ 301 │ 2024-01-09 10:12:57 │
│ 283127 │ Tom London │ 549 │ 2.8057034 │ 18 │ 208 │ 2024-01-09 10:12:57 │
│ 356804 │ Bud Osborne │ 544 │ 1.9575342 │ 16 │ 157 │ 2024-01-09 10:12:57 │
└────────┴─────────────────────┴────────────┴───────────┴──────────────┴────────────────┴─────────────────────┘
5 rows in set. Elapsed: 0.003 sec. Processed 6.71 thousand rows, 275.66 KB (2.20 million rows/s., 90.31 MB/s.)
Peak memory usage: 1.19 MiB.
此示例代表了可刷新物化视图的一个简单应用。此功能具有潜在的更广泛的应用。查询执行的周期性意味着它可以潜在地用于定期导入或导出到外部数据源。此外,这些视图可以与 DEPENDS
子句链接以在视图之间创建依赖关系,从而允许构建复杂的流程。有关更多详细信息,请参阅 CREATE VIEW 文档。
我们很想知道您如何利用此功能以及它现在可以帮助您更有效地解决哪些问题!
FINAL 的优化
贡献者:Maksim Kita
自动增量式后台数据转换是 ClickHouse 中一个重要的概念,它允许在扩展时维持高数据摄取率,同时在后台合并数据块时持续应用特定于表引擎的数据修改。例如,ReplacingMergeTree 引擎仅保留基于行排序键列值和其包含数据块的创建时间在合并数据块时最近插入的行版本。AggregatingMergeTree 引擎在合并数据块期间将具有相同排序键值的行折叠成聚合行。
只要表的某个数据块存在多个部分,表数据就只处于中间状态,即对于 ReplacingMergeTree 表可能存在过时的行,并且对于 AggregatingMergeTree 表可能并非所有行都已聚合。在持续数据摄取的场景(例如实时流式场景)中,表存在多个部分的情况几乎总是存在的。幸运的是,ClickHouse 为您提供了解决方案:ClickHouse 提供了FINAL 作为 SELECT 查询的FROM 子句的修饰符(例如 SELECT ... FROM table FINAL
),它在查询时动态应用缺失的数据转换。虽然这很方便,并且使查询结果与后台合并的进度分离,但 FINAL 也可能减慢查询速度并增加内存消耗。
在 ClickHouse 20.5 版本之前,带有 FINAL 的 SELECT 以单线程方式执行:所选数据由单个线程按物理顺序(基于表的排序键)读取并合并和转换。
ClickHouse 20.5 引入了 带有 FINAL 的 SELECT 的并行处理:所有选定的数据都将拆分为具有不同排序键范围的组,并由多个线程并发处理(读取、合并和转换)。
ClickHouse 23.12 更进一步,根据排序键值将匹配查询 WHERE 子句的表数据划分为不相交和相交范围。所有不相交的数据范围都像查询中没有使用 FINAL 修饰符一样并行处理。这仅留下相交的数据范围,对于这些范围,表引擎的合并逻辑将使用 ClickHouse 20.5 引入的并行处理方法应用。
此外,对于 FINAL 查询,如果表的 partition 键是表排序键的前缀,ClickHouse 将不再尝试跨不同 partition 合并数据。
下图描绘了 SELECT 查询使用 FINAL 的新处理逻辑
为了并行化数据处理,查询被转换为一个查询管道 - 查询的物理操作符计划,由多个独立的执行通道组成,这些通道并发地流式传输、过滤、聚合和排序所选表数据的互斥范围。独立执行通道的数量取决于max_threads 设置,默认情况下设置为可用 CPU 内核的数量。在我们上面的示例中,运行查询的 ClickHouse 服务器有 8 个 CPU 内核。
因为查询使用了 FINAL 修饰符,ClickHouse 在规划阶段使用表的主索引,在创建物理操作符计划时。
首先,识别并拆分数据块中匹配查询 WHERE 子句的所有数据范围,基于表的排序键将其拆分为不相交和相交范围。不相交范围是仅存在于单个数据块中的数据区域,不需要转换。相反,相交范围中的行可能(基于排序键值)存在于多个数据块中,需要特殊处理。此外,在我们上面的示例中,查询计划程序可以将选定的相交范围拆分为两组(图中以蓝色和绿色标记),每组都有一个不同的排序键范围。使用创建的查询管道,所有匹配的不相交数据范围(图中以黄色标记)都像往常一样并发处理(就像查询根本没有 FINAL 子句一样),通过将它们的处理均匀地分布到一些可用的执行通道中。来自选定的相交数据范围的数据 - 按组 - 按顺序流式传输,并在数据按常规处理之前应用特定于表引擎的合并逻辑。
请注意,当具有相同排序键列值的行的数量较低时,查询性能将与不使用 FINAL 时大致相同。我们通过一个具体的例子来演示这一点。为此,我们稍微修改了来自英国房产价格示例数据集的表,并假设该表存储有关当前房产报价的数据,而不是之前出售的房产数据。我们使用 ReplacingMergeTree 表引擎,允许我们通过简单地插入具有相同排序键值的新行来更新报价房产的价格和其他特性。
CREATE TABLE uk_property_offers
(
postcode1 LowCardinality(String),
postcode2 LowCardinality(String),
street LowCardinality(String),
addr1 String,
addr2 String,
price UInt32,
…
)
ENGINE = ReplacingMergeTree
ORDER BY (postcode1, postcode2, street, addr1, addr2);
接下来,我们插入约 1500 万行到表中。
我们在 ClickHouse 23.11 版本上运行一个没有 FINAL 修饰符的典型分析查询,选择三个最昂贵的邮政编码。
SELECT
postcode1,
formatReadableQuantity(avg(price))
FROM uk_property_offers
GROUP BY postcode1
ORDER BY avg(price) DESC
LIMIT 3
┌─postcode1─┬─formatReadableQuantity(avg(price))─┐
│ W1A │ 163.58 million │
│ NG90 │ 68.59 million │
│ CF99 │ 47.00 million │
└───────────┴────────────────────────────────────┘
3 rows in set. Elapsed: 0.037 sec. Processed 15.52 million rows, 91.36 MB (418.58 million rows/s., 2.46 GB/s.)
Peak memory usage: 881.08 KiB.
我们在 ClickHouse 23.11 版本上运行相同的查询,并使用 FINAL。
SELECT
postcode1,
formatReadableQuantity(avg(price))
FROM uk_property_offers FINAL
GROUP BY postcode1
ORDER BY avg(price) DESC
LIMIT 3;
┌─postcode1─┬─formatReadableQuantity(avg(price))─┐
│ W1A │ 163.58 million │
│ NG90 │ 68.59 million │
│ CF99 │ 47.00 million │
└───────────┴────────────────────────────────────┘
3 rows in set. Elapsed: 0.299 sec. Processed 15.59 million rows, 506.68 MB (57.19 million rows/s., 1.86 GB/s.)
Peak memory usage: 120.81 MiB.
请注意,使用 FINAL 的查询运行速度慢了约 10 倍,并且使用了更多的主内存。
我们在 ClickHouse 23.12 上运行带有 FINAL 修饰符的查询。
SELECT
postcode1,
formatReadableQuantity(avg(price))
FROM uk_property_offers FINAL
GROUP BY postcode1
ORDER BY avg(price) DESC
LIMIT 3;
┌─postcode1─┬─formatReadableQuantity(avg(price))─┐
│ W1A │ 163.58 million │
│ NG90 │ 68.59 million │
│ CF99 │ 47.00 million │
└───────────┴────────────────────────────────────┘
3 rows in set. Elapsed: 0.036 sec. Processed 15.52 million rows, 91.36 MB (434.42 million rows/s., 2.56 GB/s.)
Peak memory usage: 1.62 MiB.
对于我们示例数据在 23.12 上,无论是否使用 FINAL 修饰符,查询运行时间和内存使用量都保持大致相同! :)
向量化改进
在 23.12 中,由于使用 SIMD 指令增加了向量化,因此一些常用查询得到了显着改进。
更快的 min/max
由 Raúl Marín 贡献
由于允许这些函数使用 SIMD 指令进行向量化的更改,min 和 max 函数的速度得到了提高。当查询受 CPU 限制且不受 I/O 或内存带宽限制时,这些更改应该会提高查询性能。虽然这些情况可能很少见,但改进可能是显著的。考虑以下相当人为的示例,其中我们从 10 亿个整数中计算最大数。以下是在支持 Intel AVX 指令的 Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz 上执行的。
在 23.11 中
SELECT max(number)
FROM
(
SELECT *
FROM system.numbers
LIMIT 1000000000
)
┌─max(number)─┐
│ 999999999 │
└─────────────┘
1 row in set. Elapsed: 1.102 sec. Processed 1.00 billion rows, 8.00 GB (907.50 million rows/s., 7.26 GB/s.)
Peak memory usage: 65.55 KiB.
现在是 23.12
┌─max(number)─┐
│ 999999999 │
└─────────────┘
1 row in set. Elapsed: 0.482 sec. Processed 1.00 billion rows, 8.00 GB (2.07 billion rows/s., 16.59 GB/s.)
Peak memory usage: 62.59 KiB.
对于更真实的示例,请考虑以下NOAA 天气数据集,其中包含超过 10 亿行。下面我们计算有史以来记录的最高温度。
在 23.11 中
SELECT max(tempMax) / 10
FROM noaa
┌─divide(max(tempMax), 10)─┐
│ 56.7 │
└──────────────────────────┘
1 row in set. Elapsed: 0.428 sec. Processed 1.08 billion rows, 3.96 GB (2.52 billion rows/s., 9.26 GB/s.)
Peak memory usage: 873.76 KiB.
虽然 23.12 中的改进不像我们之前的人为示例那样显著,但我们仍然获得了 25% 的加速!
┌─divide(max(tempMax), 10)─┐
│ 56.7 │
└──────────────────────────┘
1 row in set. Elapsed: 0.347 sec. Processed 1.08 billion rows, 3.96 GB (3.11 billion rows/s., 11.42 GB/s.)
Peak memory usage: 847.91 KiB.
更快的聚合
由 Anton Popov 贡献
由于针对跨越块的相同键的情况进行了优化,聚合也变得更快了。ClickHouse按块处理数据。在聚合处理期间,ClickHouse 使用哈希表来存储新值或更新已有的聚合值,这些值对应于处理的行块中每一行的分组键值。分组键值用于确定聚合值在哈希表中的位置。当处理的块中所有行都具有相同的唯一分组键时,ClickHouse 只需要确定一次聚合值的位置,然后在该位置批量更新值,这可以很好地进行向量化。
让我们在 Apple M2 Max 上试一试,看看效果如何。
SELECT number DIV 100000 AS k,
avg(number) AS avg,
max(number) as max,
min(number) as min
FROM numbers_mt(1000000000)
GROUP BY k
ORDER BY k
LIMIT 10;
在 23.11 中
┌─k─┬──────avg─┬────max─┬────min─┐
│ 0 │ 49999.5 │ 99999 │ 0 │
│ 1 │ 149999.5 │ 199999 │ 100000 │
│ 2 │ 249999.5 │ 299999 │ 200000 │
│ 3 │ 349999.5 │ 399999 │ 300000 │
│ 4 │ 449999.5 │ 499999 │ 400000 │
│ 5 │ 549999.5 │ 599999 │ 500000 │
│ 6 │ 649999.5 │ 699999 │ 600000 │
│ 7 │ 749999.5 │ 799999 │ 700000 │
│ 8 │ 849999.5 │ 899999 │ 800000 │
│ 9 │ 949999.5 │ 999999 │ 900000 │
└───┴──────────┴────────┴────────┘
10 rows in set. Elapsed: 1.050 sec. Processed 908.92 million rows, 7.27 GB (865.66 million rows/s., 6.93 GB/s.)
在 23.12 中
┌─k─┬──────avg─┬────max─┬────min─┐
│ 0 │ 49999.5 │ 99999 │ 0 │
│ 1 │ 149999.5 │ 199999 │ 100000 │
│ 2 │ 249999.5 │ 299999 │ 200000 │
│ 3 │ 349999.5 │ 399999 │ 300000 │
│ 4 │ 449999.5 │ 499999 │ 400000 │
│ 5 │ 549999.5 │ 599999 │ 500000 │
│ 6 │ 649999.5 │ 699999 │ 600000 │
│ 7 │ 749999.5 │ 799999 │ 700000 │
│ 8 │ 849999.5 │ 899999 │ 800000 │
│ 9 │ 949999.5 │ 999999 │ 900000 │
└───┴──────────┴────────┴────────┘
10 rows in set. Elapsed: 0.649 sec. Processed 966.48 million rows, 7.73 GB (1.49 billion rows/s., 11.91 GB/s.)
PASTE JOIN
由 Yarik Briukhovetskyi 贡献
PASTE JOIN 用于连接多个数据集,其中每个数据集中的等效行引用同一项。即第一个数据集中的第 n 行应与第二个数据集中的第 n 行连接。然后,我们可以通过行号而不是指定连接键来连接数据集。
让我们尝试使用来自 Hugging Face 上GLUE 基准的 Quora Question Pairs2 数据集。我们将训练 Parquet 文件拆分为两个
questions.parquet,其中包含 question1、question2 和 idx labels.parquet,其中包含 label 和 idx
然后,我们可以使用 PASTE JOIN 将列连接在一起。
INSERT INTO FUNCTION file('/tmp/qn_labels.parquet') SELECT *
FROM
(
SELECT *
FROM `questions.parquet`
ORDER BY idx ASC
) AS qn
PASTE JOIN
(
SELECT *
FROM `labels.parquet`
ORDER BY idx ASC
) AS lab
Ok.
0 rows in set. Elapsed: 0.221 sec. Processed 727.69 thousand rows, 34.89 MB (3.30 million rows/s., 158.15 MB/s.)
Peak memory usage: 140.47 MiB.