ClickHouse是一个开源的面向列的数据库管理系统(DBMS),专为需要在大量数据上进行超低延迟分析查询的用例进行构建和优化。为了实现分析应用的最佳性能,通常会将表合并为一个称为数据去规范化的过程。扁平化的表通过避免连接操作来最小化查询延迟,但会增加增量ETL的复杂性,通常可以接受以换取亚秒级的查询性能。
然而,对于某些工作负载,例如来自传统数据仓库的工作负载,去规范化数据并不总是可行的,有时分析查询的源数据的一部分需要保持规范化。这些规范化的表占用更少的存储空间,并提供了数据组合的灵活性,但对于某些类型的分析需要在查询时进行连接操作。
幸运的是,与一些误解相反,ClickHouse完全支持连接操作!除了支持所有标准的SQL JOIN类型外,ClickHouse还提供了其他适用于分析工作负载和时间序列分析的JOIN类型。ClickHouse允许您在连接执行时选择6种不同的算法(我们将在博客系列的下一部分中详细探讨),或者允许查询规划器根据资源可用性和使用情况在运行时自适应选择和动态更改算法。
在ClickHouse中,即使是对大型表的连接操作也可以实现良好的性能,但这种用例尤其需要用户仔细选择和调整连接算法以适应其查询工作负载。虽然我们预计随着时间的推移,这种过程也会变得更加自动化和启发式驱动,但本博客系列提供了对ClickHouse连接执行内部原理的深入了解,以便您可以为应用程序常用的查询优化连接操作。
在本文中,我们将使用一个规范化的关系数据库示例模式,以演示ClickHouse中可用的不同连接类型。在接下来的文章中,我们将深入了解ClickHouse中可用的6种不同的连接算法。我们将探索ClickHouse如何将这些连接算法集成到其查询流水线中,以尽快执行连接类型。未来的部分将涵盖分布式连接操作。
Test Data and Resources
我们使用Venn图和示例查询来解释ClickHouse中可用的连接类型,这些示例查询基于来自关系数据集存储库的规范化IMDB数据集。
创建和加载表的说明:
上面的4个表中的数据代表movies。一个电影可以有一个或多个genres。电影中的roles由actors扮演。图中的箭头表示外键与主键之间的关系。例如,genres表中一行的movie_id列包含movies表中一行的id值。
movies和actors之间存在多对多的关系。通过使用roles表,将这种多对多的关系规范化为两个一对多的关系。roles表的每一行包含movies表和actors表的id字段的值。
Join types supported in ClickHouse
• INNER JOIN
• OUTER JOIN
• CROSS JOIN
• SEMI JOIN
• ANTI JOIN
• ANY JOIN
• ASOF JOIN
INNER JOIN
内连接(INNER JOIN)对于每对在连接键上匹配的行,返回左表行的列值与右表行的列值的组合。如果一行有多个匹配项,则返回所有匹配项(这意味着对于具有匹配的连接键的行,会产生笛卡尔积)。
以下查询通过将movies表与genres表进行连接,找到每部电影的genre(s):
请注意,可以省略INNER关键字。使用以下其他连接类型之一,可以扩展或更改INNER JOIN的行为。
左外连接(LEFT OUTER JOIN)的行为类似于内连接(INNER JOIN),此外对于左表中的非匹配行,ClickHouse会返回右表列的默认值。
右外连接(RIGHT OUTER JOIN)查询类似于左外连接,它还返回右表中非匹配行的值,以及左表列的默认值。
全外连接(FULL OUTER JOIN)查询结合了左外连接和右外连接,它返回左表和右表中非匹配行的值,以及左表和右表列的默认值。
以下查询找出所有没有流派的电影,通过查询movies表中在genres表中没有匹配的所有行,因此在查询时,对于movie_id列会得到默认值0:
Note that the OUTER keyword can be omitted.
CROSS JOIN
交叉连接(CROSS JOIN)在不考虑连接键的情况下产生两个表的完全笛卡尔积。左表的每一行都与右表的每一行组合在一起。
因此,下面的查询将movies表的每一行与genres表的每一行组合在一起。
虽然前面的示例查询本身并没有太多意义,但可以通过使用WHERE子句将匹配的行关联起来,来扩展查询,以实现查找每个movies的genre的内连接行为:
一个CROSS JOIN的另一种语法是在FROM子句中用逗号分隔的方式指定多个表。 如果查询的WHERE部分中存在连接表达式,ClickHouse会将CROSS JOIN重写为INNER JOIN。 我们可以通过EXPLAIN SYNTAX来检查示例查询是否符合这种情况(它返回查询在执行之前被重写的语法优化版本):
在语法优化的交叉JOIN查询版本中,内部JOIN子句包含“ALL”关键字,该关键字是为了保持交叉JOIN的笛卡尔积语义,即使被重写为内部JOIN,因此笛卡尔积可以被禁用。
如上所述,可以省略外部连接,并且可以添加可选的所有关键字,你可以写所有的外部连接,它会正常工作。
(LEFT / RIGHT) SEMI JOIN
左半连接(LEFT SEMI JOIN)查询返回左表中至少在右表中有一个连接键匹配的行的列值。只返回找到的第一个匹配项(禁用笛卡尔积)。
右半连接(RIGHT SEMI JOIN)查询类似,它返回右表中至少在左表中有一个匹配的行的值,但只返回找到的第一个匹配项。
以下查询找出在2023年参演过电影的所有演员/女演员。请注意,如果使用普通的(INNER)连接,同一个演员/女演员如果在2023年中扮演了多个角色,将会出现多次:
(LEFT / RIGHT) ANTI JOIN
左反连接(LEFT ANTI JOIN)返回左表中所有非匹配行的列值。
类似地,右反连接(RIGHT ANTI JOIN)返回右表中所有非匹配行的列值。
我们之前的外连接示例查询的另一种形式是使用反连接来查找数据集中没有genre的movie:
该查询使用左反连接将movies表的id与genres表的movie_id进行连接。它将返回movies表中所有没有与genres表匹配的行,即数据集中没有流派的电影的标题。这样,查询结果将只包含没有流派的电影的标题。
(LEFT / RIGHT / INNER) ANY JOIN
左任意连接(LEFT ANY JOIN)是左外连接(LEFT OUTER JOIN)和左半连接(LEFT SEMI JOIN)的组合,意味着ClickHouse将返回左表中每一行的列值,要么与右表中匹配行的列值组合,要么与右表的默认列值组合(如果没有匹配)。如果左表的一行在右表中有多个匹配项,ClickHouse只返回第一个匹配项的组合列值(禁用笛卡尔积)。
类似地,右任意连接(RIGHT ANY JOIN)是右外连接(RIGHT OUTER JOIN)和右半连接(RIGHT SEMI JOIN)的组合。
内任意连接(INNER ANY JOIN)是内连接(INNER JOIN)并禁用了笛卡尔积。
我们使用一个抽象示例和两个临时表(left_table和right_table)来演示左任意连接:
This is the same query using a RIGHT ANY JOIN:
This is the query with an INNER ANY JOIN:
ASOF JOIN
ASOF JOIN提供了非精确匹配的能力。如果左表中的一行在右表中没有精确匹配,那么将使用最接近的右表行作为匹配。
这在时间序列分析中非常有用,并可以大大降低查询的复杂性。
我们将以股票市场数据的时间序列分析为例。一个quotes表包含了基于每天特定时间的股票符号报价。在我们的示例数据中,价格每10秒更新一次。一个trades表列出了符号的交易 - 在特定时间买入的特定数量的符号:
为了计算每笔交易的具体成本,我们需要将交易与其最接近的报价时间进行匹配。
使用ASOF JOIN,这个过程非常简单和紧凑,我们可以在ON子句中指定精确匹配条件,并在AND子句中指定最接近匹配条件 - 我们要寻找在交易日期之前或者正好等于交易日期的quotes表中的最接近行:
请注意,ASOF JOIN的ON子句是必需的,并且在AND子句的非精确匹配条件旁边指定了一个精确匹配条件。
目前,ClickHouse不支持没有任何连接键部分执行严格匹配的连接(尚未支持)。
Summary
这篇博客文章展示了ClickHouse如何支持所有标准的SQL JOIN类型,以及为支持分析查询而设计的专用JOIN类型。我们描述和演示了所有支持的JOIN类型。
在这个系列的下一部分中,我们将探讨ClickHouse如何将经典的JOIN算法与其查询管道相结合,以尽可能快地执行本文中描述的JOIN类型。