当JOIN条件"下潜"到子查询深处:一场关于查询优化器代价决策的深度实践

1 阅读7分钟

> 在复杂SQL的迷宫里,让过滤条件在正确的时机、正确的位置生效,是查询优化器最艰难也最有价值的艺术。


01 被忽视的"性能杀手":为什么你的复杂SQL越跑越慢?

业务系统的SQL正在变得越来越"聪明"——CTE封装逻辑、子查询分层计算、窗口函数实时分析,这些特性让代码可读性飙升。但当我们把视角切换到执行引擎,一幅截然不同的画面浮现:

[外层表]  JOIN  [复杂子查询(去重+聚集+窗口函数)]
↑              ↑
高选择性条件      全量扫描+重计算
(仅需100行)      (产出100万行)

问题的本质不是JOIN太慢,而是过滤发生得太晚。

在真实生产环境中,这类SQL模式屡见不鲜:

  • 子查询内部完成海量数据的DISTINCT去重
  • 外层施加的精准过滤条件无法穿透子查询边界
  • 优化器被迫先计算"全集",再截取"子集"
  • 中间结果集像滚雪球般膨胀,最终压垮执行链路

这不是开发者的错,而是传统优化器在"复杂查询边界"上的能力盲区。


在这里插入图片描述

02 下推优化:看起来简单,做起来"步步惊心"

直观的解决方案似乎触手可及:把外层的JOIN条件"塞"进子查询,提前过滤不就好了?

但查询优化器的工程师都知道,这条路布满暗礁:

2.1 语义安全的红线:结果不能变

不是所有条件都能随便搬家。当子查询包含以下元素时,鲁莽的下推可能彻底改变SQL语义:

危险区域风险说明
GROUP BY聚集提前过滤可能改变分组维度与聚合结果
窗口函数行级过滤会破坏OVER子句的窗口定义
DISTINCT/UNION集合运算的语义边界可能被击穿
非确定性函数多次求值可能产生不一致结果

"能推"是技术,"推了结果不变"才是工程。

2.2 代价的悖论:快与慢的辩证法

即便语义安全,下推也未必带来收益:

  • 重复执行陷阱:外层1000行数据下推,可能导致子查询被重复执行1000次
  • 参数化开销:绑定变量、计划缓存、执行缓存的额外成本
  • 选择性误判:估计的过滤率与实际严重偏离

没有代价评估的下推,就像蒙眼开车——可能加速,也可能坠入悬崖。


03 金仓数据库的"双保险"策略:先问能不能,再问值不值

在最新版本内核中,我们构建了一套**"等价性判定+代价模型"**的双重决策机制,让连接条件下推从"经验主义"走向"科学决策"。 在这里插入图片描述

3.1 第一关:等价性守门员

优化器首先扮演"语义安检员"的角色:

  1. 结构扫描:解析子查询的算子组成,识别危险区域
  2. 依赖分析:将JOIN条件拆解为"外层列引用"与"内层列引用"
  3. 安全判定:仅当条件移动不改变逻辑语义时,才标记为"可参数化"

通过安检的条件,会被改写为参数化过滤谓词,像特洛伊木马般潜入子查询内部,在扫描阶段即生效。 在这里插入图片描述

3.2 第二关:代价精算师

通过语义安检只是拿到"参赛资格",真正的决策交给代价模型:

IF (下推后总代价 < 下推前总代价) THEN
选择下推执行路径
ELSE
保持原执行计划
END IF

评估维度包括:

  • 子查询扫描行数的减少幅度
  • 中间结果集的压缩比例
  • 参数化执行带来的重复计算开销
  • 内存、I/O、CPU的综合消耗

关键洞察:代价模型不是追求"局部最优",而是确保"全局最优"。当检测到外层基数过大、可能导致子查询被频繁重复执行时,优化器会果断放弃下推,避免"优化反噬"。


04 实战检验:从毫秒到微秒的跨越

在这里插入图片描述

4.1 基准测试:去重场景的质变

SQL模式外层表 JOIN (SELECT DISTINCT * FROM 大表)

执行策略执行时间核心特征
传统执行84ms子查询全表扫描+去重,产生庞大中间结果
代价下推0.14ms条件提前过滤,扫描即裁剪,去重数据量锐减

600倍性能提升的背后,是中间结果集从"百万级"到"百级"的断崖式下跌。

对比测试(某商业数据库,不支持下推):执行时间1.62ms,验证了下推机制的技术必要性。

4.2 极限挑战:多层嵌套的"地狱级"SQL

测试用例融合了OLAP场景的典型复杂度:

  • UNION ALL合并多个DISTINCT子查询
  • 窗口函数实时计算分组聚合
  • 多层子查询嵌套引用
  • 多表JOIN串联

执行计划对比

维度未下推代价下推
扫描策略各子查询独立全表扫描条件下潜,选择性扫描
中间结果多个百万级结果集千级裁剪后数据
执行时间1081ms0.23ms
性能提升基准4700倍

深度解析

  • 未下推时,优化器被迫先执行内部UNION去重,产生结果集A;再与外层表JOIN产生B;同时右侧子查询窗口函数全量计算产生C,再JOIN得D;最终B与D大表JOIN。
  • 下推后,JOIN条件化作"探照灯",在子查询扫描阶段即精准定位目标数据,后续所有操作都在"瘦身"后的数据集上进行。

在这里插入图片描述

05 技术启示:查询优化的未来方向

连接条件下推的实践,折射出查询优化器演进的深层逻辑:

5.1 从规则到代价的范式转移

早期优化器依赖"启发式规则"(如:条件下推总是好的),但在复杂场景下往往失效。现代优化器必须是代价驱动的——没有放之四海而皆优的规则,只有特定上下文下的最优决策。

5.2 从局部到全局的系统思维

单点优化容易陷入"只见树木不见森林"。金仓的方案强调:

  • 等价性保障是底线(正确性优先)
  • 代价评估是准绳(性能导向)
  • 全局最优是目标(拒绝局部陷阱)

5.3 面向混合负载的适应性

随着HTAP(混合事务/分析处理)架构普及,查询优化器需要同时应对:

  • OLTP的点查短事务(低延迟敏感)
  • OLAP的复杂分析(高吞吐优先)
  • 两者混杂的不可预测负载

基于代价的连接条件下推,正是适应这种复杂性的关键能力——让优化器拥有"审时度势"的智慧


结语:在正确的时间,做正确的过滤

查询优化的终极追求,可以用一句话概括:让数据在流动中被尽早裁剪,让计算在必要时才发生。

金仓数据库基于代价的连接条件下推机制,不是简单的技术特性,而是一种工程哲学的体现:

  • 尊重SQL语义的严谨性
  • 相信代价模型的判断力
  • 追求全局最优的执行力

当JOIN条件成功"下潜"到子查询深处,我们看到的不仅是执行时间的数量级下降,更是国产数据库在查询优化领域**从"能用"到"好用"、从"跟随"到"引领"**的技术跨越。

对于深陷复杂SQL性能困境的DBA和开发者而言,这意味着:你可以继续用优雅的CTE和子查询组织业务逻辑,而把"何时过滤、如何过滤"的难题,交给一个足够聪明的优化器。