​深度介绍 ClickHouse Joins 的底层知识(ClickHouse 中对 Join 类型的支持 - Part 2)

图片

图片

在我们之前的帖子中,我们回顾了ClickHouse中可用的SQL JOIN类型。作为提醒:ClickHouse具有完整的SQL Join支持。

在本篇文章中,我们将开始探索ClickHouse中join执行的内部机制,以便您可以针对应用程序使用的查询优化join操作。在这里,您将看到ClickHouse如何将这些经典的join算法整合到Query pipeline中,以尽快执行join操作。

查询流水线

ClickHouse的设计是高效快速的。ClickHouse中的查询以高度并行的方式进行处理,利用当前服务器上的所有必要资源,并在许多情况下利用硬件达到其理论极限。服务器拥有的CPU核心数和主内存越多,查询的并行执行将获得更高的性能收益。

Query pipeline(查询流水线)决定了每个查询执行阶段的并行级别。

下图展示了在具有4个CPU核心的服务器上,ClickHouse的Query pipeline如何处理查询:

图片

查询的表数据会动态地分布在4个独立且并行的流式阶段中,这些阶段按数据块的方式将数据流式传输到ClickHouse中。由于服务器具有4个CPU核心,大多数Query Pipeline中的查询处理阶段会由4个线程并行执行。

所使用的线程数量取决于max_threads设置,其默认值设置为ClickHouse在运行机器上看到的CPU核心数。

对于所有查询,包括联接操作,Query Pipeline确保表数据以高度并行和可扩展的方式进行处理。

联结聚合的底层原理

为了确保资源的最大利用,ClickHouse开发了6种不同的join算法。这些算法决定了join查询的计划和执行方式。ClickHouse可以根据资源的可用性和使用情况,在运行时自适应选择最佳的算法,并进行动态更改。然而,ClickHouse也允许用户自行指定所需的join算法。以下图表根据相对内存消耗和执行时间,对这些算法进行了概述:

图片

在本博客文章中,我们将详细描述并比较上图中基于内存哈希表的三种ClickHouse联接算法。这些算法在ClickHouse中提供了高效的联接处理:

  • Hash join 哈希联结

  • Parallel hash join 并行哈希联结

  • Grace hash join 优雅哈希联结

在本文中,我们将探讨哈希联接算法的快速性和通用性。并行哈希联接算法在右侧表格较大时可能更快,但需要更多的内存。哈希联接和并行哈希联接都是内存受限的。而优雅哈希联接是一种非内存受限版本,它会将数据临时溢出到磁盘。Grace哈希联接不需要对数据进行排序,因此克服了其他将数据溢出到磁盘的联接算法(partial)的一些性能挑战(我们将在第二部分中介绍)。

在下一篇文章中,我们将介绍上图中基于外部排序的两种算法:

  • Full sorting merge join

  • Partial merge join

我们将最好的保留到最后,并将在另一篇文章中结束我们对ClickHouse联接算法的探索,我们将描述上图中ClickHouse最快的联接算法:

  • Direct join

测试数据和资源

对于所有示例查询,我们将使用前一篇文章中介绍的标准化IMDB数据集中的两个表:

图片

为了进行充分的测试,我们在一个名为imdb_large的新数据库中生成了这些表的大规模版本。

以下查询列出了示例表中的行数和未压缩数据量:

SELECT
    table,
    formatReadableQuantity(sum(rows)) AS rows,
    formatReadableSize(sum(data_uncompressed_bytes)) AS data_uncompressed
FROM system.parts
WHERE (database = 'imdb_large') AND active
GROUP BY table
ORDER BY table ASC;

┌─table──┬─rows───────────┬─data_uncompressed─┐
│ actors │ 1.00 million   │ 21.81 MiB         │
│ roles  │ 100.00 million │ 2.63 GiB          │
└────────┴────────────────┴───────────────────┘

为了使可视化结果简洁易读,我们人为地限制了ClickHouse查询流水线中使用的并行级别,通过设置 max_threads = 2 来实现。

