news 2026/9/10 20:47:03

TiDB FULL OUTER JOIN 支持解析:语法门控、规划器语义与四阶段执行落地路线

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TiDB FULL OUTER JOIN 支持解析:语法门控、规划器语义与四阶段执行落地路线

TiDB FULL OUTER JOIN 支持解析:语法门控、规划器语义与四阶段执行落地路线

【免费下载链接】tidbTiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. No noisy neighbors. No infrastructure ceiling.项目地址: https://gitcode.com/GitHub_Trending/ti/tidb

本文以 docs/agents/executor/fullouter_join_dev_note.md 开发笔记为主体,讲述 TiDB 为 SQL 标准FULL OUTER JOIN制定的设计决策与分阶段实现计划:从仅能"识别语法、拒绝执行"开始,逐步打通 Volcano 规划器路径与 root HashJoin v1 执行器,最后再扩展至 TiFlash MPP shuffle join。读完本文,你将理解 TiDB 为何用特性开关门控该语法、为何刻意不为FULL JOIN引入简写、规划器如何保证"绝不静默退化为 inner join",以及每一步落地时用户可以观察到的行为变化。

一、目标与范围:为 FULL OUTER JOIN 建立可演进的分阶段路线

FULL OUTER JOIN(全外连接)会保留左右两侧全部输入:既输出匹配到的行,也输出左侧独有的行与右侧独有的行(未匹配侧以 NULL 补齐)。相比 TiDB 已支持的 left / right outer join,它需要同时处理两侧的 NULL 扩展,涉及语法、AST、规划器与执行器多个层面的联动变更,因此被设计为一个分四步落地的功能(对应跟踪 issue #69998):

  1. 增加语法、AST restore 输出与特性开关,执行仍保持"不支持";
  2. 增加 root 规划器(Volcano 路径)支持,执行仍保持"不支持";
  3. 增加 root HashJoin v1 执行器支持;
  4. 在 root 语义稳定后,再增加 TiFlash MPP shuffle join 支持。

首期目标(In Scope)

  • 支持FULL OUTER JOIN ... ON ...语法形式;
  • 走 Volcano planner 路径(非 Cascades planner);
  • 由 root HashJoin v1 完成执行;
  • 通过tidb_enable_full_outer_join特性开关门控,默认OFF

首期明确不做(Non-goals)

  • FULL OUTER JOIN ... USING (...)NATURAL FULL OUTER JOIN
  • Cascades planner 支持;
  • root IndexJoin、MergeJoin、HashJoin v2 支持;
  • TiFlash MPP broadcast join 支持;
  • 非 MPP 的 coprocessor join 下推。

二、关键设计决策解读

开发笔记用 Decision Log 记录了语法、特性开关、可空性、ON 条件处理与 Join Key 语义五个关键设计点,这些决策直接决定了后面每一步的落地方式。

2.1 语法决策:只引入 FULL OUTER JOIN,FULL JOIN 不设简写

设计上只引入FULL OUTER JOIN一种全连接语法,不把FULL JOIN作为简写。原因在于FULL在当前语法中是一个非保留关键字(unreserved keyword),可以被用作别名或标识符。若把FULL JOIN纳入全连接语义,会破坏与 MySQL 兼容的解析行为。例如下面的 SQL:

SELECT * FROM t1 full JOIN t2 ON t1.a = t2.a;

它应当继续被解析为t1 AS full JOIN t2(把full当作 t1 的别名),而不是一个 full outer join。

这一决策在词法层有对应的特殊处理。由于FULL既可以是关键字又能作表别名,若只依赖语法(grammar)规则,t1 full outer join t2会先被规约成t1 AS full,随后在OUTER处解析失败。为此 pkg/parser/lexer.go 在扫描到FULL时向后探测两个 token,若恰好是OUTER JOIN,就返回一个专用 tokenfullJoinType;反过来,刻意在 lexer 与语法层把FULL JOIN处理成全连接简写。

在 pkg/parser/parser.y 中可以看到fullJoinType对应的语法定义"FULL OUTER JOIN"。AST 侧的连接类型枚举位于 pkg/parser/ast/dml.go,其中定义了CrossJoin / LeftJoin / RightJoin / FullJoin四种类型。AST restore(把语法树还原为 SQL 文本)路径则负责输出FULL OUTER关键字,保证解析后再还原的 SQL 与原文等价——pkg/parser/parser_test.go 中就有还原一致性测试:

select * from t1 full outer join t2 on t1.a <=> t2.a -- 还原为 SELECT * FROM `t1` FULL OUTER JOIN `t2` ON `t1`.`a`<=>`t2`.`a`

2.2 特性门控:tidb_enable_full_outer_join 默认 OFF

全外连接会同时改变多个 join 路径上的规划器与执行器行为,因此在所有分阶段 PR 完成前,默认关闭可以让语法"先被认识、却不可被意外执行",避免 root 语义尚未完备时被误用。

在 pkg/sessionctx/vardef/tidb_vars.go 中注册了变量名tidb_enable_full_outer_join,其默认值在 同文件 中定义为false。会话变量定义位于 pkg/sessionctx/variable/sysvar.go:它是一个Global | Session双作用域布尔变量,SetSession回调把字符串值写入SessionVars.EnableFullOuterJoin(见 pkg/sessionctx/variable/session.go)。

对应地,pkg/sessionctx/variable/sysvar_test.go 覆盖了该变量的默认值、on/0/1三种赋值方式的读写测试。

2.3 禁止静默回退:PlanBuilder 快速失败

parser 识别出ast.FullJoin之后,不允许PlanBuilder中落入默认的 inner join 路径。在规划器与执行器尚未完全接通前,最安全的行为是快速失败,返回ErrNotSupportedYet("FULL OUTER JOIN")

当前仓库的 pkg/planner/core/logical_plan_builder.go 已经落地了这一 fail-fast 守卫,其检查顺序非常清晰地体现了笔记中的全部限制条件:

  • 特性开关未开启(EnableFullOuterJoin == false)→ 直接报不支持;
  • 开启 Cascades planner → 报"FULL OUTER JOIN with cascades planner"不支持(全连接仅支持 Volcano 路径);
  • NATURAL JOINUSING、缺少ON条件、以及 LATERAL 派生表场景 → 全部报不支持,避免这些形式在后续路径中被悄悄转换成 inner apply / inner join。

2.4 可空性(Nullability):两侧输出都视为可空

全外连接同时保留两侧输入,左右两边的子节点都可能产生 NULL 扩展行,因此两侧输出列都必须被当作 nullable。这与 left / right outer join 不同——后者只有单侧会被 NULL 扩展。规划器一旦漏掉这一点,NULL 扩展行上的谓词求值、投影与表达式推导都会出错。

2.5 ON 条件处理:一测谓词不允许无证明地下推

全外连接不能复用单侧 outer join 的所有假设:

  • EqualConditions视作 join key 条件;
  • 单侧ON谓词必须保留在 join 语义中,除非后续有规则能证明某个变换是安全的;
  • 依赖"单一保留侧"(single preserved side)的优化规则,需要显式加入全连接的守卫。

最核心的正确性风险是:把某个保留侧的谓词意外下推到 join 之下,在 NULL 扩展发生之前就过滤掉本该保留的行。

2.6 Join Key 语义:复用 = / <=> 与 IsNullEQ 机制

root HashJoin v1 应复用现有 join key 的 null-safe 等值比较逻辑,而不要为全连接引入一套全新的比较规则:

  • =不匹配 NULL join key;
  • <=>(null-safe equal)允许NULL <=> NULL匹配;
  • 现有IsNullEQ机制按 key 表达<=>行为。

只有当现有IsNullEQ处理被证明不够用时,才考虑引入仅全连接专用的比较规则。这一点在 parser 还原测试t1.a <=> t2.a中也有体现。

三、四步实现计划详解与用户可见行为

Step 1:语法与开关(Syntax and Gate)

笔记标注该步骤已在首个 PR 中完成,实现范围包括:

  • parser 增加FULL OUTER JOIN的 token/grammar 支持;
  • AST 增加 join 类型并支持 restore 输出;
  • 保持FULL JOIN的别名兼容解析,不作为全连接简写;
  • 新增tidb_enable_full_outer_join,默认OFF
  • PlanBuilder增加 fail-fast 守卫,使FULL OUTER JOIN返回ErrNotSupportedYet而非落入 inner join;
  • 补齐 parser、sysvar、planner-gate 测试。

Step 1 之后的用户可见行为:

  • parser 能识别FULL OUTER JOIN
  • FULL JOIN仍按别名兼容语法解析;
  • 执行FULL OUTER JOIN依旧返回 unsupported。

这里有一个容易被误解的细节:即便把开关设为ON,Step 1 阶段执行仍会返回ErrNotSupportedYet。这是刻意为之——Step 1 只是让语法可被识别,同时阻止它静默回退成别的 join 类型。

Step 2:root 规划器支持(Root Planner Support)

规划器侧的落地内容包括:

  • 增加逻辑连接类型FullOuterJoin。当前仓库中该类型已定义在 pkg/planner/core/base/plan_base.go,与CrossJoin / InnerJoin / LeftOuterJoin / RightOuterJoin / SemiJoin / AntiJoin并列,且IsOuterJoin一类判断会把全连接与单侧外连接归入同一族;
  • PlanBuilder仅在tidb_enable_full_outer_join=ON时构建全外连接(守卫代码见上文 2.3);
  • 修复全连接的输出可空性;
  • 守卫谓词下推、outer join simplification、join reorder、runtime filter 与物理计划枚举等规则;
  • 物理计划范围限制为 root HashJoin v1;
  • 增加临时 executor 构建守卫,使正常执行仍返回ErrNotSupportedYet("FULL OUTER JOIN")
  • 增加逻辑/物理计划层面的 planner 测试。

为什么 EXPLAIN 也需要走 planner 全路径?笔记明确指出:TiDB 的 explain executor 为了获取 partition pruning 元数据,仍会构建目标 executor。因此在 Step 2,用户对FULL OUTER JOIN ... ON ...执行EXPLAIN 同样返回 unsupported,直到 Step 3 才移除 HashJoin v1 executor 的守卫。

从当前仓库源码与测试的完成度看,Step 2 的规划器语义已不只是"计划中"。逻辑连接类型FullOuterJoin已经存在,全连接行数估算在 pkg/planner/cardinality/join.go 有EstimateFullJoinRowCount入口,物理计划枚举在 pkg/planner/core/exhaust_physical_plans.go 也已有FullOuterJoin分支。更为系统的证据是 pkg/planner/core/casetest/fulljoin/full_join_test.go,其中已包含大量规划器行为测试,例如:

  • 特性开关默认关闭(TestFullOuterJoinFeatureSwitchDefaultOff);
  • 逻辑计划正确构建为FullOuterJoinTestFullOuterJoinLogicalBuild);
  • USINGNATURAL等不支持形式快速失败(TestFullOuterJoinUnsupportedFormsFailFast);
  • Cascades 路径快速失败(TestFullOuterJoinCascadesFailFast);
  • 物理计划仅落到 HashJoin(TestFullOuterJoinPhysicalPlanHashJoinOnly);
  • 使用非 HashJoin 的 join method hint 时给出警告(TestFullOuterJoinUnsupportedJoinMethodHintsWarn);
  • outer join simplification 与 join reorder 不会破坏全连接语义(TestFullOuterJoinSimplifyOuterJoinTestFullOuterJoinSkipJoinReOrder)。

