同一条嵌套查询,在数据库版本升级后执行计划出现天翻地覆的变化,这个现象并不罕见。嵌套查询看似只是把子查询写在括号里,实际却牵涉优化器对半连接、反连接、子查询展开和物化等多种转换策略的选择。版本变迁带来新规则、新成本模型和默认参数调整,执行路径自然可能大不相同。

嵌套查询执行计划的基础转换
优化器处理嵌套查询的第一步通常是语法树改写。以内层IN子查询为例,优化器会尝试将IN改写为半连接,因为半连接只需判断驱动表记录是否在内层结果集中存在,不需要处理重复记录,这比普通内连接更高效。EXISTS、NOT EXISTS、NOT IN也有对应的半连接和反连接表示。
但改写不是无条件的。优化器必须判断改写后的连接顺序、连接算法和索引利用是否优于原始方案。当子查询中包含聚合、DISTINCT、LIMIT等复杂结构时,改写难度增加,优化器可能退回到物化方式,即先执行内层查询并把结果存入临时表。不同版本对改写条件的判定阈值和成本估算方法不同,直接导致计划选择不同。
下面这条查询是一个常见的嵌套查询例子。
SELECT e.employee_id, e.first_name, e.last_name FROM employees e WHERE e.department_id IN ( SELECT d.department_id FROM departments d WHERE d.location_id = 1700 );
在旧版本中,优化器可能先执行子查询得到所有location_id为1700的部门编号,将它们物化到一个临时表,再让employees表与临时表做全表扫描关联。新版本则可能把子查询展开为半连接,直接对departments表按location_id索引取数,然后对employees表的department_id做探测。两种计划在数据量大时性能差距可达数倍。
优化器版本差异的根源:规则与成本模型演进
数据库优化器每次大版本升级都不是简单修补。开发团队会引入新的查询转换规则,例如子查询提升、半连接物化、派生表合并、公共子表达式消除等。新增规则会改变候选计划空间,原本没有的路径突然变得可用,优化器自然可能选择新的计划。
成本模型的变化同样关键。优化器根据统计信息估算每个候选计划的CPU、IO和内存代价,再选总代价最低者。不同版本可能调整代价权重,比如把随机IO的代价提高、把哈希连接内存代价调低。嵌套查询通常涉及多表关联,代价敏感度更高,一点参数变化就会让计划切换。
另外,统计信息收集策略也随版本变化。比如直方图类型增加、采样比例调整、自动收集触发条件改变。如果升级后统计信息没有及时重新收集,或者收集方式与旧版本不同,优化器得到的基数估算偏差会放大,最终导致执行计划漂移。默认优化器开关的变化也需要关注,有些数据库新增了类似semi-join、materialization之类的开关,默认开启后对嵌套查询影响巨大。
典型版本变化对比:子查询物化与半连接优化
为了更直观地理解差异,可以对比两个代表性版本对同一条NOT IN查询的处理。假设需要找出从未下过订单的客户。
SELECT c.customer_id, c.customer_name FROM customers c WHERE c.customer_id NOT IN ( SELECT o.customer_id FROM orders o WHERE o.order_date >= CURRENT_DATE - INTERVAL '30' DAY );
早期优化器缺少反连接改写能力,会老老实实执行子查询,把orders中所有符合条件的customer_id去重后物化成临时表,然后外层customers表逐行与临时表做NOT IN判断。这种实现方式存在两个问题:一是临时表可能非常大,二是NOT IN对NULL值敏感,当子查询结果包含NULL时整个查询结果可能为空,优化器为了正确性往往无法走索引。
新版本优化器会尝试将NOT IN改写为反连接,尤其是当优化器能够证明子查询结果不包含NULL或者用户明确语义为NOT EXISTS时,可以直接使用索引嵌套循环反连接。针对customers表每条记录,利用orders表上的customer_id索引探测是否存在匹配。只要找到一条匹配就跳过,找不到则返回该客户。这种执行方式不需要物化,内存占用低,返回首行速度快,非常适合OLTP场景。
可以从执行计划输出中看出端倪。旧版计划常出现Materialize和Full Table Scan,新版则出现Nested Loop Anti Join或Hash Anti Join。升级后如果发现计划从物化变为反连接,通常性能会变好;但也可能出现反面情况,比如新版本高估了某边基数,选择了哈希反连接导致内存争用,反而变慢。
如何应对跨版本执行计划漂移
升级数据库版本前,务必在测试环境采集重点SQL的执行计划基线。对于高并发核心业务的嵌套查询,尤其要对比升级前后的cost、行数估算和访问路径。很多数据库提供执行计划管理功能,例如计划基线或SQL计划管理,可以将旧版本中对生产已验证的执行计划导入到新版本并固定下来。
如果无法使用计划基线,还可以通过优化器提示或会话级参数做临时控制。例如关闭子查询展开、强制使用物化或半连接、指定连接顺序等。但这类提示会写死在SQL文本中,不利于后续维护,只适合作为过渡方案。
同时要重视统计信息的同步更新。升级后立刻重新收集涉及表及其索引的统计信息,避免因直方图变化导致基数估算失真。若数据库提供成本模型版本参数,应结合业务负载测试不同值的影响,选择与旧版本行为最接近的配置。最后,建立持续的性能监控,一旦发现执行计划漂移导致慢查询,能够快速回退或调整参数,确保升级平稳落地。