然而,对于所有示例查询运行,我们使用默认设置的 max_threads 。如上所述,默认情况下, max_threads 设置为ClickHouse在运行的机器上看到的CPU核心数。这些示例使用的是ClickHouse Cloud服务,其中一个节点具有30个CPU Core。

SELECT getSetting('max_threads');

┌─getSetting('max_threads')─┐
│                        30 │
└───────────────────────────┘

现在让我们开始探索ClickHouse联接算法。我们首先从最通用的哈希联接算法开始。

哈希联结

描述

一个内存中的哈希表可以每秒处理250百万个完全随机的请求(如果它适合CPU缓存,甚至可以处理超过十亿个请求)。这种非常快速的查找能力使得内存中的哈希表成为ClickHouse中实现联接的自然选择,特别是在无法或不适合利用表排序的情况下。

哈希联接算法是ClickHouse中可用联接实现中最通用的算法。我们在下面展示了将哈希联接算法整合到ClickHouse查询流水线中的示意图:

图片

可以看到:

① 右侧表的所有数据被流式传输(由于max_threads = 2,以2个线程并行),然后ClickHouse将这些数据填充到内存中的哈希表中。

② 左侧表的数据被流式传输(由于max_threads = 2,以2个线程并行),并且 ③ 通过在哈希表中进行查找来与右侧表进行联接。

请注意,由于ClickHouse将右侧表格并创建一个哈希表放在RAM中,将较小的表格放在联接的右侧会更节省内存。我们将在下面进行演示。

还请注意,哈希表是ClickHouse中的一个关键数据结构。根据每个特定的查询和联接查询的具体情况,基于联接键列类型和联接严格性,ClickHouse会自动选择30多种变量之一。

支持的联结类型

ClickHouse支持所有联接类型和严格度设置。此外,目前只有哈希联接支持在ON子句中使用多个联接键,并且这些键使用OR进行组合。

对于希望深入了解的读者,源代码中包含了对哈希联接算法如何实现这些类型和设置的非常详细的描述。您可以查阅源代码以获取更深入的了解。

示例

我们将通过两次查询运行演示哈希联结算法。

右侧较小的表:

SELECT *
FROM roles AS r
JOIN actors AS a ON r.actor_id = a.id
FORMAT `Null`
SETTINGS join_algorithm = 'hash';

0 rows in set. Elapsed: 0.817 sec. Processed 101.00 million rows, 3.67 GB (123.57 million rows/s., 4.49 GB/s.)

右侧较大的表:

SELECT *
FROM actors AS a
JOIN roles AS r ON a.id = r.actor_id
FORMAT `Null`
SETTINGS join_algorithm = 'hash';

0 rows in set. Elapsed: 5.063 sec. Processed 101.00 million rows, 3.67 GB (19.95 million rows/s., 724.03 MB/s.)

我们可以查询query_log系统表,以查看最后两次查询运行的运行时统计信息:
 

SELECT
    query,
    formatReadableTimeDelta(query_duration_ms / 1000) AS query_duration,
    formatReadableSize(memory_usage) AS memory_usage,
    formatReadableQuantity(read_rows) AS read_rows,
    formatReadableSize(read_bytes) AS read_data
FROM clusterAllReplicas(default, system.query_log)
WHERE (type = 'QueryFinish') AND hasAll(tables, ['imdb_large.actors', 'imdb_large.roles'])
ORDER BY initial_query_start_time DESC
LIMIT 2
FORMAT Vertical;

Row 1:
──────
query:          SELECT *
                FROM actors AS a
                JOIN roles AS r ON a.id = r.actor_id
                FORMAT `Null`
                SETTINGS join_algorithm = 'hash'
query_duration: 5 seconds
memory_usage:   8.95 GiB
read_rows:      101.00 million
read_data:      3.41 GiB

Row 2:
──────
query:          SELECT *
                FROM roles AS r
                JOIN actors AS a ON r.actor_id = a.id
                FORMAT `Null`
                SETTINGS join_algorithm = 'hash'
