引言:复杂 SQL 里最容易被忽略的那一层
企业里的 SQL 很少还停留在单表查询。CTE、嵌套子查询、窗口函数、聚合,这些写法把业务逻辑讲清楚了,但也顺手把优化器逼到了角落里。最常见的坑,就是外层连接上的高选择性条件没法尽早进入子查询,结果中间结果集先膨胀一轮,后面的执行再怎么补救都很难看。
这类问题不是'SQL 写得复杂'这么简单,本质上是过滤发生得太晚。金仓数据库在 V009R002C014 里做的连接条件下推,就是专门冲着这个点去的:先保证语义不变,再判断值不值得推,避免那种看起来聪明、实际把计划搞慢的改写。
为什么高选择性条件经常推不进去
看一个很典型的例子:
SELECT * FROM (
SELECT DISTINCT s1.a, s1.b FROM s1
) s
JOIN s2 ON s.s1a = s2.s2a
WHERE s2.b = 3;
从业务角度,这个 SQL 没什么问题:先把 s1 去重,再和 s2 连接,最后筛掉 s2.b = 3 之外的数据。逻辑是顺的,写的人也通常觉得没毛病。
但执行时就不一定了。子查询 s 还是要先对 s1 做全表扫描和去重,外层的过滤条件却卡在后面,没机会提前缩小数据量。于是中间结果先做大,连接再去处理它,代价自然就上来了。
真正的问题不在连接,而在过滤太靠后。
这件事难就难在两层
先看能不能推
不是所有连接条件都能安全下推。只要子查询里有 GROUP BY、窗口函数、DISTINCT、UNION 这类操作,谓词的位置一变,语义就可能跟着变。再碰上非确定性函数或者带副作用的表达式,事情会更麻烦。
所以第一步不是'想办法推',而是先判断语义是否等价。推下去以后结果必须和原 SQL 一样,这条线不能越。
再看值不值得推
就算语义没问题,也不代表一定赚。
下推之后,外层条件可能把子查询改成参数化执行。这个模式在驱动表小的时候很香,但如果外层基数很大,子查询就会被反复执行很多次,累计成本可能比原来那种一次性全表扫描还高。看上去做了优化,实际上把性能换了个地方亏掉。
所以这一步必须算代价,不能只靠规则拍板。
传统优化器为什么容易卡住
很多传统优化器遇到这类 SQL 时,通常会走一条很保守的路:先完整执行子查询,生成中间结果,再和外层表连接,最后才应用过滤条件。
这套策略的问题很直接:外层的高选择性条件没法反过来影响子查询的扫描阶段。只要子查询本身重一点、数据多一点,执行计划就容易顶在中间结果上,后面怎么接都不顺。
金仓的做法:先等价,再算账
V009R002C014 里的连接条件下推,不是单纯加了一条规则,而是拆成了两步。
第一步:等价性判定
优化器先分析子查询结构,确认谓词能不能安全往里放。这里会检查子查询里有没有聚合、窗口、集合操作之类的敏感点,也会把连接谓词拆成两部分:一部分依赖外层列,适合做参数化;另一部分留在子查询内部。
只有通过这个检查的谓词,才会被改写成参数化过滤条件,塞进子查询的扫描或过滤阶段。这个阶段的目标很朴素:别改错结果。
第二步:代价模型评估
等价没问题之后,也不会立刻下推。优化器还会比较下推前后的代价,看看扫描行数、中间结果规模、参数化执行的重复开销到底是什么量级。
如果算下来不划算,或者有回退风险,优化器会直接放弃下推,继续走原计划。这个判断其实挺务实的:不是所有能优化的地方都值得优化。


