优化器偷偷在“推理“:那个DISTINCT根本不用去重
文章目录

从一条让人血压飙升的SQL说起
上周群里有个哥们甩了条SQL出来问为啥慢,我一看——SELECT DISTINCT a, b FROM s1 WHERE a=1 AND b=1。a和b都被WHERE钉死了,你就俩固定值,DISTINCT个啥呢?去重个寂寞啊。但数据库你猜怎么着,还真就老老实实全表扫一遍排序去重,跑了几十毫秒才出来。我当时就寻思,优化器要是能自己推导出来"a和b已经被固定成常量了结果最多一条",直接LIMIT 1返回不就完了?0.03毫秒的事。
后来我翻了翻KES的更新日志——说实话翻日志这事我一般懒得干,那天闲的无聊——发现V9R4C19还真就把这活给干了。两层优化,第一层DISTINCT改GROUP BY,第二层更狠,目标列被常值固定的话直接用LIMIT 1替代去重。我测了一下,30毫秒变0.03毫秒。一千倍。这种提升你靠什么索引优化啊调参啊根本做不到,纯粹是优化器在"想"。
今天就掰扯掰扯这事。不光聊KES怎么做的,更想聊聊这背后优化器从"机械执行"到"逻辑推理"的演变。看着是个小功能,但折射出来的方向我觉得挺大的。
DISTINCT这玩意儿怎么就这么招人烦
DISTINCT谁不会用啊,去重嘛,SQL入门就学了。问题不在语法上,在于数据库处理DISTINCT的方式太……怎么说呢,太"实在"了,或者说太傻了。
SELECT DISTINCT a, b FROM s1;
你写这么一句,数据库干了啥?全表扫描s1,把所有行捞出来,排序,然后去重。数据量小的时候你感觉不到啥,数据量一大排序本身就是性能杀手——排序的时间复杂度O(N log N),空间复杂度也不低,中间结果还得写临时段。
我之前在一个项目里碰到过更离谱的。一个报表SQL,DISTINCT嵌了三层子查询,最外层DISTINCT的列在最里层WHERE里已经被等值条件固定了,但数据库还是傻乎乎地全量去重。当时跟同事吐槽说,这要是优化器能自己推导出来"结果唯一"就好了。同事说做梦吧,优化器哪有这脑子。没想到KES还真做了。有时候你还真别说,梦想还是要有的。
KES那套"先稳后狠"的两层刀法
KES对DISTINCT的优化分两层,我管它叫"先稳后狠"。第一层是稳妥的等价改写,第二层是激进的推理消除。
第一层:DISTINCT变GROUP BY,看着不起眼但真管用
这层听着平平无奇但实际很实用。DISTINCT和GROUP BY在语义上本来就等价嘛——SELECT DISTINCT a, b跟SELECT a, b ... GROUP BY a, b是一个意思。但GROUP BY有DISTINCT没有的好处:它后面能接上已有的键值消除和并行处理能力。这个差别看着小,实际跑起来差别大了去了。
-- 你写的
SELECT DISTINCT a, b FROM s1;
-- KES内部给你改成
SELECT a, b FROM s1 GROUP BY a, b;
改成GROUP BY之后有啥好处呢?如果a或b恰好有主键约束或者唯一索引,KES的分组消除机制就派上用场了——分组键包含主键的话每组天然就一行,去重直接省掉了,连分组操作本身都不用做。而且GROUP BY还能走并行执行,DISTINCT在很多数据库里是不支持并行去重的,你数据量大的时候这一条就很要命。
就这一层改写,我实测464毫秒降到249毫秒,差不多砍半。听着没一千倍那么夸张,但胜在通用性强——基本上所有带DISTINCT的查询都能走这层,不挑场景。
-- 开启DISTINCT优化
SET kdb_rbo.rbo_rule = on;
SET kdb_rbo.enable_distinct_optimization = on;
嗯对,这个功能也是通过kdb_rbo系列参数控制的,跟标量子查询消除一个套路。默认关闭,得手动开。我一开始还挺纳闷为啥默认关,后来想了想也能理解——新功能嘛,先让用户自己试确认没问题再全局开,稳妥。
第二层:LIMIT 1替代去重——这是真正的狠活
这层才是我今天最想聊的。当目标列被常值固定的时候,结果集最多就一条,去重完全没必要,直接LIMIT 1就完事了。
-- 你写的
SELECT DISTINCT a, b FROM s1 WHERE a=1 AND b=1;
-- KES内部给你改成
SELECT a, b FROM s1 WHERE a=1 AND b=1 LIMIT 1;
谓词传递稍微绕一点,我得举个例子。INNER JOIN的等值条件ON s1.a = s2.b AND s1.a = 5,已知s1.a等于5,而s1.a又等于s2.b,那s2.b也必然等于5——这就是谓词沿着等值链在表之间"传递"。你把这条链理清楚,优化器就能推导出s2.b也被固定了。
把这两个合起来看,优化器实际上在干一件事——构建一棵逻辑表达式树,然后沿着树做推导。a=1推导出a被固定 → b=1推导出b被固定 → 目标列{a,b}全被固定 → 结果最多一条 → 不需要去重 → LIMIT 1替代。这整条推理链一步一步都是确定性的逻辑推导,不涉及任何概率估算。
我第一次意识到这个的时候……怎么说呢,有点被震撼到。因为传统优化器给人的感觉就是"你写啥我执行啥",顶多在物理层面选选算法——哈希连接还是嵌套循环,走索引还是全表扫描。但KES这个DISTINCT优化是在逻辑层面做了推理——它不是在选算法,是在判断"这个操作本身有没有必要做"。这是质的区别。
那个"逻辑表达式树"到底在怎么推
我试着把KES的推理过程画出来,大概长这样。
你给优化器一条SQL:SELECT DISTINCT a, b FROM s1 WHERE a=1 AND b=1
优化器拿到之后先构建查询的逻辑表达式树。WHERE条件被解析成一组谓词:{a=1, b=1}。然后优化器遍历SELECT的目标列{a, b},对每个目标列检查:有没有谓词把它固定成常量?
- 检查a:WHERE里有a=1,常量传递,a被固定为1 ✓
- 检查b:WHERE里有b=1,常量传递,b被固定为1 ✓
- 所有目标列都被固定 → 结果最多一条 → DISTINCT可以替换为LIMIT 1
INNER JOIN场景多一步谓词传递:
SELECT s1.a, s2.b FROM s1
INNER JOIN s2 ON s1.a = s2.b AND s1.a = 5
GROUP BY s1.a, s2.b;
- s1.a=5(WHERE直接约束)
- s1.a = s2.b(JOIN等值条件)
- 谓词传递:s2.b = 5
- 目标列{s1.a, s2.b}都被固定为5 → LIMIT 1
你看这个推理链,每一步都是确定性的逻辑推导,不涉及任何概率估算和统计信息。这就是它比CBO代价模型更可靠的地方——代价模型依赖统计信息,统计信息可能过期可能不准,但a=1这个事实永远成立,不会有"过期"这回事。
当然KES目前能处理的场景还是比较基础的——直接等值条件、INNER JOIN等值推导,这些是比较规整的模式。如果常值是通过函数间接得到的呢?比如WHERE UPPER(name) = 'TOM',优化器能不能推导出name被固定了?理论上UPPER(name)='TOM’能推出name=‘Tom’(假设大小写唯一映射),但这个推导涉及函数语义理解,目前KES还没做到。再比如跨多层子查询的常量传递,或者通过CASE表达式推导的等价条件,或者EXISTS子查询隐含的唯一性约束,这些都更复杂。现阶段KES只能处理最直接的等值链推导,更深层的语义推理还有很大空间。但方向是对的——从"机械翻译"到"逻辑推理"再到未来的"语义理解",这条路虽然长但是方向清晰。
实操的时候几个得注意的事
效果确实好,但用起来有些门道。
这功能默认关闭的,得手动开。跟标量子查询消除一样走kdb_rbo参数。我建议先session级别试试,确认效果再考虑全局开。别上来就改kingbase.conf,出了问题回滚都麻烦。
-- 先session级别试试水
SET kdb_rbo.rbo_rule = on;
SET kdb_rbo.enable_distinct_optimization = on;
第一层DISTINCT转GROUP BY基本没什么风险,语义完全等价,改完跑出来结果一模一样。但第二层LIMIT 1那个有个前提——目标列必须被"完全"固定。啥叫完全?就是你SELECT里出现的每个列都得有等值条件把它钉死。有一个没钉住就不能用LIMIT 1,因为结果可能不止一条,你LIMIT 1就把合法的结果截掉了。这个……嗯,出问题的话不好排查,因为不会报错只是结果少了。
还有一个容易忽略的点:常量不一定只来自WHERE的直接约束,还可能来自INNER JOIN的等值推导。KES能处理INNER JOIN的情况,但LEFT JOIN不行——左连接右表可能没有匹配行导致NULL补位,等值链不成立。这个一定要搞清楚,不然你写了个LEFT JOIN还指望优化器帮你推导常量,结果它跳过了你还纳闷为啥没生效。
另外DISTINCT消除(distinct elimination)是另一个不同的优化——当去重列本身就有唯一性约束的时候直接省掉去重操作。这跟DISTINCT转GROUP BY不是一回事,别搞混了。DISTINCT消除靠的是约束信息,DISTINCT转GROUP BY靠的是语义等价改写,LIMIT 1替代靠的是常量推导——三个层次逻辑递进,一个比一个激进。
我看有人拿DM数据库做了对比,同样SELECT DISTINCT a,b FROM t WHERE a=1 AND b=1这种SQL,DM V8还是老老实实走DISTINCT处理不支持转LIMIT 1。说实话这个对比嘛……也不能说明太多,不同数据库优化策略侧重点本来就不一样,DM可能在别的方向有它的优势。但至少说明KES在"基于逻辑推理的冗余消除"这条路上确实走在前面一些。
优化器在"思考",不只是"计算"
写到这我想说点虚的但是我觉得挺重要的话。
KES的DISTINCT优化看着是个小功能——不就是把DISTINCT改成GROUP BY或者LIMIT 1嘛有什么大不了的。但你仔细想它的推理过程:解析WHERE条件 → 构建等值链 → 常量传递 → 谓词传递 → 判断目标列固定性 → 决定能否省略去重操作。这每一步都是逻辑推导,不是代价计算。不是"算一算哪个方案便宜",而是"推一推这个操作有没有必要"。
传统优化器是"计算器"——你给它算式它帮你算得又快又准。推理型优化器更像是"分析师"——它会先看看你这个算式本身合不合理,有没有冗余步骤可以直接删掉。删掉一个不需要的操作,比把这个操作优化到极致,收益大得多。
数据库优化器的演进,从RBO到CBO再到ABO,这已经是大趋势了没人会否认。但我觉得在完全走向ABO之前,有一个被低估的中间阶段——基于逻辑推理的优化。它不像ABO那样需要训练数据和模型——学习型优化器的冷启动问题到现在也没解决,你部署一套新系统哪来的历史负载?也不像CBO那样依赖可能不准确的统计信息——我见过太多ANALYZE忘了跑导致优化器选错计划的case了。逻辑推理靠的是确定性的推导,a=1就是1,不需要采样不需要估算不需要训练。
当然逻辑推理的覆盖范围有限,复杂场景推理链太长优化器处理不了。但在它能覆盖的简单场景里,效果是爆炸性的——30毫秒变0.03毫秒这种提升你靠调参调索引根本做不到。我觉得未来的优化器应该是逻辑推理和代价估算的组合——先用逻辑推理消除确定性的冗余操作,再用代价模型在剩下的方案里选最优。KES的DISTINCT优化就是前者的一个缩影。它告诉我们一个道理:有时候优化不是要算得更快,而是要想清楚到底需不需要算。
折腾了挺久才把这玩意儿搞明白的。搞明白之后再去看优化器的执行计划,感觉就不一样了——不是"这个快那个慢"的层面了,而是能从逻辑推理的角度去理解优化器为什么做了这个选择。这个视角挺有意思的,建议大家也试试。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)