query_duration: 0 seconds
memory_usage:   716.44 MiB
read_rows:      101.00 million
read_data:      3.41 GiB

正如预期的那样,右侧较小的 actors 表的联接查询消耗的内存明显比右侧较大的 roles 表的联接查询要少。

请注意,所指示的峰值内存使用量为8.95 GiB和716.44 MiB,比两个查询运行中各自右侧表的未压缩大小2.63 GiB和21.81 MiB要大。原因是哈希表的大小是根据联接键列的类型以及特定内部哈希表缓冲区大小的倍数来选择并动态增加的。 memory_usage 指标计算了为哈希表保留的总内存,尽管它可能没有完全填充。

对于两个查询的执行,ClickHouse读取了相同数量的总行数(和数据):从roles表中读取1亿行+从actors表中读取100万行。然而,右侧较大的roles表的联接查询速度慢了五倍。这是因为默认的哈希联接对于将右侧表的行插入哈希表而言并不是线程安全的。因此,哈希表的填充阶段在单个线程中运行。我们可以通过检查实际的查询流水线来进一步确认这一点。

Query pipeline

我们可以使用ClickHouse命令行客户端来检查哈希联接查询的查询流水线(可以在这里找到快速安装说明)。我们使用EXPLAIN语句来打印用DOT图形描述语言描述的查询流水线图,并使用Graphviz dot将图形呈现为PDF格式:

./clickhouse client --host ekyyw56ard.us-west-2.aws.clickhouse.cloud --secure --port 9440 --password  --database=imdb_large --query "
EXPLAIN pipeline graph=1, compact=0
SELECT *
FROM actors a
JOIN roles r ON a.id = r.actor_id
SETTINGS max_threads = 2, join_algorithm = 'hash';" | dot -Tpdf > pipeline.pdf

我们使用了上面抽象图表中的相同带圈数字对流水线进行了注释,稍微简化了主要阶段的名称,并添加了两个参与联接的表,以使两个图表保持对齐:

图片

我们可以看到查询流水线①从两个并行的流式传输阶段开始(因为max_threads设置为2),用于从右侧表格流式传输数据,接着是一个单个的填充阶段,用于填充哈希表。两个额外的并行流式传输阶段②和两个并行联接阶段③用于流式传输和联接左侧表格的数据。

正如前面提到的,对于将右侧表格的行插入哈希表, 默认的哈希联接算法不是线程安全的。因此,上述流水线中使用了一个调整大小阶段,将从右侧表格流式传输数据的两个线程减少为单线程的填充阶段。这可能会成为查询运行时的瓶颈。如果右侧表格很大-参见我们上面的两个查询运行,在联接的右侧有大型的 roles 表格的查询比较慢,速度是另一个查询的五倍。

然而,从ClickHouse 22.7版本开始,通过使用并行哈希算法,可以显著加快从右侧表格构建哈希表的过程,尤其适用于大型表格。

并行化哈希联结

描述

并行哈希联接算法是哈希联接的一种变体,它将输入数据分割成多个部分并同时构建多个哈希表,以加快联接速度,但会增加内存开销。我们在下面概述了该算法的步骤:

图片

可以看到:

① 从右侧表格的所有数据被流式传输(由于 max_threads = 2 ,以2个线程并行)到内存中。数据以块为单位进行流式传输。每个流式传输的块中的行通过将哈希函数应用于每一行的联接键而被分成2个桶( max_threads = 2 )。我们在上面的图表中用橙色和蓝色表示。并行地,每个桶使用单个线程填充一个内存中的哈希表。请注意,用于将行分成桶的哈希函数与内部使用的哈希表中的哈希函数不同。

② 从左侧表格中的数据被流式传输(由于 max_threads = 2 ,以2个线程并行),并且将步骤①中使用的相同“桶哈希函数”应用于每一行的联接键,以确定相应的哈希表,并 ③ 通过对相应的哈希表进行查找来进行联接。

