跳转到主要内容
INNOT INGLOBAL INGLOBAL NOT IN 运算符将单独介绍,因为其功能较为复杂。 该运算符的左侧为单个列或一个 Tuple。 示例:
如果左侧是索引中的单个列,右侧是一组常量,则系统将使用该索引处理查询。 不要显式列举过多的值 (例如数百万个) 。如果数据集较大,请将其放入临时表中 (例如,请参阅用于查询处理的外部数据章节) ,然后使用子查询。 该运算符的右侧可以是一组常量表达式、一组包含常量表达式的 Tuple (如上述示例所示) ,或者数据库表的名称,或括号中的 SELECT 子查询。 出于历史兼容性考虑,当右侧为单个 tuple 表达式时,该表达式的解释方式取决于 IN 运算符左侧的内容——既可被解释为一组值,也可被解释为单个 Tuple 值。若左侧为标量值,ClickHouse 会将该右侧 tuple 表达式中的各元素视为独立的 IN 值:
Query
Response
其行为类似于 SELECT 1 IN (1, 2)。如果左侧也是一个 Tuple,则右侧将被解释为 Tuple 值的集合:
Query
Response
此特殊处理仅适用于右侧为单个 tuple 表达式的情况。标量左侧无法与包含多个 tuple 值的右侧进行匹配:
Query
Response
ClickHouse 允许 IN 子查询左右两侧的类型不同。 在这种情况下,它会将右侧的值转换为左侧的类型, 如同对右侧应用了 accurateCastOrNull 函数。 这意味着数据类型将变为 Nullable,若转换无法执行,则返回 NULL 示例
Query
Response
如果运算符右侧是表名 (例如 UserID IN users) ,则等价于子查询 UserID IN (SELECT * FROM users)。当需要处理随查询一起发送的外部数据时,可使用此方式。例如,可将查询与一组已加载到 ‘users’ 临时表中的用户 ID 一起发送,并对其进行过滤。 如果运算符右侧是使用 Set 引擎的表名 (一个始终驻留在 RAM 中的预备数据集) ,则该数据集不会在每次查询时重新创建。 子查询可以指定多个列来过滤元组。 示例:
Query
IN 运算符左右两侧的列应为相同类型。 IN 运算符和子查询可以出现在查询的任何位置,包括聚合函数和 lambda 函数中。 示例:
Query
Response
对于 3 月 17 日之后的每一天,统计由在 3 月 17 日访问过该网站的用户贡献的页面浏览量占比。 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 / JOINGLOBAL IN / GLOBAL JOIN。两者在分布式查询处理中的执行方式上存在差异。
请注意,下面介绍的算法可能会因 设置 distributed_product_mode 的设置不同而表现不同。
使用常规 IN 时,查询会被发送到远程服务器,每台服务器分别执行 INJOIN 子句中的子查询。 使用 GLOBAL IN / GLOBAL JOIN 时,系统会首先执行所有子查询,并将结果收集到临时表中。随后,这些临时表会被发送到每个远程服务器,查询将在各服务器上使用这些临时数据执行。 对于 GLOBAL ... JOIN,哪一侧作为子查询进行计算取决于 JOIN 的类型:对于 LEFTINNER JOIN,计算右表;对于 RIGHT JOIN,则计算左表,因为右表是保留侧,需从各分片中读取。 对于非 Distributed 查询,请使用常规的 IN / JOIN 在分布式查询处理中,使用 IN / JOIN 子句中的子查询时请务必谨慎。 我们来看一些示例。假设集群中的每台服务器都有一个普通的 local_table 表。每台服务器还有一个 distributed_table 表,其类型为 Distributed,该表覆盖集群中的所有服务器。 针对 distributed_table 的查询会被分发到所有远程服务器,并在这些服务器上使用 local_table 执行。 例如,以下查询
将作为以下内容发送至所有远程服务器
并在每个节点上并行执行,直到中间结果可以被合并为止。随后,中间结果将返回至请求服务器并在其上完成合并,最终结果再发送至客户端。 现在来看一个使用 IN 的查询:
  • 两个站点受众的交集计算。
