ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

连接条件下推原理与KingbaseES实战:让SQL优化器先过滤再连接

连接条件下推原理与KingbaseES实战:让SQL优化器先过滤再连接 慢SQL优化是大多数DBA和开发者的日常必修课光是“为什么不走索引”“为什么这条SQL跑了十分钟”这类问题就能让人排查一个下午。而在所有慢SQL的根因里有一种情况特别隐蔽明明单表过滤条件能过滤掉大量数据但优化器偏偏把过滤动作放到了连接之后导致中间结果集膨胀得厉害整个查询就像背着一麻袋石头跑步。连接条件下推正是针对这类问题的经典优化手段。这篇文章我从原理层面拆解它到底解决了什么问题再结合国产数据库KingbaseES的实际执行计划讲清楚怎么判断、怎么改写、怎么通过参数和统计信息让优化器自动选择这类计划。适合正在做SQL调优、或者刚接触国产数据库迁移和验证的读者内容偏实操。1. 连接条件下推的核心逻辑与适用场景1.1 为什么说“先过滤再连接”是黄金法则关系型数据库里连接操作算是最昂贵的运算之一。两个一万行的表做连接理论上就是上亿次比较动作哪怕走索引或者哈希连接代价依然比单表扫描高一个量级。所以所有优化器的基本盘都是“尽可能减少参与连接的数据量”。条件下推就是干这个的——把WHERE条件里只涉及某一个表的过滤谓词尽量下推到表扫描或索引扫描阶段执行让进入连接环节的数据量从一开始就变小。我用个生活化的例子。你去菜市场买鱼摊主给你两条网兜一条网兜里装了一百条鱼另一条装了八十条要求你从里面挑出重量大于两斤的鱼做配对。最笨的办法是把所有鱼倒进一个大盆再从头到尾一条条称重配对聪明的办法是先在各自网兜里把不够两斤的鱼放回去只拿着保留下来的十来条鱼去配对。连接条件下推就是这个“先把不够格的扔出去”的过程单表谓词下推得越彻底连接算子的输入越少整体代价自然越低。很多朋友最初听到“条件下推”第一反应只知道索引条件下推Index Condition PushdownICP以为所有条件下推都是存储引擎层的事。实际上连接条件下推是优化器层面的逻辑重写它关注的是“谓词应该挂在执行计划的哪个位置”。在Oracle里这叫Predicate Pushdown在PostgreSQL里叫Constraint Exclusion之类在KingbaseES里同样有完整的实现。理解了这一点后面看执行计划时你就能抓到重点谓词挂在动态索引扫描上还是挂在连接之上的Filter节点上结果天差地别。1.2 哪些场景最需要关注连接条件下推不是所有SQL都需要关心条件下推它主要针对以下几类场景发挥价值。第一类是星型模型里的多表关联查询。典型如订单事实表关联客户维表、商品维表、门店维表事实表行数几百万上千万维表几万行。如果过滤条件只落在维表上例如“只看北京地区的客户订单”这个过滤条件能下推到客户维表扫描阶段客户维表先缩成一个很小的子集再参与连接效果立竿见影。第二类是子查询展开后的连接。优化器会把很多IN、EXISTS子查询改写成连接如果子查询内部本身有过滤条件能否下推到子查询内部执行直接决定子查询产生的中间集有多大。很多“明明子查询很快但整条SQL奇慢”的案例就是这里出了问题。第三类是视图或CTECommon Table Expression公共表表达式展开后的谓词下推。开发者习惯把复杂逻辑封装成视图在外层加过滤条件理想情况下过滤条件应该直接穿透到视图内部的基表上。能不能穿透就取决于优化器的下推能力。KingbaseES的查询重写机制对这类场景处理得比较成熟。反过来说如果你的SQL本身没有连接或者所有过滤条件都集中在单表单列上那连接条件下推对你的帮助有限这时候该关注的是索引设计、统计信息、执行计划是不是走了顺序扫描这类更基础的问题。1.3 连接条件下推和索引条件下推的区别顺带说一个容易混淆的点。索引条件下推发生在存储引擎层面它把WHERE里无法通过索引定位、但跟索引列相关的条件传给存储引擎在读取索引记录时一并判断减少回表次数。MySQL的ICP就是典型例子。连接条件下推则发生在优化器逻辑优化阶段它不管存储引擎怎么读数据只决定谓词算子应该挂在执行计划树上的哪个位置。两者是不同层级的东西KingbaseES作为关系型数据库其执行计划通过EXPLAIN可视化之后你能看到谓词在算子上的分布这是判断条件下推是否生效最直接的证据。2. 为什么优化器有时“不愿”下推代价模型与统计信息的博弈2.1 连接顺序与下推的联动关系要让条件下推真实落地优化器得先确定连接顺序。连接顺序不同谓词下推的位置可能完全不同。比如A、B、C三表连接过滤条件落在A表优化器如果选择先连接B和C再把结果跟A连接那A表的过滤条件就只能等最后一步才能执行B、C连接产生的中间结果全得背在身上。反过来如果优化器先把A过滤完再跟B连接代价就小得多。所以连接条件下推不是孤立的技术它跟连接顺序选择直接挂钩。这也是为什么很多情况下明明某个单表条件能过滤99%的数据优化器依然选了糟糕的路径大概率是把带过滤条件的表放到了连接顺序的后半段。KingbaseES的优化器在处理这类问题时会综合每个表的行数估计值、分布特征、索引情况来做代价评估。“把最能把结果集削小的表优先连接”是优化器试图遵循的原则但能不能识别出来依赖统计信息是否准确。经常能看到统计信息过旧导致行数估计偏差十倍百倍优化器据此选错了连接顺序条件下推也跟着失效。2.2 统计信息优化器判断下推效益的输入我接触过不少国产数据库迁移的现场最大的坑就是迁移完忘了收集统计信息。原来数据分布是老样子统计信息却还是空的或者旧的优化器两眼一抹黑全靠默认值猜。这样条件下推就算逻辑上能执行代价评估也可能认为“下推没有收益”而放弃。举个实际例子一张五百万行的订单表客户编号列上过滤条件customer_id 10086实际只有几百行命中。如果统计信息缺失优化器可能认为这个条件会命中一半数据行那下推后参与连接的还是两百五十万行收益被严重高估后反而变成“不值得优化”。所以排查条件下推失效问题时第一步不是改SQL是看统计信息新鲜度。KingbaseES里可以查pg_statistic或直接用ANALYZE命令主动收集。2.3 连接算法对下推效果的影响连接算法也会影响下推的显性收益。嵌套循环连接Nested Loop Join下驱动表如果能被条件过滤缩小内外层循环次数同步下降收益是乘数级的。哈希连接Hash Join下小表先建哈希表如果小表侧有条件下推建哈希表代价和哈希探测代价都降低。排序合并连接Merge Join类似两侧数据预先排序开销减少。理解这个点对分析执行计划很有用。你在EXPLAIN里看到Hash Join通常希望过滤条件能下推到哈希连接的小表侧也就是内表看到Nested Loop则希望下推到外表侧作为驱动。如果条件挂在了连接之上的Filter里意味着数据先连接完再过滤这在大数据量下几乎是灾难。3. KingbaseES 环境下连接条件下推的实战形态3.1 从执行计划判断下推是否生效在KingbaseES里做SQL调优最顺手的工具就是EXPLAIN。需要注意一点单靠默认的EXPLAIN看不出谓词挂在哪个节点建议用EXPLAIN (ANALYZE, VERBOSE, COSTS ON, BUFFERS ON)把输出信息打全。VERBOSE选项尤其重要它会显示出每个算子的输出表达式和过滤条件这样就能清晰看到单表谓词到底被推到了哪里。拿到一份执行计划先关注每个扫描节点下的Filter或者Index Cond。比如这样一个SQLSELECT * FROM orders o JOIN customers c ON o.cust_id c.id WHERE c.city Beijing AND o.amount 1000;理想计划中customers表的扫描节点上应该有Filter: (city Beijing::text)orders表的扫描节点上有Filter: (amount 1000)。两个过滤动作都在扫描阶段完成之后才进入连接。如果执行计划长这样Nested Loop - Seq Scan on customers c Filter: (city Beijing::text) - Seq Scan on orders o Filter: (amount 1000) ...说明下推成功。但如果看到连接节点上方还有一层Filter: (c.city Beijing::text)那就要警惕了可能优化器因为某种原因没有完成下推或者等价改写受到了限制。3.2 常见可下推谓词类型与改写方法在KingbaseES中绝大多数基础比较谓词都能下推比如等值比较、范围比较、IS NULL、IN列表、LIKE前缀匹配等。这些谓词只要确定只引用单表列逻辑上一定可以下推优化器一般不会犯低级错误。真正容易出问题的是这几类第一类函数包裹列的情况。像WHERE upper(c.name) ZHANGSAN如果函数本身被定义为非不可变不是IMMUTABLE优化器无法确定每次调用结果是否一致下推就会被限制。解决办法是改成WHERE c.name zhangsan配合函数索引或者把函数改成不可变版本。第二类跨表引用条件的子查询。比如WHERE o.amount (SELECT AVG(amount) FROM orders WHERE status OK)这种带子查询的谓词通常不能直接下推到orders表扫描里因为需要先算出聚合值。优化器一般会先执行子查询把结果作为参数再传递下去这属于“参数化下推”的范畴KingbaseES里也支持但不如单表谓词那么直观。第三类OR条件连接多个表字段。WHERE a.x 1 OR b.y 2这类谓词在连接之前无法下推到任何一个单独的表上因为结果依赖连接后的行。除非优化器能做等价改写拆成UNION分支的形式否则这个Filter只能挂在连接之上。遇到这种情况建议人工改写SQL把逻辑拆清楚。3.3 视图与CTE场景下的下推实战视图和CTE的下推是实践中最常踩坑的地方之一。看一个CTE例子WITH filtered_orders AS ( SELECT cust_id, SUM(amount) AS total FROM orders GROUP BY cust_id ) SELECT c.name, f.total FROM filtered_orders f JOIN customers c ON f.cust_id c.id WHERE c.city Beijing;理想情况下c.city Beijing可以下推到customers表扫描阶段filtered_orders的内部也可以先跟裁剪后的customers做半连接避免先聚合全部订单再过滤客户。但实际情况中优化器如果把这个CTE物化后面就无法执行谓词穿透只能先全量聚合再跟customers连接最后过滤city。在KingbaseES中CTE是否物化受查询特征影响。像上述场景更推荐把CTE改写成子查询或直接展开成普通连接让优化器有更大改写空间。同时多试几种等价写法把执行计划拿出来对比。写法和执行计划的对应关系只有在实践中才能真切感知。再比如视图嵌套视图的场景外层筛选条件能不能一路穿透到最底层基表取决于每层视图里是否有聚合、DISTINCT、窗口函数这类“下推阻断算子”。我一般会建议开发人员尽量把视图写“薄”不要过度包装业务逻辑否则优化器再聪明也没法突破语义边界。3.4 通过连接条件推动连接顺序两张表时的启发式思路连接条件下推跟连接顺序强相关这里分享一个自己常用的调优思路。当查询里有两张表以上时我会先找出“过滤性最强的表”也就是单表条件下预估返回行数最少的表想办法让它成为驱动表。在KingbaseES里可以通过EXPLAIN看估算行数如果发现驱动表的估算行数远大于实际值优先收集统计信息如果统计信息准确但优化器还是选错再考虑用JOIN_ORDER提示干预。下面是我在实际项目里用过的一个双表案例。某系统里有个大订单表和商品表订单表五千万行商品表二十万行。原始SQL是查特定分类下的大额订单优化器先生成了一份二十来万的订单中间结果再跟商品表连接执行时间三秒多。后来我把条件做了等价改写把商品表的分类过滤作为子查询先物化再跟订单表做连接执行计划变成了先过滤商品表再嵌套循环订单表执行时间降到几百毫秒。当然KingbaseES的优化器本身已经相当智能这只是特定统计信息不理想时的兜底方法不建议一上来就重度改写SQL。4. 常见问题与排查技巧实录4.1 谓词未下推时的标准排查路径当你怀疑一条慢SQL存在谓词未下推问题时我的排查顺序通常是这样的先看完整执行计划用EXPLAIN (ANALYZE, VERBOSE)确认Filter节点位置。如果Filter挂在连接节点之上说明确实没有下推。检查统计信息新鲜度看看相关表的reltuples、relpages是否与实际相符必要时执行ANALYZE再跑一次计划。检查谓词里有没有函数包裹列、隐式类型转换这两种情况最容易导致下推失效。如果SQL中有视图、CTE尝试将视图定义展开把过滤条件直接写到基表查询里验证是否由优化器改写能力不足导致。最后再考虑改写SQL结构或者使用提示干预这一步必须谨慎因为人工优化往往失去通用性。这套流程在KingbaseES和PostgreSQL系数据库里都通用实际排障时很顺手。4.2 为什么子查询条件没“沉”下去子查询未下推的问题也很典型。有个项目反馈某条SQL用了WHERE EXISTS (SELECT 1 FROM detail d WHERE d.order_id o.id AND d.shop_id 100)整体查询非常慢。执行计划显示detail表被全表扫描而且没有携带shop_id 100的过滤条件优化器先扫描整个detail表再跟外层做半连接。排查下来发现detail.shop_id列上缺乏统计信息优化器无法估计选择率干脆保守处理。补完统计信息后执行计划恢复正常shop_id条件被下推到索引扫描上。这类问题本质上不是优化器不聪明而是统计信息缺失导致它“不敢”做激进下推。碰到子查询里条件列有索引但没被利用的情况先别急着改SQL很多通过补统计信息就能解决。如果用EXPLAIN ANALYZE看到某个节点实际行数与估算行数差了一个数量级以上那基本可以断定统计信息出了问题。4.3 参数调整是否能强制下推有些同行会问KingbaseES有没有类似开关的参数能强行打开条件下推。坦白讲优化器层面的连接条件下推没有极端的开关来控制它受代价模型驱动不是简单的on/off。但有几个参数会间接影响例如enable_hashjoin、enable_nestloop、enable_mergejoin关闭某类连接算法往往能改变执行计划间接导致条件下推位置改变。from_collapse_limit、join_collapse_limit这两个参数影响多个连接表的重写顺序调大后优化器可能获得更多连接顺序尝试空间。geqo_threshold控制遗传查询优化器何时启用超过阈值后优化器可能不再做穷举搜索连接顺序和下推都有可能受影响。这些参数不建议在生产环境随意调整除非你已经通过EXPLAIN分析确认问题源于连接顺序枚举不足改参数立竿见影才能考虑系统性验证后下发。否则治标不治本下次数据分布一变又出问题。4.4 一张排查速查表我把实际遇到的高频问题整理成表方便大家复制到自己的排障手册里。现象可能原因首选处理办法连接节点之上还有Filter谓词涉及多表函数包裹列统计信息缺失查看VERBOSE计划补统计信息尝试等价改写子查询内部表不携带过滤条件子查询展开受限统计信息缺失先ANALYZE尝试拆分子查询视图/CTE外层条件未穿透物化CTE或视图内部有聚合阻断改写为子查询重写视图手动下推驱动表顺序不佳多表连接顺序枚举不足估行不准检查join_collapse_limit必要时使用提示干预相同SQL时快时慢统计信息过旧数据分布变化定期自动收集统计信息4.5 KingbaseES迁移优化场景里的一个实战示例最后分享一个完整的小案例来自某企业从国外数据库迁移到KingbaseES的项目。迁移后某报表查询跑了二十七秒用户完全不能接受。原SQL大概长这样SELECT ... FROM sales_fact f JOIN product_dim p ON f.product_key p.product_key JOIN store_dim s ON f.store_key s.store_key WHERE p.category Electronics AND s.region East AND f.trans_date BETWEEN 2024-01-01 AND 2024-03-31;查看执行计划发现两个严重问题一是sales_fact表过滤条件只有trans_date被下推到了索引条件另外两个维表过滤条件挂在了Hash Join之上的Filter节点二是连接顺序是先连接两个维表再连接大表导致中间结果膨胀。处理步骤先对所有相关表执行ANALYZE更新统计信息然后把两个维表条件通过改写SQL提前用子查询围住维表数据。修改后执行计划变成先过滤维表再以过滤后的结果集作为驱动连接事实表整体执行时间从二十七秒降到一点八秒。这个案例最妙的点是原SQL本身没有任何语法问题也不是传统意义上的“烂SQL”纯粹是优化器在统计信息偏差下做出的局部最优决策。可见连接条件下推不只是优化器的职责也是SQL编写者需要具备的意识——怎么写能让优化器更轻松地发现下推机会本身就是值得修炼的能力。KingbaseES在这方面跟经典PostgreSQL系优化器一脉相承但国产数据库的统计信息收集、执行计划展示和参数语义有自己的细节差异。建议新接触的朋友多花点时间读执行计划尝试用EXPLAIN的VERBOSE选项逐节点核对谓词位置当你对“谓词应该出现在哪一层”形成直觉之后很多慢SQL一眼就能看出病根。我自己这几年做SQL优化的体会是遇到慢SQL别急着加索引先花五分钟看执行计划思考“这批数据能不能更早变少”。连接条件下推的理念就是这句话的落地规则让每一层算子都只处理它必须处理的数据。把这个原则刻在脑子里再配合准确的统计信息和合理的SQL写法大多数连接型慢查询都能找到清晰的优化路径。之后你再看执行计划时关注点就会从“走了什么连接算法”升级到“每个节点过滤了多少数据”那才算真正入了SQL优化的门。
返回列表