Step 2 之后的用户可见行为:

  • 开启特性开关后,planner 测试可以产出 root 全外连接计划;
  • 执行或 EXPLAINFULL OUTER JOIN ... ON ...仍返回 unsupported;
  • 不支持的形式与不支持的 planner 路径仍然快速失败。

Step 3:root HashJoin v1 执行器

第一个可执行的实现应只做 root HashJoin v1,把首个语义实现聚焦在正确性上,需要逐一覆盖:

  • 匹配行(matched rows);
  • 仅左侧未匹配行(left-only unmatched rows);
  • 仅右侧未匹配行(right-only unmatched rows);
  • 单侧ON过滤;
  • =<=>两种 join key;
  • 落盘行为(spill)。

本步骤的落地内容还包括:移除 Step 2 的临时 executor 构建守卫;补充 executor 与集成测试。相关执行器代码位于 pkg/executor/join/hash_join_v1.go。HashJoin v1 的可空等值处理沿用现有IsNullEQ机制(即 2.6 节所述),不需要新增仅全连接专用的 join-key 比较规则。

至于 root IndexJoin、MergeJoin、HashJoin v2,必须等它们被单独设计与测试之后才能放开,不会与全连接同时引入。

Step 3 之后的用户可见行为:

  • 开启特性开关后,FULL OUTER JOIN ... ON ...在 root HashJoin v1 上可以正常工作;
  • 不支持的形式与不支持的执行路径仍然快速失败。