此查询将以如下形式发送到所有远程服务器
换句话说,IN 子句中的数据集将在每台服务器上独立收集,且仅基于各服务器本地存储的数据。 只有当您已针对此情况做好规划,并将数据分散到集群各服务器上,使同一个 UserID 的数据完整地存储在同一台服务器上时,此方式才能正确且高效地运行。在这种情况下,每台服务器均可在本地获取所有所需数据。否则,结果将不准确。我们将这种查询变体称为 “local IN”。 要修正数据随机分布在集群服务器上时查询的执行方式,可以在子查询中指定 distributed_table。该查询如下所示:
此查询将以如下形式发送到所有远程服务器
子查询将在每个远程服务器上开始执行。由于该子查询使用了分布式表,每个远程服务器上的子查询将被重新发送至所有远程服务器,具体形式如下:
例如,如果集群中有 100 台服务器,执行整个查询将需要发起 10,000 个基本请求,这通常是不可接受的。 在这种情况下,应始终使用 GLOBAL IN 而非 IN。下面来看看它对以下查询的工作原理:
请求方服务器将执行子查询:
结果将存入 RAM 中的临时表。然后,该请求将以如下形式发送至每个远程服务器:
临时表 _data1 会随查询一起发送到每台远程服务器 (临时表的名称由实现决定) 。 这比使用普通的 IN 更优。不过,请注意以下几点:
  1. 创建临时表时,数据不会自动去重。为了减少网络传输的数据量,请在子查询中指定 DISTINCT。 (普通的 `IN“ 不需要这样做。)
  2. 临时表会被发送到所有远程服务器。传输过程不会考虑网络拓扑。例如,如果有 10 台远程服务器位于相对于请求方服务器非常偏远的数据中心,那么数据将通过通往该远程数据中心的链路传输 10 次。使用 GLOBAL IN 时,应尽量避免使用大型数据集。
  3. 向远程服务器传输数据时,网络带宽限制无法配置。这样可能会导致网络过载。
  4. 请尽量将数据分布到各台服务器上,以避免经常使用 GLOBAL IN
  5. 如果你经常需要使用 GLOBAL IN,请规划 ClickHouse 集群的部署位置,使同一组副本不要分布在多个数据中心,而是只位于一个数据中心内,并确保它们之间具备高速网络,这样查询就可以完全在单个数据中心内完成处理。
GLOBAL IN 子句中指定本地表也是合理的,例如,当该本地表仅在请求方服务器上可用,而你希望远程服务器也能使用其中的数据时。

分布式子查询与 max_rows_in_set

你可以使用 max_rows_in_setmax_bytes_in_set 来控制分布式查询期间传输的数据量。 如果 GLOBAL IN 查询返回大量数据,这一点尤为重要。请看下面的 SQL:
如果 some_predicate 的选择性不够高,就会返回大量数据并引发性能问题。在这种情况下,最好限制通过网络传输的数据量。另外请注意,set_overflow_mode 默认设置为 throw,这意味着当达到这些阈值时会抛出异常。

分布式子查询与 max_parallel_replicas

max_parallel_replicas 大于 1 时,分布式查询会进一步改写。 例如,如下所示:
在每个服务器上都会转换为:
其中,M 的取值介于 13 之间,具体取决于本地查询是在哪个副本上执行的。 这些设置会影响查询中的每个 MergeTree 家族表,其效果与对每个表应用 SAMPLE 1/3 OFFSET (M-1)/3 相同。 因此,只有当两个表具有相同的复制方案,并且都按 UserID 或其子键进行采样时,添加 max_parallel_replicas 设置才会得到正确结果。特别是,如果 local_table_2 没有采样键,就会产生错误结果。同样的规则也适用于 JOIN 如果 local_table_2 不满足这些要求,一种变通办法是使用 GLOBAL INGLOBAL JOIN 如果表没有采样键,则可以使用更灵活的 parallel_replicas_custom_key 选项,从而实现不同且更优的行为。
最后修改于 2026年7月2日