请注意, max_threads 设置确定了并行哈希表的数量。稍后我们将通过检查具体的查询流水线来证明这一点。

支持的联结类型

ClickHouse支持INNER和LEFT联接类型以及除ASOF之外的所有严格度设置。

示例

现在,我们将比较相同查询中哈希和并行哈希算法的运行时间和峰值内存消耗。

右侧具有较大表格的哈希联接:

SELECT *
FROM actors AS a
JOIN roles AS r ON a.id = r.actor_id
FORMAT `Null`
SETTINGS join_algorithm = 'hash';

0 rows in set. Elapsed: 5.385 sec. Processed 101.00 million rows, 3.67 GB (18.76 million rows/s., 680.77 MB/s.)

右侧具有较大表格的并行哈希联接:

SELECT *
FROM actors AS a
JOIN roles AS r ON a.id = r.actor_id
FORMAT `Null`
SETTINGS join_algorithm = 'parallel_hash';

0 rows in set. Elapsed: 2.639 sec. Processed 101.00 million rows, 3.67 GB (38.28 million rows/s., 1.39 GB/s.)

我们查看最后两次查询运行的运行时统计信息:

SELECT
    query,
    formatReadableTimeDelta(query_duration_ms / 1000) AS query_duration,
    formatReadableSize(memory_usage) AS memory_usage,
    formatReadableQuantity(read_rows) AS read_rows,
    formatReadableSize(read_bytes) AS read_data
FROM clusterAllReplicas(default, system.query_log)
WHERE (type = 'QueryFinish') AND hasAll(tables, ['imdb_large.actors', 'imdb_large.roles'])
ORDER BY initial_query_start_time DESC
LIMIT 2
FORMAT Vertical;

Row 1:
──────
query:          SELECT *
                FROM actors AS a
                JOIN roles AS r ON a.id = r.actor_id
                FORMAT `Null`
                SETTINGS join_algorithm = 'parallel_hash'
query_duration: 2 seconds
memory_usage:   18.29 GiB
read_rows:      101.00 million
read_data:      3.41 GiB

Row 2:
──────
query:          SELECT *
                FROM actors AS a
                JOIN roles AS r ON a.id = r.actor_id
                FORMAT `Null`
                SETTINGS join_algorithm = 'hash'
query_duration: 5 seconds
memory_usage:   8.86 GiB
read_rows:      101.00 million
read_data:      3.41 GiB

并行哈希联接的运行时间大约比标准哈希联接快100%,但峰值内存消耗超过了两倍,尽管读取的行数、数据量以及右侧表格的大小对于两个查询来说是相同的。

这种更高的内存消耗的原因是查询在一个具有30个CPU核心的节点上运行,因此 max_threads 设置为30。这意味着,如下面所示,使用了30个并发的哈希表。如前所述,每个哈希表的大小最初是根据联接键列的类型以及特定内部哈希表缓冲区大小的倍数选择并动态增加的。哈希表很可能没有完全填充,但 memory_usage 指标计算了为哈希表保留的总内存。

Query pipeline

我们提到了 max_threads 设置确定了并发哈希表的数量。我们可以通过检查具体的查询流水线来验证这一点。

首先,我们检查将 max_threads 设置为2的并行哈希联接查询的查询流水线:

./clickhouse client --host ekyyw56ard.us-west-2.aws.clickhouse.cloud --secure --port 9440 --password  --database=imdb_large --query "
EXPLAIN pipeline graph=1, compact=0
SELECT *
FROM actors a
JOIN roles r ON a.id = r.actor_id
SETTINGS max_threads = 2, join_algorithm = 'parallel_hash';" | dot -Tpdf > pipeline.pdf

像往常一样,我们使用与上面抽象图表相同的带圈数字对流水线进行了注释,稍微简化了主要阶段的名称,并添加了两个参与联接的表格,以使两个图表保持对齐:

图片

