简介
在另一篇博文中,我们研究了 ClickHouse 与 Elasticsearch 在大规模数据分析和可观测性用例中常见的工作负载上的性能——对数十亿行表数据进行 count(*) 聚合。我们展示了 ClickHouse 在运行大型数据量聚合查询时,性能远超 Elasticsearch。具体来说:
ClickHouse 中的 Count(*) 聚合查询能够高效利用硬件,结果是,与 Elasticsearch 相比,聚合大型数据集的延迟至少降低 5 倍。这意味着,在延迟相当的情况下,ClickHouse 所需的硬件规模更小,成本降低 4 倍。
由于上述原因,我们看到越来越多的用户从 Elasticsearch 迁移到 ClickHouse,客户案例突出了以下优势:
- 在 PB 级可观测性用例中大幅降低成本
“从 Elasticsearch 迁移到 ClickHouse 后,我们的可观测性硬件成本降低了 30% 以上。”滴滴出行
- 突破数据分析应用的技术限制
“这释放了新功能、增长和更易扩展的潜力。”Contentsquare
- 大幅提升监控平台的可扩展性和查询延迟
“ClickHouse 帮助我们将每月处理的数据量从数百万行扩展到数十亿行。”
“切换之后,我们看到平均读取延迟提高了 100 倍。”The Guild
您可能会想,“为什么 ClickHouse 比 Elasticsearch 快这么多,效率也高这么多?” 这篇博客将为您提供关于这个问题的深入技术解答。
ClickHouse 和 Elasticsearch 中的 Count 聚合
数据分析场景中聚合的常见用例是计算和排序数据集中值的频率。例如,此屏幕截图中的所有数据可视化都来自 ClickPy 应用程序(分析了近 9000 亿行 Python 包下载事件),这些可视化都使用了 SQL GROUP BY
子句与 count(*)
聚合在底层结合使用:
同样,在 日志用例(或更广泛的 可观测性用例)中,聚合最常见的应用之一是统计特定日志消息或事件发生的频率(并在频率异常时发出警报)。
在 Elasticsearch 中,与 ClickHouse 的 SELECT count(*) FROM ... GROUP BY ...
SQL 查询等效的是 terms aggregation,它是 Elasticsearch 的一种 bucket aggregation。
ClickHouse 的 GROUP BY
与 count(*)
和 Elasticsearch 的 terms aggregation
在功能上大致等效,但它们在实现、性能和结果质量方面差异很大,如下所述。
我们在随附的博文中比较了 count 聚合的性能。
除了 bucket aggregations,Elasticsearch 还提供了 metric aggregations。我们将在另一篇博客中比较 ClickHouse 和 Elasticsearch 在 metric 用例中的表现。
Count 聚合方法
并行化
ClickHouse
ClickHouse 从一开始就被设计为尽可能快速和高效地过滤和聚合互联网规模的数据。为此,ClickHouse 在 ① 列值、② 表块和 ③ 表分片的级别上并行化 SELECT 查询,包括 count(*)
和所有其他 90 多个聚合函数:
① SIMD 并行化
ClickHouse 利用 CPU 的 SIMD 单元(例如 AVX512)将相同的操作应用于列中连续的值。这篇博文详细介绍了其工作原理。
② 多核并行化
在具有 n
个 CPU 内核的单台机器上,ClickHouse 使用 n
个并行执行通道(如果用户通过 max_threads 设置请求,也可以更少或更多)运行聚合查询:
上图显示了 ClickHouse 如何并行处理 n
个非重叠数据范围。这些数据范围可以是任意的,例如,它们不需要基于分组键。当聚合查询包含 WHERE
子句形式的过滤器,并且可以利用主索引来评估此过滤器时,ClickHouse 会定位匹配的表数据范围,并将这些范围动态地分布在 n
个执行通道之间。
这种并行化方法通过使用部分聚合状态来实现:n
个执行通道中的每一个都生成部分聚合状态。这些部分聚合状态最终合并为最终聚合结果。
对于 count(*)
聚合,部分聚合状态只是一个递增更新的计数变量。实际上,count(*)
聚合是最简单的聚合类型,即使没有部分聚合状态的概念,也可以在内部并行化。为了给出一个部分聚合状态实现并行化的具体示例,我们使用此聚合查询来计算基于英国房价数据集的每个城镇的平均房价
SELECT
town,
avg(price) AS avg_price
FROM uk_price_paid
GROUP BY town;
假设数据库想要使用两个并行执行通道计算 London
的平均房价:
执行通道 1 对其数据范围内的所有行(包含两条 London 记录)的房价求平均值。对于 avg
,生成的 partial aggregation state
通常包含
- 一个
sum
(此处为执行通道 1:500,000
,表示London
中汇总的房价)和 - 一个
count
(此处为执行通道 1:2
,表示为London
处理的记录数)。
执行通道 2 计算类似的部分聚合状态。这两个部分聚合状态合并后,即可生成最终结果:London
记录的平均房价为 (500,000 + 400,000) / (2 + 1)
= 900,000 / 3
= 300,000
。
部分聚合状态对于计算正确结果是必要的。简单地对子范围的平均值求平均值会产生不正确的结果。例如,如果我们对第一个子范围的平均值 (250,000
) 与第二个子范围的平均值 (400,000
) 求平均值,我们将得到 (650,000 / 2)
= 325,000
,这是不正确的。
ClickHouse 将其支持的所有 90 多个聚合函数及其与聚合函数组合器的组合,在所有可用的 CPU 内核上并行化。
③ 多节点并行化
如果聚合查询的源表是分片的并分布在多个节点上,那么 ClickHouse 会在所有可用节点的所有可用 CPU 内核上并行化聚合函数。
每个节点(并行)使用上述多核并行化技术在本地执行聚合。生成的部分聚合状态被流式传输到发起节点(最初接收聚合查询的节点)并由其合并。
作为一种优化,如果聚合查询的 GROUP BY 键是分片键的前缀,那么发起节点不需要合并部分聚合状态,合并作为最后一步在每个节点上发生,最终结果流式传输回发起节点。
增量聚合
到目前为止讨论的所有技术都应用于“查询时”,即当用户运行 SELECT count(*) FROM ... GROUP BY ...
查询时。如果相同的开销大的聚合查询重复运行(例如,每小时运行一次),或者如果需要亚秒级延迟,但查询的数据集太大而无法实现,那么 ClickHouse 提供了额外的优化,将负载从查询时转移到插入时和(主要是)后台合并时。具体来说,上述部分聚合技术(基于partial aggregation states
)也可以在 数据 part 级别上在(并行)后台 part 合并期间使用。
这产生了一种强大的、高度可扩展的连续数据汇总技术,结果是聚合查询将发现大部分数据已经聚合。下图概述了这一点:
① -State 聚合函数组合器可用于指示 ClickHouse 查询引擎仅组合部分聚合状态(由并行执行通道生成),而不是组合和计算最终结果
SELECT
town,
avgState(price) AS avg_price_state
FROM uk_price_paid
GROUP BY town;
当您运行此查询时,
avg_price_state
中的部分聚合状态实际上并不打算在屏幕上打印。
然后,可以将这些组合的部分聚合状态作为数据 part写入具有 AggregatingMergeTree 表引擎的表中
INSERT INTO <table with AggregatingMergeTree engine>
SELECT
town,
avgState(price) AS avg_price_state
FROM uk_price_paid
GROUP BY town;
② 此表引擎在后台 part 合并期间继续增量(和并行化)部分聚合。增量合并后的结果等同于对所有原始数据运行聚合查询。
③ 在查询时,可以使用 -Merge 聚合函数组合器将部分聚合状态组合成最终结果聚合值
SELECT
town,
avgMerge(avg_price_state) AS avg_price
FROM <table with AggregatingMergeTree engine>
GROUP BY town;
Elasticsearch
与 ClickHouse 相比,Elasticsearch 为其 terms aggregation(用于在 Elasticsearch 中计算计数)利用了完全不同的并行化方法。这导致硬件利用率不如 ClickHouse 高效:
terms aggregation
始终需要一个 size 参数(在下面称为 n
),并且 Elasticsearch 使用每个分片一个 CPU 线程运行此聚合,这与 CPU 内核数无关。每个线程从其处理的分片中计算分片本地的 top n
结果(基于每个分组的最大的 count
值,默认)。分片本地结果最终合并为全局 top n
最终结果。
Elasticsearch 的 terms aggregation
的多节点并行化工作方式类似。当分片分布在多个节点上时,每个节点(使用上述技术)生成节点本地的 top n
结果,这些节点本地结果由协调节点(最初接收聚合查询的节点)合并为最终全局结果。
基于数据在分片上的分布方式,这种并行化方法可能存在精度问题。我们用一个例子来说明这一点: 我们假设,基于英国房价数据集,我们想要计算售出房产最多的前两个城镇。数据分布在两个分片上。上图抽象地显示了 ① 每个城镇的房产销售记录的分片本地计数。每个处理线程 ② 返回一个分片本地的
top 2
结果,该结果 ③ 被合并为最终的全局 top 2
结果。但是,此结果是不正确的。基于分片全局数据,这是每个城镇的正确记录计数:
Town 1
:11
Town 2
:8
Town 3
:9
Town 4
:9
Elasticsearch 计算的 ③ 结果在 Town 2
的计数和排名方面都是错误的。可以通过分析返回的计数错误并调整 shard size 参数来提高精度 - 这指示每个处理线程从每个分片返回比查询请求的更大的 top n
结果,从而增加内存需求和运行时。
Elasticsearch 还利用基于 JVM 的自动向量化和 Java Panama Vector API 的 SIMD 硬件单元。此外,从 Elasticsearch 8.12 开始,查询处理线程可以并行搜索段,但
terms aggregation
除外。因为将上述技术应用于段而不是分片,也会出现相同的精度问题。由于一个分片由多个段组成,这也将使每个处理线程的工作量成倍增加,并增加shard size 参数
。
精度
Elasticsearch
如上文所述,当查询的数据分布在多个分片上时,Elasticsearch 的 terms aggregation 中的计数默认是近似值。可以通过分析返回的计数错误并调整 shard size 参数来提高结果的准确性,但这会增加运行时和内存需求。
ClickHouse
ClickHouse 中的 count(*)
聚合函数计算的结果完全准确。
ClickHouse count(*)
聚合易于使用,并且不需要像 Elasticsearch 中那样的额外配置。
(无)limit 子句
Elasticsearch
由于其执行模型,在 Elasticsearch 中,没有 limit 子句的 count 聚合是不可能的——用户必须始终指定 size 设置。即使使用较大的 size 值,对高基数数据集的 bucket aggregations 也受到 max_buckets 设置的限制,或者需要使用开销大的 composite aggregation 来分页浏览结果。
ClickHouse
ClickHouse count(*)
聚合不受 size 限制。如果查询内存消耗超过用户指定的(可选)最大内存阈值,它们还支持将临时结果溢出到磁盘。此外,与数据集大小无关,如果聚合中的分组列构成主键的前缀,ClickHouse 可以以最小的内存需求运行聚合。
同样,与 Elasticsearch 相比,ClickHouse count(*)
聚合的复杂度较低。
连续数据汇总方法
无论数据库中的聚合和查询处理多么高效,聚合数十亿或数万亿行数据(在现代数据分析用例中很常见)由于必须处理的数据量巨大,因此始终是内在昂贵的。
因此,专门用于分析工作负载的数据库通常提供数据汇总作为构建块,供用户自动将传入数据转换为汇总数据集,以 预聚合 且通常 显著缩小 的格式表示原始数据。查询将利用预计算的数据在交互式用例中提供亚秒级延迟,例如上述 ClickPy 应用程序。
Elasticsearch 和 ClickHouse 都为自动连续数据汇总提供了内置技术。它们的技术具有相同的功能,但在实现、效率以及由此产生的计算成本方面却截然不同。
Elasticsearch
Elasticsearch 提供了一种称为 transforms 的机制,用于批量转换现有索引为汇总索引,或持续转换摄取的数据。
根据 Elasticsearch 的 磁盘格式,我们在此处 详细 描述了 transforms 的工作原理。
我们注意到 Elasticsearch 方法的三个缺点,即
-
需要保留旧的原始数据:否则,transforms 无法正确地重新计算聚合。
-
可伸缩性差和计算成本高:每当在 检查点 之后检测到存储桶的新原始数据文档时,所有存储桶数据都会从不断增长的原始数据源索引中查询并重新聚合。这无法扩展到数十亿甚至万亿级文档集,并导致高昂的计算成本。
-
非实时:只有在下一个 检查间隔 之后,transforms 预聚合目标索引才会与原始数据源索引同步。
仅针对时间序列指标数据,Elasticsearch 还提供了一种 降采样 技术,通过以降低的粒度存储数据来减少该数据的占用空间。降采样是 rollups 的后继者,相当于一个 transforms,它按时间戳(转换为固定的时间间隔,例如小时、天、月或年)对指标数据文档进行分组,然后应用一组固定的聚合(
min
、max
、sum
、value_count
和average
)。与 ClickHouse 链式物化视图 的比较可能是未来博客的主题。
ClickHouse
ClickHouse 使用 物化视图 结合 AggregatingMergeTree 表引擎和 部分聚合状态,用于自动且(与 Elasticsearch 相比)增量式数据转换。
根据 ClickHouse 的 磁盘格式,我们在此处 详细 解释了增量式物化视图的机制。
ClickHouse 物化视图比 Elasticsearch transforms 具有三个主要优势
-
无原始数据依赖性:永远不会查询源表中的原始数据,即使当用户想要执行精确的聚合计算时也是如此。这允许对源表和预聚合目标表应用不同的 TTL 设置。此外,在仅应执行同一组聚合查询的场景中,用户可以选择在预聚合后完全放弃源数据(使用 Null 表引擎)。
-
高可伸缩性和低计算成本:增量聚合专门为原始数据源表包含数十亿或数万亿行数据的场景而设计。当该组的新原始数据行存在时,ClickHouse 不会重复查询不断增长的源表并从属于同一组的所有现有行重新计算聚合值,而是简单地从(仅)新插入的原始数据行的值计算 部分聚合状态。此状态在后台与先前计算的状态进行增量 合并。换句话说 - 每个原始数据值仅与其他原始数据值聚合一次。与对原始数据进行暴力聚合相比,这显著降低了计算成本。
-
实时:当成功确认插入到原始数据源表时,预聚合目标表保证是最新的。
回填预聚合
在我们随附的基准测试博客文章中,我们使用 Elasticsearch 中的连续 transforms 和 ClickHouse 中等效的物化视图来将摄取的数据动态预聚合到单独的数据集中。有时,这是不可行的。例如,当大部分数据已经摄取,重新摄取是不可能或成本太高,并且引入了将受益于以预聚合格式对这些数据运行的查询时。
Elasticsearch
我们在 Elasticsearch 中模拟了这种情况,方法是对已经摄取的 100 亿行数据集 运行 batch
transform,预先计算计数到单独的数据集中,以加速我们用作基准测试一部分的聚合查询。 continuous
和 batch
transforms 都使用上述基于检查点的相同机制。由于此机制的缺点,在检查点处会重复查询和聚合相同的值
通过 batch transform 回填花费了整整 5 天(使用大量的计算成本)。等待查询可以从预聚合数据中受益需要很长时间。
ClickHouse
在 ClickHouse 中,回填预聚合的工作方式是使用 INSERT INTO SELECT 语句直接插入到物化视图的目标表中,使用 视图的 SELECT 查询(转换)。
对于 100 亿行数据集,这需要 20 秒 而不是整整 5 天.
这 20 秒包括聚合完整的 100 亿行数据集并将结果(作为部分聚合状态)写入目标表,随后物化视图将使用该目标表来处理额外的传入数据。根据原始数据集的基数,这可能是一种内存密集型方法,因为完整的原始数据集是临时聚合的。或者,用户可以利用 变体,该变体需要最少的内存。
请注意,手动聚合原始数据集并将结果直接插入目标表的 20 秒 ClickHouse 方法
在 Elasticsearch 中是不可行的。理论上,这可以使用 reindex(索引到索引复制)操作来完成。但是,这将需要保留原始数据集中的 _source 数据(需要显著更多的存储空间)。它还需要一种手动创建必要检查点的机制,以便在流式传输数据开始时正确地继续预聚合过程。在 ClickHouse 中,在我们如上所述运行回填后,物化视图将继续对新的传入数据执行增量聚合过程。
ClickHouse 中的高性能聚合
包括 ClickHouse 在内的大多数数据库都使用哈希聚合算法的某种变体来实现 GROUP BY
,其中输入行的聚合值存储在 哈希表 中并使用分组列作为键进行更新。选择正确的哈希表类型对于性能至关重要。在底层,ClickHouse 利用复杂的 哈希表框架 来实现聚合。根据分组列的数据类型、估计的基数和其他因素,从 30 多种(截至 2024 年 4 月)不同的实现中为每个聚合查询单独选择最快的哈希表。ClickHouse 专门为对海量数据进行高性能聚合而构建。
ClickHouse 是当今市场上 最快 的数据库之一,具有用于数据分析的独特功能
-
现代 SQL 方言和 丰富的数据类型,包括映射和数组(以及 80 多个用于 处理 数组的函数),用于优雅而简单地建模和 解决 各种问题
-
超过 1000 个用于数学、地理、机器学习、时间序列等领域的常规数据处理 函数
-
完全 并行化的窗口函数
总结
在这篇博客文章中,我们对 ClickHouse 在处理计数聚合方面比 Elasticsearch 更快、更高效 的原因提供了深入的技术解答,计数聚合在数据分析和日志/可观测性用例中很常见。
我们解释了 ClickHouse 和 Elasticsearch 中计数聚合的并行化方法以及结果质量和可用性复杂性的差异。我们探讨了 Elasticsearch 和 ClickHouse 用于预计算计数的内置机制。我们强调了为什么 ClickHouse 物化视图比 Elasticsearch transforms 更高效,更适合处理数十亿/万亿级行数据集。
我们建议阅读我们随附的博客文章 ClickHouse 与 Elasticsearch:十亿行数据对决,以了解 ClickHouse 高性能聚合的实际应用。作为预告,我们在此处包含了一些基准测试结果