Step 4:TiFlash MPP shuffle join

TiFlash MPP 支持应放在 root 语义稳定之后作为后续工作。首个 TiFlash 范围被限定为shuffle HashJoin

  • 允许全外连接的 MPP shuffle join;
  • 不允许全外连接的 MPP broadcast join;
  • 不在全外连接之上广告单侧 hash 分区属性(需要安全的输出分区属性);
  • <=>/NullEQjoin-key 下推保持不支持,留作单独的兼容性跟进项。

落地内容包括:在 TiPB(TiFlash 通信协议)中编码 full outer join 类型、为全外连接选择安全的输出分区属性、补充 MPP planner 测试。

Step 4 之后的用户可见行为:

  • 条件满足时,全外连接可以被规划为 TiFlash MPP shuffle join;
  • broadcast join 与NullEQjoin-key 下推仍作为独立的后续项跟进。

四、进度追踪与规划器测试现状

笔记中的进度追踪表完整记录如下:

StepScopeStatus
Step 1Parser、AST restore、sysvar 门控、planner fail-fast 守卫Done
Step 2root 规划器语义与临时 executor unsupported 守卫Planned
Step 3root HashJoin v1 执行器支持Planned
Step 4TiFlash MPP shuffle full outer join 下推Planned

结合当前仓库实际可见的代码与测试,可以对上表做一处重要补充:Step 2(规划器侧)的大部分工作已在仓库中落地——FullOuterJoin逻辑连接类型、PlanBuilder门控与限制检查、行数估算、物理计划枚举分支,以及 pkg/planner/core/casetest/fulljoin/full_join_test.go 中成体系的 planner 测试均已存在;而笔记标注的 Step 3 executor 行为与 Step 4 TiFlash MPP 下推,按笔记记录仍处于后续计划状态。若读者想验证"默认 OFF + 全部 fail-fast"这些不变量,可直接运行pkg/planner/core/casetest/fulljoin目录下的规划器测试。

