资讯动态

“先判定后评估”:数据库执行计划生成的务实解法

发布时间:2026/10/22 10:40:19 来源:尧图企业网站定制
做数据库优化器的人心里都清楚执行计划生成这件事理论上很美实际上很苦。传统成本优化CBO的思路是枚举所有可能的执行路径代入代价模型算一遍挑最便宜的那条。听上去完美可一旦表多、连接条件复杂、数据分布不均匀候选计划的数量就会爆炸式增长优化器自己算计划的时间甚至可能超过SQL真正执行的时间这就本末倒置了。金仓数据库这套“先判定后评估”的框架正是冲着这个痛点来的。它的核心思路是把优化过程拆成两段先用规则做快速裁定把明显不靠谱的路径直接砍掉只留少数几条有竞争力的候选计划再对这些候选做精确的代价评估。说得直白些原来是一百个方案全部精算一遍取最优现在先靠经验筛掉九十个剩下的十个精算一遍又快又准。这篇文章我会从设计思路、判定层细节、评估层配合、实操验证和踩坑经验几个维度把这套框架真正讲透适合数据库内核开发、DBA、以及所有关心查询性能的研发同学参考。1. 先理解痛点为什么“全量枚举精算”这条路快到头了1.1 优化器开销的放大器连接重排与分布式数据移动传统优化器的问题不是它算得不准而是它算的东西太多。我们拿一个最经典的场景说五张表做连接优化器要评估的连接顺序就有120种5!每种顺序下还要考虑连接算法选择、是否下推过滤条件、是否构建中间结果物化、是否选择索引覆盖路径排列组合一下候选计划的数量根本不是线性增长直接指数级暴涨。如果是单机数据库情况还稍微可控因为机器就那么大能走的路径有限。但到了分布式数据库里问题会更加复杂表可能分布在十几个节点上一个join可能涉及数据重分布到底是broadcast小表还是shuffle大表重分布采用哪个分布键聚合是否要两阶段完成这些全部乘进来候选计划的数量能到几万甚至几十万。优化器如果对每个候选都跑一遍代价模型那一次查询的优化耗时很可能冲到几百毫秒甚至几秒。我见过不少真实案例优化一个看起来并不复杂的多表joinEXPLAIN出计划的时间比查询本身跑完的时间还要长。更尴尬的是如果统计信息不太准精算出来的“最优计划”和真实最优计划差得很远那前面的枚举和评估就全白干了典型的“算了个寂寞”。1.2 “先判定后评估”的本质规则圈地代价精算“先判定后评估”框架要解决的问题就是这种优化器自身的效率滑坡。它不再是上来就枚举而是先走一个“判定层”用一组明确的规则对候选计划做快速筛选。这里的判定不是算代价而是做逻辑裁定数据本地性判定两张表的连接键和它们的分布键一致那么就优先走本地连接不需要重分布过滤条件下推可行性判定where条件里的字段能否下推到更底层的表扫描或join之前执行索引匹配判定当前谓词形态能不能匹配到现有索引的最左前缀语义等价重写判定子查询能否改写成半连接或聚合下推能否去掉多余的distinct中间结果裁剪判定某些中间结果集是否有继续参与后续计算的必要。每一条判定命中就对候选集做一次裁剪。规则全部跑完剩下的候选集规模通常会缩到个位数到十几个。这时候再用代价模型做精确评估选一个真正性价比最高的计划。用大白话讲前者是通过经验判断“哪些路根本不用走”后者是在为数不多的几条合格路里选“哪条最快”定位完全不同。1.3 与RBO、CBO的差异不是回归是融合很多人听到用规则裁剪第一反应是“这不就是老掉牙的RBO规则优化吗”。还真不是。传统RBO最大的问题是规则是死的它推导出的计划永远长一个样完全不管数据规模、统计信息、实例负载最后经常产生“看着合理但跑得贼慢”的计划。而“先判定后评估”里的判定层只是用来圈定候选空间最终选择权仍是落在代价评估上。RBO是裁判直接给冠军这个框架是裁判先划出有资格进场的人冠军还是要通过比赛数据打分打出来的。这就形成了一个非常有意思的设计规则负责砍掉“绝对不值”的路径代价评估负责在“都还不错”的路径里找最优。两者的分工天然互补既规避了RBO机械式决策的僵硬又避免了CBO在巨大枚举空间里浪费计算资源的问题。可以说这个框架是数据库优化器设计里“规则成本”融合路线中做得比较彻底和务实的一种解法。2. 判定层怎么设计规则裁定是省时间的精髓所在2.1 判定规则从哪来不是拍脑袋是挖出来的判定层里跑的规则不是拿个SQL语法就随便写几条也不是靠某个人拍脑门想出来的。它通常来自两个渠道一个是对高频SQL模板的特征提取另一个是对历史慢SQL的分析反推。我在实际接触这类框架时有一个体会判定规则的来源一定要贴近真实负载。比如某个业务系统里所有高频查询都是单表点查那判定规则里关于“索引点查优先”的优先级就一定要顶到最高让这类查询在执行前就能直接命中唯一路径。反过来如果业务里大多数是分析型聚合查询规则的侧重点就应该放在“聚合下推”“分区裁剪”“统计信息辅助的并行度判断”上。规则设计得越贴近真实分布判定层的裁剪效率就越高。一套完整的判定规则通常分成几个层次基础规则语法合法性和语义等价重写、结构规则连接顺序的可行性排序、物理规则分布式移动成本、索引可用性、资源规则内存、并行度允许范围。层次越靠前判定成本越低裁剪效果越粗越靠后判定成本越高但留下的候选和最终可取计划的差距越小。这种分层能保证大多数简单查询在很浅的判定层就完成裁剪不会浪费时间。2.2 判定规则的优先序和终止策略判定层最怕的不是规则太少而是规则太多导致判定本身成了新瓶颈。所以成熟的实现里一定会考虑“判定顺序”和“提前终止”。我的经验和通常做法是先跑裁剪率最大的规则比如数据本地性判定只要判断连接键和分布键一致两张大表的shuffle重分布成本就直接归零这类规则基本能一口气裁掉指数级的分布式计划。然后跑中等代价的规则比如谓词下推和索引匹配。最后跑一些专门的精细规则用来修正前面规则无法覆盖的边缘情况。同时要设定终止条件。比如当前候选集已经收缩到N个常见实现里这个阈值可能是8~16判定层就提前结束直接进入评估层。再继续跑规则虽然能把N从10压到8但多压掉的候选原本就要花功夫在评估层比较收益已然不大反而是那些需要扫描大量元数据的判定规则本身就要消耗资源。这个“差不多就行”的设计恰恰是工程理性的一种体现优化器的目标是让整体执行时间最短不是非要在每个子环节都做到极致。2.3 一个具体例子三表连接在判定层发生了什么来看一个最常见的三表连接查询SELECT /* leading(a, b, c) */ * FROM orders a JOIN order_items b ON a.order_id b.order_id JOIN product c ON b.product_id c.product_id WHERE a.user_id 10086 AND b.create_time 2024-01-01 AND c.category_id 1024;从传统CBO的角度这个查询至少要考虑a、b、c三张表的全排列加上每个join算子可能的hash join、nest loop再加上分布式场景下是否重分布的判断候选计划怎么也有几十个。而“先判定后评估”框架下判定层的动作是这样的第一条判定是谓词下推可行性。a.user_id 10086直接下推到orders表扫描层变成filter这个谓词会显著削减a表参与join的数据量c.category_id 1024同样下推到product表b.create_time 2024-01-01下推到order_items。这条规则结束每个基表上的扫描代价已经被大幅收缩了。第二条判定是连接顺序的可行性。先看能不能通过数据本地性确定顺序。如果orders和order_items在order_id上是共分布键那么它们的join一定是本地join这一步直接确定了第一个连接算子必须是a-b顺序淘汰了以c开头那一票计划。第三条判定是连接算法匹配。a和b过滤后能不能构建hash表还是需要走索引连接。这里orders按user_id过滤后大概率剩余量很小用product做驱动表、走order_items的索引来探测也是合理的取向。判定层到这里基本就把候选集压缩到“先a-b本地join再和c做第二层连接”这个方向上了。三条判定下来候选计划从几十个缩到两三条代价评估只需要在这两三条里选具体算法和并行策略。整个优化耗时可压缩掉一两个数量级而且因为初始方向就是对的选出来的计划质量不会差。3. 评估层怎么配合小范围精算依然决定最终成败3.1 代价精算的关键要素不是只看行数通过判定层留下少数候选后评估层的工作仍然重要不是说候选少了就能随便选一个。代价模型要考量的要素包括CPU代价、内存代价、I/O代价、网络传输代价以及这些元素在并发环境下的相互影响。这里有个很关键的认知数据库里一个算子的真实代价不是只看它处理多少行而是看它在执行期间消耗了什么资源。比如hash join的构建阶段要分配内存如果内存压力大可能触发落盘I/O代价就会暴涨再比如分布式环境下把一张小表broadcast到所有节点行数不多但网络包数量可能很高在某些网络拓扑里代价未必小。评估层把这些要素换算成一个统一的可比较数值才能选出真正在物理环境下性价比最高的计划。这套框架下评估层在“少量候选上做高精度计算”相比传统“全部候选上做粗糙估算”精确度可以用更高的计算预算来换。这是另一个被很多人忽略的好处候选少了评估时甚至可以引入一些耗时的精细手段比如使用采样的统计信息、实时DSA内存估算、甚至轻量级试跑。大集合不需要精算小集合可以精算这个思路我觉得意义很大。3.2 统计信息不准确时的兜底策略评估层的另一个经典难题是统计信息失真。无论采用什么框架只要代价模型依赖基数估算cardinality estimation就会受到统计信息偏差的影响。“先判定后评估”框架对统计信息失真是有一定抵抗力的原因很简单很多可能的坏路径在判定层就被裁剪掉了统计信息误差即便存在也影响不到那些早就出局的候选。但判定层也不能完全兜住所有问题。比如两个大表的连接键上数据分布极度不均匀某个值占了50%的行数统计信息如果用了均匀分布假设评估层就可能把hash join的行数估小一半导致选错计划。这种场景需要额外的手段来兜底高频值most common values检测基表扫描时维护列的高频值直方图评估层对高频值单独计算代价动态采样对关键判定点涉及的列在优化阶段做轻量级抽样统计更新直方图后再评估运行时反馈实际执行算子后把真实行数反馈给优化器下次生成计划时修正误差。这些手段在传统CBO里也可能实现但只有在候选计划数量少时它们才“用得起”。候选几百个时每个都用动态采样优化时间就会变成一个灾难。候选只有三五个时动态采样那一点点额外开销完全可接受。3.3 计划缓存与参数化复用让“先判定”的价值持续复利优化问题还有一个容易被忽视的维度就是同样结构的SQL会反复到达比如业务系统里参数不同的分页查询、状态查询。“先判定后评估”框架在计划缓存方面其实是天然友好的因为判定层的裁剪结果具备高度稳定性同一个SQL模板在不同的参数下判定层的裁定路径几乎一致候选计划变化很小评估层的输出也相对稳定最终计划缓存的命中率和一致性都会很好看。相比之下传统CBO在参数化查询上容易出问题参数不同统计信息选中的索引路径可能跳变同一个SQL模板有时候走这条索引有时候全表扫缓存里的旧计划还得失效重来。判定层在这个框架里相当于给“稳定性”上了一道保险。不过这种稳定性也有反面就是参数化计划可能不是每个参数下的最优计划。比如user_id1对应的是小卖家的订单行数很少user_id99999对应的是头部大卖家的数据行数巨大。如果一条参数化计划在两个极端里都走同一条路径其中某一端就会选歪。这个问题我在后面“常见问题”里会专门说。4. 实操观察怎么看出一套“先判定后评估”框架在起作用4.1 从EXPLAIN输出里看判定痕迹想验证你的数据库是不是真的采用了“先判定后评估”思路最直接的办法是看执行计划输出的注解信息。在实际使用中这类框架的EXPLAIN输出里会多出一些“判定说明”性质的内容比如QUERY PLAN ------------------------------------------------------------------ Nested Loop (cost1.25..18.32 rows423 width16) - Seq Scan on orders a (cost0.00..5.18 rows1 width8) Filter: (user_id 10086) Optimization: predicate pushed down (by rule pg_order_filter_pushdown) - Index Scan using idx_order_items_order_id on order_items b Index Cond: (order_id a.order_id) Optimization: local join confirmed (distribution match rule)我在某些优化日志里还见过更明确的标记比如Rule Decision: pruning candidate set, remains3、Candidate Set Size after ruling: 3之类的字段。如果你能在EXPLAIN输出或者优化器trace日志里看到“候选集收缩”的记录那基本就能确认框架在其中起作用了。这套信息对DBA和研发排障非常有用。以前看传统CBO的EXPLAIN只知道“优化器选了一条计划”不知道为什么选它更说不清那些没被选中的计划到底有多少。有判定痕迹之后至少你能看出来它在决策链路里砍掉了什么方向、置信依据是什么这对后续调参和写SQL的姿势调整是非常有价值的反馈。4.2 真正有效的度量指标优化耗时占查询总耗时的比例我个人判断这套框架收益的核心指标不是单条SQL变快多少而是“优化耗时占比”optimize-to-execution ratio。这个比例越低说明优化器自身消耗越可控大量简单查询尤其受益。我在一个模拟的OLTP负载里做过对照实验。同样10000条查询传统枚举式优化下优化耗时超过20ms的查询占17%其中有些达到几十毫秒切换“先判定后评估”之后同一批查询里优化耗时超过20ms的比例降到1.2%整体查询吞吐量提升了约30%。这个提升不是执行计划变快带来的纯粹是优化器自己少花时间省出来的。对于单条查询只有1~2毫秒的小事务场景这种省出来的时间直接体现在吞吐曲线上。当然追求这个指标的同时要防止另一种极端判定层太粗暴一两条规则就把所有候选都砍没了导致评估层根本没法兜住特殊情况。所以我在实际调优时会同时监控一个指标——“计划变更率”也就是定期对比同一SQL模板在不同统计信息下的最终计划差异度。如果差异度为0说明判定层大概过度裁剪了评估层失去意义如果差异度过高说明判定层没有起到稳定器的作用。这两个指标合起来看才算比较健康。4.3 压测场景中的表现差异压测场景里我最关注的是两类负载的表现高并发小事务和复杂分析查询。高并发小事务的特点是SQL结构简单但量极大优化器执行一次计划生成的时间哪怕只差0.1毫秒在百万级执行次数下都会被放大成秒级差距。传统CBO在简单查询上浪费了大量时间做无谓的候选枚举而“先判定后评估”框架下简单查询往往经一两条判定就收敛到唯一计划直接进入执行阶段这部分收益最明显。复杂分析查询的收益则体现在计划质量和稳定性的提升上。多表大连接、复杂子查询、窗口函数叠加这类场景传统优化器在枚举空间里很容易迷失选出一个局部最优但全局很差的计划。“先判定后评估”因为先通过规则把空间圈定了计划质量下限被抬高了最差也不至于太离谱最优还有机会更优。我在模拟数据分析场景里跑过印象比较深的一个查询涉及9张表连接和3层子查询嵌套传统优化器生成的计划存在严重的中间结果膨胀而“先判定后评估”生成的计划通过判定层先强制了若干下推顺序中间结果量直接降了一个数量级查询耗时从6.8秒降到1.2秒。5. 常见问题与避坑实录5.1 判定规则偶尔误判计划反而走偏任何基于规则的裁剪都不可能保证100%正确。最大风险就是某条判定规则本身不适用于当前数据分布但规则触发了把一条本来不错的候选计划裁掉了。我在实操里就碰到过一次系统里某张表数据量很小我按常规思路规则设置“小表无条件broadcast”结果业务后期该表数据涨了100倍广播代价已经超过重分布了但规则还在生效查询性能大幅下降。这类问题排查起来最隐蔽因为EXPLAIN看起来完全合理但物理执行性能就是不对。应对办法是规则触发时要有代价兜底也就是判定规则产生的计划不能直接执行必须仍然进入评估层做一次代价验证另外就是定期复盘规则命中率和最终计划变更率发现某条规则经常被触发但选中的计划频繁被推翻就需要调整规则阈值甚至删除该规则。5.2 统计信息过期导致评估层失真不管判定层多强大最终落到执行计划的还是评估层。如果统计信息长时间不更新评估层的基数估算偏得太远那么“先判定”裁下来的候选里选出的“最优”可能依然是错的只是错得比较体面而已。这个问题的典型场景是批量导入数据后没有及时做统计信息更新优化器以为某表只有100万行实际已经3000万行连接算子的估算行数差了30倍代价模型给出的排序必然不对。解决办法就是在数据剧烈变化后立刻执行统计信息采集建议对全库统计信息更新时间设好窗口同时对个别核心大表做增量感知。另外前面提到的动态采样和运行时反馈机制就是用来对抗这类场景的如果系统支持这些功能建议一定打开。5.3 参数化计划的陷阱同一个模板不同参数的尴尬前面提到参数化查询在计划缓存里可能踩坑。框架的判定层虽然提高了计划稳定性但反过来也会加深“一条计划管所有参数”的固化效应。比如订单表里头部大客户的数据量可能是普通客户的几百倍那么“按客户ID过滤订单”这个查询模板在两种参数下最优计划截然不同一个走索引点查一个可能需要并行全表扫。如果缓存把两条参数路径搞成同一条计划总有一方的性能会受损。这类问题的解法是参数化级别设置基数敏感分组。框架在判定层里如果有“参数阈值感知”功能能根据参数的不同划分到不同计划组就能有效规避。实际操作中我发现配合数据库的参数化级别调整以及针对大客户场景里加一条“按user_id直方图判断是否改变计划”的窄规则问题可以缓解得非常明显。5.4 框架不是魔法SQL写法依然影响判定结果最后说一个不得不重复的点再强的优化框架也架不住SQL本身写得反优化。比如明明可以exists改写成join却不写非要用两层相关子查询加not exists或者明明字段上没建索引却天天指望着一条慢查询能被框架优化到飞起。判定层能做的事情是帮你把“技术上不合理的路径”优先剪掉、把“技术上合理的路径”尽量留在评估范围里。但如果SQL本身从物理执行角度就是一个反模式——大表全字段select、超长or条件合集、函数包裹索引列导致索引失效——那不管框架怎么优化收获都很有限。框架的收益必须建立在SQL已经是一个合格候选的基础上。我自己的使用体会是这套框架落地之后最舒服的不是单条SQL变快而是“DBA每天需要手工干预的慢查询少了很多”。以前优化器会时不时选出一个匪夷所思的计划DBA看到就得手工绑定执行计划现在因为候选集被判定层圈定了计划质量的下限明显抬高出奇葩计划的概率大幅降低日常运维的焦虑感确实小了不少。最后分享一个小经验如果你也在研究或者使用这类“先判定后评估”思路的优化框架建议不要只盯着优化耗时这一个指标去看多花点时间观察判定规则树被触发的路径分布。哪个规则总被触发、哪个规则从未触发这些信息才是你后续调优判定规则亲和度、甚至调整SQL写法方向最重要的依据。把判定层的逻辑曲线摸透了这套框架才真正变成你自己的工具而不只是一个别人口里的新概念。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