我们可以看到,在填充阶段中有两个并发(并行)的填充阶段,用于使用右侧表格的数据填充两个哈希表。此外,还使用两个并发的联接阶段来联接(通过哈希表查找)左侧表格的数据。

请注意,上面的查询流水线中使用了调整大小阶段,用于在所有填充阶段和所有联接阶段之间建立明确的联结:所有联接阶段应等待所有填充阶段完成。

接下来,我们检查将 max_threads 设置为4的并行哈希联接查询的查询流水线:

./clickhouse client --host ekyyw56ard.us-west-2.aws.clickhouse.cloud --secure --port 9440 --password  --database=imdb_large --query "
EXPLAIN pipeline graph=1, compact=0
SELECT *
FROM actors a
JOIN roles r ON a.id = r.actor_id
SETTINGS max_threads = 4, join_algorithm = 'parallel_hash';" | dot -Tpdf > pipeline.pdf

图片

现在,使用四个并发的填充阶段并行地填充了四个哈希表,这些哈希表使用右侧表格的数据进行填充。同时,使用四个并发的联接阶段来联接左侧表格的数据。

原始的PR中的测量结果表明,速度提升几乎与并行度呈线性相关。

优雅哈希联结

描述

上述描述的哈希和并行哈希联接算法都非常快速,但受内存限制。如果右侧表格无法适应主内存,ClickHouse将引发OOM异常。在这种情况下,ClickHouse用户可以牺牲性能,并使用(部分)合并算法(在下一篇文章中描述),该算法在合并之前将表格的数据(部分)排序到外部存储中。

幸运的是,ClickHouse 22.12引入了另一种称为“grace hash”的联接算法,它不受内存限制,但基于哈希表,因此不需要对数据进行排序。这克服了(部分)合并算法的一些性能挑战。

该算法使用两阶段的方法来联接数据。我们的实现略有不同,以适应我们的查询流水线。下图显示了第一阶段的过程:

图片

① 从右侧表格的所有数据以块为单位(由于 max_threads = 2 ,以2个线程并行)流式传输到内存中。每个流式传输的块中的行通过将哈希函数应用于每一行的联接键而被分成3个桶(因为 grace_hash_join_initial_buckets = 3 )。我们在上图中用橙色、蓝色和绿色表示这一过程。一个内存中的哈希表使用来自第一个(橙色)桶的行进行填充。来自右侧表格的其他两个(绿色和蓝色)桶的联接被延迟,并保存到临时存储中。

请注意,如果内存中的哈希表超过了内存限制(由 max_bytes_in_join 设置),ClickHouse会动态增加桶的数量,并重新计算每一行分配的桶。不属于当前桶的任何行都会被刷新和重新分配。

另请注意,ClickHouse始终将 grace_hash_join_initial_buckets 设置的值向上舍入到最接近的2的幂。因此,将3舍入为4,并使用4个初始桶。为了可读性,在我们的图表中使用了3个桶,与使用4个桶的内部工作没有实质性差别。

② 左侧表格的数据以2个线程并行流式传输( max_threads = 2 ),并且将步骤①中使用的相同“桶哈希函数”应用于每一行的联接键,以确定相应的桶。与第一个桶对应的行被 ③ 联接(因为相应的哈希表在内存中)。其他桶的联接被延迟,并保存到临时存储中。

步骤①和②的关键是,“桶哈希函数”将一致地将值分配给相同的桶,从而有效地将数据进行分区,并通过分解来解决问题。

在第二阶段,ClickHouse处理在磁盘上剩余的桶。剩余的桶按顺序进行处理。以下两个图表概述了这一过程。第一个图表显示了如何首先处理蓝色桶。第二个图表显示了最后一个绿色桶的处理过程。

图片

图片

① ClickHouse从右侧表格数据的每个桶构建哈希表。同样,如果ClickHouse内存不足,它会动态增加桶的数量。

② 一旦从右侧表格的一个桶构建了哈希表,ClickHouse会流式传输相应左侧表格桶的数据,并 ③ 完成该对的联接。

