【Calcite 系列】将 INTERSECT 转换为 EXISTS

1. 背景与动机 (Motivation)

在 SQL 标准中,INTERSECT(交集)是一个常见的集合操作符。默认情况下,INTERSECT 暗含 DISTINCT 语义,即返回同时存在于两个查询结果集中的唯一行。

在传统的执行引擎中,INTERSECT 通常通过以下两种方式实现:

  1. Hash Join / Merge Join:将两边数据进行全量 Join 并在之后进行去重。
  2. Sort + Unique:对两边结果集排序后进行比对去重。

然而,在很多场景下,特别是当左表(Left Input)较小而右表(Right Input)很大且有索引时,将 INTERSECT 转换为 EXISTS 子查询(即 Semi-Join 半连接)往往能获得巨大的性能提升。

为什么 EXISTS 更快?

  • 短路优化EXISTS 只需要找到第一条匹配的记录即可返回,无需处理右表的所有重复项。
  • 谓词下推 :优化器更容易将过滤条件推入 EXISTS 子查询中。
  • 索引利用EXISTS 转换后生成的 Semi-Join 可以直接触发索引扫描,避免全表集合运算。

基于此,CALCITE-6836 引入了 IntersectToExistsRule 规则。


2. 核心设计方案 (Design)

2.1 转换逻辑

该规则的目标是将 LogicalIntersect 节点转换为等价的 LogicalCorrelate(带 EXISTS)或 Semi-Join 结构。

对于多路交集(N-way Intersect):

sql 复制代码
SELECT columns FROM T1
INTERSECT
SELECT columns FROM T2
INTERSECT
SELECT columns FROM T3

会被改写为:

sql 复制代码
SELECT DISTINCT columns FROM T1
WHERE EXISTS (SELECT 1 FROM T2 WHERE T2.cols = T1.cols)
  AND EXISTS (SELECT 1 FROM T3 WHERE T3.cols = T1.cols)

2.2 限制条件

为了保证语义正确性,该规则仅在以下场景触发:

  1. 仅限 INTERSECT DISTINCT :不支持 INTERSECT ALL。因为 ALL 需要精确计数匹配次数,而 EXISTS 只关心"有无"。
  2. 列数与类型匹配:所有输入的字段数量和物理类型必须能够进行等值比较。

3. 实现细节剖析 (Implementation)

在 PR #4209 中,核心实现逻辑集中在新增的 IntersectToExistsRule 类中。

3.1 规则触发机制

规则匹配 LogicalIntersect 算子:

java 复制代码
// 核心匹配逻辑
public class IntersectToExistsRule extends RelOptRule {
    public static final IntersectToExistsRule INSTANCE = new IntersectToExistsRule(RelBuilderFactory.INSTANCE);

    private IntersectToExistsRule(RelBuilderFactory factory) {
        super(operand(LogicalIntersect.class, any()), factory, null);
    }

    @Override
    public void onMatch(RelOptRuleCall call) {
        LogicalIntersect intersect = call.rel(0);
        // 1. 过滤 INTERSECT ALL
        if (intersect.all) {
            return;
        }
        
        // 2. 转换开始...
        transform(call, intersect);
    }
}

3.2 树结构重组

通过 RelBuilder 递归地将每一个右侧输入(Input[1...N])包装成一个 EXISTS 子查询,并与第一个输入(Input[0])通过 Correlate 关联。

核心伪代码逻辑

java 复制代码
RelNode left = intersect.getInput(0);
List<RelNode> rights = intersect.getInputs().subList(1, n);

RelBuilder builder = call.builder();
builder.push(left);

for (RelNode right : rights) {
    // 为每一个交集项创建一个 CorrelationId
    CorrelationId id = cluster.createCorrel();
    
    // 构造内部 Filter 条件:T1.col1 = T2.col1 AND T1.col2 = T2.col2 ...
    builder.push(right);
    RexNode condition = builder.fields().stream()
        .map(f -> builder.equals(f, builder.field(id, f.getIndex())))
        .reduce(builder::and)
        .get();
        
    builder.filter(condition);
    // 包装成 EXISTS 语义
    builder.exists(false); 
    
    // 执行逻辑关联
    builder.join(JoinRelType.INNER, builder.literal(true));
}

// 最后处理 INTERSECT DISTINCT 语义
builder.distinct();
call.transformTo(builder.build());

4. 优化前后的执行计划对比

假设我们有两张表 EMPDEPT,执行 SELECT deptno FROM EMP INTERSECT SELECT deptno FROM DEPT

优化前

使用的是集合运算节点,通常会导致全量数据扫描和代价昂贵的 SetOp 实现。

text 复制代码
LogicalIntersect(all=[false])
  LogicalProject(deptno=[$7])
    LogicalTableScan(table=[[scott, EMP]])
  LogicalProject(deptno=[$0])
    LogicalTableScan(table=[[scott, DEPT]])

优化后

转换成了 Correlate 配合 EXISTS 过滤器。

text 复制代码
LogicalAggregate(group=[{0}])
  LogicalCorrelate(correlation=[$cor0], joinType=[inner], variant=[exists])
    LogicalProject(deptno=[$7])
      LogicalTableScan(table=[[scott, EMP]])
    LogicalFilter(condition=[=($0, $cor0.deptno)])
      LogicalTableScan(table=[[scott, DEPT]])

注:此计划随后可以进一步被 SemiJoinRule 优化为标准的物理 SemiJoin 算子。


5. 测试验证 (Testing)

PR 中包含了完善的单元测试(RelOptRulesTest.xml),涵盖了:

  • 基础两路交集 :验证 INTERSECT 转换为 EXISTS 的基本正确性。
  • 多路交集:测试 A ∩ B ∩ C 的链式转换。
  • NULL 语义测试 :确保 INTERSECT 对 NULL 的处理(NULLs are equal for intersection)在转换后依然保持一致。
  • 负案例 :确保 INTERSECT ALL 不会被错误触发。

参考资料

相关推荐
草履虫建模10 小时前
力扣算法 1768. 交替合并字符串
java·开发语言·算法·leetcode·职场和发展·idea·基础
qq_2975746712 小时前
【实战教程】SpringBoot 实现多文件批量下载并打包为 ZIP 压缩包
java·spring boot·后端
老毛肚12 小时前
MyBatis插件原理及Spring集成
java·spring·mybatis
学嵌入式的小杨同学12 小时前
【Linux 封神之路】信号编程全解析:从信号基础到 MP3 播放器实战(含核心 API 与避坑指南)
java·linux·c语言·开发语言·vscode·vim·ux
lang2015092813 小时前
JSR-340 :高性能Web开发新标准
java·前端·servlet
Re.不晚13 小时前
Java入门17——异常
java·开发语言
缘空如是13 小时前
基础工具包之JSON 工厂类
java·json·json切换
追逐梦想的张小年13 小时前
JUC编程04
java·idea
好家伙VCC13 小时前
### WebRTC技术:实时通信的革新与实现####webRTC(Web Real-TimeComm
java·前端·python·webrtc
南极星100514 小时前
蓝桥杯JAVA--启蒙之路(十)class版本 模块
java·开发语言