IN、NOT IN、GLOBAL IN 和 GLOBAL NOT IN 运算符将单独介绍,因为其功能较为复杂。
该运算符的左侧为单个列或一个 Tuple。
示例:
SELECT 子查询。
出于历史兼容性考虑,当右侧为单个 tuple 表达式时,该表达式的解释方式取决于 IN 运算符左侧的内容——既可被解释为一组值,也可被解释为单个 Tuple 值。若左侧为标量值,ClickHouse 会将该右侧 tuple 表达式中的各元素视为独立的 IN 值:
Query
Response
SELECT 1 IN (1, 2)。如果左侧也是一个 Tuple,则右侧将被解释为 Tuple 值的集合:
Query
Response
tuple 表达式的情况。标量左侧无法与包含多个 tuple 值的右侧进行匹配:
Query
Response
IN 子查询左右两侧的类型不同。
在这种情况下,它会将右侧的值转换为左侧的类型,
如同对右侧应用了 accurateCastOrNull 函数。
这意味着数据类型将变为 Nullable,若转换无法执行,则返回 NULL。
示例
Query
Response
UserID IN users) ,则等价于子查询 UserID IN (SELECT * FROM users)。当需要处理随查询一起发送的外部数据时,可使用此方式。例如,可将查询与一组已加载到 ‘users’ 临时表中的用户 ID 一起发送,并对其进行过滤。
如果运算符右侧是使用 Set 引擎的表名 (一个始终驻留在 RAM 中的预备数据集) ,则该数据集不会在每次查询时重新创建。
子查询可以指定多个列来过滤元组。
示例:
Query
IN 运算符左右两侧的列应为相同类型。
IN 运算符和子查询可以出现在查询的任何位置,包括聚合函数和 lambda 函数中。
示例:
Query
Response
IN 子句中的子查询始终只会在单个服务器上执行一次。不存在相关子查询。
NULL 处理
IN 运算符认为,与 NULL 进行运算的结果始终为 0,无论 NULL 位于运算符左侧还是右侧。如果 transform_null_in = 0,则任何数据集中都不会包含 NULL 值;它们彼此不对应,也无法比较。
下面是一个使用 t_null 表的示例:
SELECT x FROM t_null WHERE y IN (NULL,3) 将得到以下结果:
y = NULL 的那一行被从查询结果中排除了。这是因为 ClickHouse 无法判断 NULL 是否属于 (NULL,3) 这个 Set,因此会返回 0 作为该运算的结果,而 SELECT 会将这一行排除在最终输出之外。
Distributed 子查询
IN 运算符 (与 JOIN 运算符类似) ,有两种选项:普通的 IN / JOIN 和 GLOBAL IN / GLOBAL JOIN。两者在分布式查询处理中的执行方式上存在差异。
请注意,下面介绍的算法可能会因 设置
distributed_product_mode 的设置不同而表现不同。IN 时,查询会被发送到远程服务器,每台服务器分别执行 IN 或 JOIN 子句中的子查询。
使用 GLOBAL IN / GLOBAL JOIN 时,系统会首先执行所有子查询,并将结果收集到临时表中。随后,这些临时表会被发送到每个远程服务器,查询将在各服务器上使用这些临时数据执行。
对于 GLOBAL ... JOIN,哪一侧作为子查询进行计算取决于 JOIN 的类型:对于 LEFT 和 INNER JOIN,计算右表;对于 RIGHT JOIN,则计算左表,因为右表是保留侧,需从各分片中读取。
对于非 Distributed 查询,请使用常规的 IN / JOIN。
在分布式查询处理中,使用 IN / JOIN 子句中的子查询时请务必谨慎。
我们来看一些示例。假设集群中的每台服务器都有一个普通的 local_table 表。每台服务器还有一个 distributed_table 表,其类型为 Distributed,该表覆盖集群中的所有服务器。
针对 distributed_table 的查询会被分发到所有远程服务器,并在这些服务器上使用 local_table 执行。
例如,以下查询
IN 的查询:
- 两个站点受众的交集计算。
IN 子句中的数据集将在每台服务器上独立收集,且仅基于各服务器本地存储的数据。
只有当您已针对此情况做好规划,并将数据分散到集群各服务器上,使同一个 UserID 的数据完整地存储在同一台服务器上时,此方式才能正确且高效地运行。在这种情况下,每台服务器均可在本地获取所有所需数据。否则,结果将不准确。我们将这种查询变体称为 “local IN”。
要修正数据随机分布在集群服务器上时查询的执行方式,可以在子查询中指定 distributed_table。该查询如下所示:
GLOBAL IN 而非 IN。下面来看看它对以下查询的工作原理:
_data1 会随查询一起发送到每台远程服务器 (临时表的名称由实现决定) 。
这比使用普通的 IN 更优。不过,请注意以下几点:
- 创建临时表时,数据不会自动去重。为了减少网络传输的数据量,请在子查询中指定 DISTINCT。 (普通的 `IN“ 不需要这样做。)
- 临时表会被发送到所有远程服务器。传输过程不会考虑网络拓扑。例如,如果有 10 台远程服务器位于相对于请求方服务器非常偏远的数据中心,那么数据将通过通往该远程数据中心的链路传输 10 次。使用
GLOBAL IN时,应尽量避免使用大型数据集。 - 向远程服务器传输数据时,网络带宽限制无法配置。这样可能会导致网络过载。
- 请尽量将数据分布到各台服务器上,以避免经常使用
GLOBAL IN。 - 如果你经常需要使用
GLOBAL IN,请规划 ClickHouse 集群的部署位置,使同一组副本不要分布在多个数据中心,而是只位于一个数据中心内,并确保它们之间具备高速网络,这样查询就可以完全在单个数据中心内完成处理。
GLOBAL IN 子句中指定本地表也是合理的,例如,当该本地表仅在请求方服务器上可用,而你希望远程服务器也能使用其中的数据时。
分布式子查询与 max_rows_in_set
max_rows_in_set 和 max_bytes_in_set 来控制分布式查询期间传输的数据量。
如果 GLOBAL IN 查询返回大量数据,这一点尤为重要。请看下面的 SQL:
some_predicate 的选择性不够高,就会返回大量数据并引发性能问题。在这种情况下,最好限制通过网络传输的数据量。另外请注意,set_overflow_mode 默认设置为 throw,这意味着当达到这些阈值时会抛出异常。
分布式子查询与 max_parallel_replicas
M 的取值介于 1 到 3 之间,具体取决于本地查询是在哪个副本上执行的。
这些设置会影响查询中的每个 MergeTree 家族表,其效果与对每个表应用 SAMPLE 1/3 OFFSET (M-1)/3 相同。
因此,只有当两个表具有相同的复制方案,并且都按 UserID 或其子键进行采样时,添加 max_parallel_replicas 设置才会得到正确结果。特别是,如果 local_table_2 没有采样键,就会产生错误结果。同样的规则也适用于 JOIN。
如果 local_table_2 不满足这些要求,一种变通办法是使用 GLOBAL IN 或 GLOBAL JOIN。
如果表没有采样键,则可以使用更灵活的 parallel_replicas_custom_key 选项,从而实现不同且更优的行为。