请注意,在此阶段,可能会有一些行属于当前桶以外的另一个桶,这是因为它们在桶的数量动态增加之前被保存到临时存储中。在这种情况下,ClickHouse会将它们保存到新的实际桶中,并进一步处理它们。

这个过程对于剩余的所有桶都会重复进行。

支持的联结类型

支持INNER和LEFT联接类型以及除ASOF之外的所有严格度设置。

示例

以下是使用哈希联接和grace hash联接算法运行相同联接查询的运行时间和峰值内存消耗的比较。

右侧具有较大表格的哈希联接:

SELECT *
FROM actors AS a
JOIN roles AS r ON a.id = r.actor_id
FORMAT `Null`
SETTINGS join_algorithm = 'hash';

0 rows in set. Elapsed: 5.038 sec. Processed 101.00 million rows, 3.67 GB (20.05 million rows/s., 727.61 MB/s.)

右侧较大的表使用优雅哈希联结:

SELECT *
FROM actors AS a
JOIN roles AS r ON a.id = r.actor_id
FORMAT `Null`
SETTINGS join_algorithm = 'grace_hash', grace_hash_join_initial_buckets = 3;

0 rows in set. Elapsed: 13.117 sec. Processed 101.00 million rows, 3.67 GB (7.70 million rows/s., 279.48 MB/s.)

我们获取最近两次查询运行的运行时统计信息:

SELECT
    query,
    formatReadableTimeDelta(query_duration_ms / 1000) AS query_duration,
    formatReadableSize(memory_usage) AS memory_usage,
    formatReadableQuantity(read_rows) AS read_rows,
    formatReadableSize(read_bytes) AS read_data
FROM clusterAllReplicas(default, system.query_log)
WHERE (type = 'QueryFinish') AND hasAll(tables, ['imdb_large.actors', 'imdb_large.roles'])
ORDER BY initial_query_start_time DESC
LIMIT 2
FORMAT Vertical;

Row 1:
──────
query:          SELECT *
                FROM actors AS a
                JOIN roles AS r ON a.id = r.actor_id
                FORMAT `Null`
                SETTINGS join_algorithm = 'grace_hash', grace_hash_join_initial_buckets = 3
query_duration: 13 seconds
memory_usage:   3.72 GiB
read_rows:      101.00 million
read_data:      3.41 GiB

Row 2:
──────
query:          SELECT *
                FROM actors AS a
                JOIN roles AS r ON a.id = r.actor_id
                FORMAT `Null`
                SETTINGS join_algorithm = 'hash'
query_duration: 5 seconds
memory_usage:   8.96 GiB
read_rows:      101.00 million
read_data:      3.41 GiB

正如预期的那样,哈希联接更快。然而,grace hash联接只消耗了峰值主内存的一半。

通过增加 grace_hash_join_initial_buckets 设置,可以进一步减少grace hash联接的主内存消耗。我们通过使用 grace_hash_join_initial_buckets 设置为8重新运行查询来演示这一点:

SELECT *
FROM actors AS a
JOIN roles AS r ON a.id = r.actor_id
FORMAT `Null`
SETTINGS join_algorithm = 'grace_hash', grace_hash_join_initial_buckets = 8;

0 rows in set. Elapsed: 16.366 sec. Processed 101.00 million rows, 3.67 GB (6.17 million rows/s., 224.00 MB/s.)

让我们检查最后两个查询运行的运行时统计信息:

SELECT
    query,
    formatReadableTimeDelta(query_duration_ms / 1000) AS query_duration,
    formatReadableSize(memory_usage) AS memory_usage,
    formatReadableQuantity(read_rows) AS read_rows,
    formatReadableSize(read_bytes) AS read_data
FROM clusterAllReplicas(default, system.query_log)
WHERE (type = 'QueryFinish') AND hasAll(tables, ['imdb_large.actors', 'imdb_large.roles'])
ORDER BY initial_query_start_time DESC
LIMIT 2
FORMAT Vertical;