五、评审清单:合入前必须守住的不变量

笔记在末尾给出了供 reviewer 使用的检查清单,可以作为任何全外连接相关 PR 的验收标准:

  • FULL JOIN没有变成 full outer join 的简写;
  • FULL OUTER JOIN从不静默降级为 inner join;
  • 特性开关默认值为OFF
  • 全连接语义启用后,两侧输入都被视为可空;
  • 单侧ON谓词不会被不安全地推到 join 之下;
  • root 执行从 HashJoin v1 开始;
  • 未显式实现前,不支持路径一律快速失败;
  • TiFlash MPP full outer join 只在 root 行为被充分覆盖后才引入。

六、开发者如何本地验证

当前仓库中该功能仍受特性开关门控,若要自行验证解析与规划器行为,需要注意限制条件:开关默认关闭,且 Step 3 执行器落地前即使开启也会得到 unsupported 错误。

开启特性的方式(按会话或全局):

SET SESSION tidb_enable_full_outer_join = ON; -- 或 SET GLOBAL tidb_enable_full_outer_join = ON;

关闭与状态查询:

SET SESSION tidb_enable_full_outer_join = OFF; SHOW VARIABLES LIKE 'tidb_enable_full_outer_join';

可复现的语法检查用例(对应 pkg/parser/parser_test.go):

SELECT * FROM t1 FULL OUTER JOIN t2 ON t1.a <=> t2.a;

而下面这条 SQL 由于FULL被当作别名,不会构成全外连接:

SELECT * FROM t1 FULL JOIN t2 ON t1.a = t2.a; -- 解析为 t1 AS full JOIN t2

需要提醒的是:在仓库当前的演进状态下,对FULL OUTER JOIN ... ON ...的实际执行与 EXPLAIN 仍受笔记中分阶段计划的约束,最终可执行能力以对应 step 是否落地为准。

七、延伸阅读

  • 开发笔记原文
  • 语法与词法:pkg/parser/lexer.go、pkg/parser/parser.y、pkg/parser/ast/dml.go
  • 特性开关:pkg/sessionctx/variable/sysvar.go、pkg/sessionctx/vardef/tidb_vars.go、pkg/sessionctx/variable/session.go
  • 规划器守卫与连接类型:pkg/planner/core/logical_plan_builder.go、pkg/planner/core/base/plan_base.go
  • 规划器测试:pkg/planner/core/casetest/fulljoin/full_join_test.go
  • 执行器(Step 3 相关):pkg/executor/join/hash_join_v1.go

【免费下载链接】tidbTiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. No noisy neighbors. No infrastructure ceiling.项目地址: https://gitcode.com/GitHub_Trending/ti/tidb

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 20:44:31

Laravel接口绑定与依赖注入实战:从原理到应用与排查

做 Laravel 开发这几年&#xff0c;我在不少项目里见过同一种代码&#xff1a;控制器里直接 new UserService() &#xff0c;Repository 写死在类里面&#xff0c;等哪一天想把存储层从 MySQL 换成缓存&#xff0c;或者想把支付渠道从微信换成支付宝&#xff0c;才发现改文件…

作者头像 李华
网站建设 2026/9/10 20:40:24

百考通得力助手:AI赋能期刊论文写作

在学术研究领域&#xff0c;期刊论文的撰写是成果输出的关键环节&#xff0c;却也让众多科研工作者与学生倍感压力&#xff1a;选题迷茫、逻辑梳理困难、格式规范复杂、内容提炼耗时&#xff0c;严重拖慢了学术成果的发表节奏。百考通&#xff08;https://www.baikaotongai.com…

作者头像 李华
网站建设 2026/9/10 20:39:57

数据资产入表:现状、痛点与选型指南

1. 数据资产入表的行业现状与核心痛点 当前企业数据资产管理正面临从粗放式向精细化转型的关键阶段。根据Gartner最新调研显示&#xff0c;超过78%的CIO将"数据资产价值量化"列为年度战略优先级&#xff0c;但实际落地过程中普遍存在三大典型困境&#xff1a; 首先是…

作者头像 李华
网站建设 2026/9/10 20:35:26

CANN/GE ACL获取输出名称API

aclmdlGetOutputNameByIndex 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch…

作者头像 李华