Row 1:
──────
query:          SELECT *
                FROM actors AS a
                JOIN roles AS r ON a.id = r.actor_id
                FORMAT `Null`
                SETTINGS join_algorithm = 'grace_hash', grace_hash_join_initial_buckets = 8
query_duration: 16 seconds
memory_usage:   2.10 GiB
read_rows:      101.00 million
read_data:      3.41 GiB

Row 2:
──────
query:          SELECT *
                FROM actors AS a
                JOIN roles AS r ON a.id = r.actor_id
                FORMAT `Null`
                SETTINGS join_algorithm = 'grace_hash', grace_hash_join_initial_buckets = 3
query_duration: 13 seconds
memory_usage:   3.72 GiB
read_rows:      101.00 million
read_data:      3.41 GiB

使用8个初始桶的grace hash联接运行消耗的主内存比使用3个初始桶的运行少约70%。通过增加桶的数量,可以以相当线性的方式减少内存消耗,尽管执行时间会稍微增加。

请注意,正如前面提到并在下面演示的那样,ClickHouse始终将 grace_hash_join_initial_buckets 设置的值向上舍入到最接近的2的幂。因此,设置 grace_hash_join_initial_buckets 为3的查询实际上使用了4个初始桶。

Query pipeline

我们检查将 max_threads 设置为2和 grace_hash_join_initial_buckets 设置为3的grace hash联接查询的查询流水线:

./clickhouse client --host ekyyw56ard.us-west-2.aws.clickhouse.cloud --secure --port 9440 --password  --database=imdb_large --query "
EXPLAIN pipeline graph=1, compact=0
SELECT *
FROM actors AS a
JOIN roles AS r ON a.id = r.actor_id
FORMAT `Null`
SETTINGS max_threads = 2, join_algorithm = 'grace_hash', grace_hash_join_initial_buckets = 3';" | dot -Tpdf > pipeline.pdf

使用添加的带圈数字、稍微简化的主要阶段名称和添加的两个参与联接的表格,以与上面的抽象图表保持对齐:

图片

我们可以看到,在①处有两个并行流式传输阶段( max_threads=2 ),从右侧表格中的数据流式传输到内存中。我们还可以看到使用了两个并行填充阶段来填充内存中的哈希表。两个额外的并行流式传输阶段②和两个并行联接阶段③用于流式传输和联接左侧表格的数据。延迟阶段表示某些联接阶段被推迟。

但是,我们无法从查询流水线中看到桶的数量,因为桶的创建是动态的,并且取决于内存压力,ClickHouse会根据需要动态增加桶的数量。所有的桶都在延迟的...Transform阶段中进行处理。

为了检查已创建和处理的桶的数量,我们需要检查grace hash联接查询的实际执行情况,通过要求ClickHouse在执行期间向ClickHouse命令行客户端发送追踪级别的日志。

我们使用 max_threads 设置为2和 grace_hash_join_initial_buckets 值为3(请注意 send_logs_level='trace' 设置)来执行grace hash联接查询:

./clickhouse client --host ea3kq2u4fm.eu-west-1.aws.clickhouse.cloud --secure --password  --database=imdb_large --send_logs_level='trace' --query "
SELECT *
FROM actors AS a
JOIN roles AS r ON a.id = r.actor_id
FORMAT Null
SETTINGS max_threads = 2, join_algorithm = 'grace_hash', grace_hash_join_initial_buckets = 3;"

    ...
... GraceHashJoin: Initialize 4 buckets
... GraceHashJoin: Joining file bucket 0
    ...
... imdb_large.actors ...: Reading approx. 1000000 rows with 2 streams
    ...
... imdb_large.roles ...: Reading approx. 100000000 rows with 2 streams
    ...
... GraceHashJoin: Joining file bucket 1
... GraceHashJoin: Loaded bucket 1 with 250000(/25000823) rows
    ...
... GraceHashJoin: Joining file bucket 2
... GraceHashJoin: Loaded bucket 2 with 250000(/24996460) rows
    ...
... GraceHashJoin: Joining file bucket 3
... GraceHashJoin: Loaded bucket 3 with 250000(/25000742) rows
    ...
... GraceHashJoin: Finished loading all 4 buckets
    ...

现在我们可以看到创建了4个(而不是3个)初始桶。因为正如之前提到的,ClickHouse始终将 grace_hash_join_initial_buckets 设置的值向上舍入到最接近的2的幂。我们还可以看到每个表格使用了2个并行流式传输阶段来读取表格的行。两个表格的第一个对应桶(上面的追踪日志消息中的桶0)立即进行联接。

其他3个桶被写入磁盘,并在稍后顺序加载以进行联接。我们可以看到,每个桶中的行数均匀分配 - 分别为25万行和约2,500万行,分别对应于1百万行和1亿行。

为了比较,我们使用 max_threads 设置为4和 grace_hash_join_initial_buckets 值为8(请注意 send_logs_level='trace' 设置)来执行grace hash联接查询:

./clickhouse client --host ea3kq2u4fm.eu-west-1.aws.clickhouse.cloud --secure --password  --database=imdb_large --send_logs_level='trace' --query "
SELECT *
FROM actors AS a
JOIN roles AS r ON a.id = r.actor_id
FORMAT Null
SETTINGS max_threads = 4, join_algorithm = 'grace_hash', grace_hash_join_initial_buckets = 8;"

    ...
... GraceHashJoin: Initialize 8 buckets
... GraceHashJoin: Joining file bucket 0
    ...
... imdb_large.actors ...: Reading approx. 1000000 rows with 4 streams
    ...
... imdb_large.roles ...: Reading approx. 100000000 rows with 4 streams
    ...
...  GraceHashJoin: Joining file bucket 1
...  GraceHashJoin: Loaded bucket 1 with 125000(/12502068) rows
    ...
...  GraceHashJoin: Joining file bucket 2
...  GraceHashJoin: Loaded bucket 2 with 125000(/12498406) rows
    ...
...  GraceHashJoin: Joining file bucket 3
...  GraceHashJoin: Loaded bucket 3 with 125000(/12502699) rows
    ...
...  GraceHashJoin: Joining file bucket 4
...  GraceHashJoin: Loaded bucket 4 with 125000(/12498074) rows
    ...
...  GraceHashJoin: Joining file bucket 5
...  GraceHashJoin: Loaded bucket 5 with 125000(/12498755) rows
    ...
...  GraceHashJoin: Joining file bucket 6
...  GraceHashJoin: Loaded bucket 6 with 125000(/12498054) rows
    ...
...  GraceHashJoin: Joining file bucket 7
...  GraceHashJoin: Loaded bucket 7 with 125000(/12498043) rows
    ...
...  GraceHashJoin: Finished loading all 8 buckets
    ...

我们可以看到创建了8个初始桶,并且每个表格使用了4个并行流式传输阶段来读取表格的行。

总结

这篇博文详细介绍并比较了基于内存哈希表的3种ClickHouse联接算法。

哈希联接算法快速且最通用,支持所有联接类型和严格度设置,但内存中哈希表的创建是单线程的,如果右侧表格非常大,可能会成为瓶颈。

并行哈希联接算法通过同时构建多个哈希表,在右侧表格很大时可能更快,但需要更多内存。

Grace hash联接算法是一种非内存受限的版本,将输入数据分成桶,在处理之前将其中一些转移到磁盘上,然后在内存中顺序处理。

下表总结了本文中所有联接查询运行的内存消耗和执行时间(使用max_threads设置为30,并且右侧表格较大):

图片

在本系列的下一部分中,我们将探索ClickHouse中剩下的3种联接算法:

  • Full sorting merge join

  • Partial merge join

  • Direct join

图片

联系我们

手机号:13910395701

邮箱:Tracy.Wang@clickhouse.com

满足您所有的在线分析列式数据库管理需求

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值