优化器在整条链路中的位置

一条 SQL 进入数据库后,大致要经过语法解析、逻辑计划生成、逻辑优化、物理计划选择四步。查询优化器是关系型数据库中最难懂的部分之一,也是性能差异的主要来源。

  1. parser:把 SQL 文本解析成抽象语法树,构造出对应的 AST 结构;
  2. 逻辑计划:把 AST 转换为逻辑算子组成的树,例如 Sort、Projection、DataSource;
  3. 逻辑优化:做谓词下推、常量折叠、列裁剪等改写,减少后续处理的数据量;
  4. 物理计划:为每个逻辑算子枚举可选的物理实现,按代价选出最优组合。

以 Order By 为例看优化过程

排序是很有代表性的例子。以 ORDER BY 子句的处理为例,分析语句的正确方式是从 parser 入手,看它构造了哪些 AST 结构:OrderBy 语法最终会构造出 ast.OrderByClause,其核心数据结构是 ast.ByItem,每一个 ByItem 持有排序表达式和是否为降序的标记。

进入计划构建阶段后,buildSelect 在发现 sel.OrderBy != nil 时会调用 buildSort,创建一个 Sort 逻辑计划。这里有一个容易被忽略的细节:如果 order by 的表达式里带有聚合函数,需要先把聚合函数中关联的列改写为 selectFields 中对应的项,并记录索引映射,供后续 Sort 算子使用;对于聚合函数本身,则会在 selectFields 末尾追加一个辅助字段并记录其下标。

常量折叠与表达式改写

buildSort 中会对每个 byItem 的表达式做一次 rewrite。这一步的作用是把能提前算出来的结果直接算掉,例如 YEAR('2009-05-19') 可以直接折叠成常量,1 + 1 也可以直接变成 2。Order By 子句中的表达式大多是列引用,rewrite 会把它转换成对应 schema 中的列对象。这类基于 Visitor 模式的改写是优化器的常见手法。

物理计划:要不要真的排序

逻辑计划确定后,需要通过代价比较来决定物理实现。关键在于:如果 ORDER BY 的列刚好是主键或某个索引的前缀,那么数据本身就是有序的,可以省掉 Sort 算子。实现上的典型做法是对同一个子树分别以两种不同的 requiredProp 调用一次物理计划转换:一次不带排序需求,一次带上 ORDER BY 的列作为有序需求,然后比较两次生成的计划代价,再决定是否下推。

如果判定可以下推,最终得到的可能是 IndexReader 或 IndexLookupReader 这类本身就保序的算子,而不需要额外构造排序;否则才真正生成 Sort 算子。当 SQL 带 LIMIT 时,排序算子还可能被优化为 TopN,避免对全量结果排序。

调优时的实用判断

  • 先看执行计划:用 EXPLAIN 观察是否出现全表扫描、是否有意料之外的算子;
  • 关注代价估算的输入:统计信息是否准确,直接决定优化器的判断质量;
  • 避免表达式阻断下推:在索引列上做函数运算通常会让优化器无法利用索引;
  • 警惕隐式类型转换:字段类型与查询条件类型不一致时,索引可能失效;
  • 理解排序与 Limit:合理利用索引序和 TopN,能显著降低内存与网络开销。

优化器不是黑盒。理解了它构造计划、比较代价、选择算子的顺序,调优就从试错变成了有依据的推断。

优化器处理链路
语法解析SQL 文本 -> 抽象语法树,构造 OrderByClause、ByItem 等结构
逻辑计划生成 Sort / Projection / DataSource 等逻辑算子树
表达式改写常量折叠、列引用解析、聚合函数与 selectFields 对齐
代价比较对同一子树按不同有序需求生成计划,比较代价决定是否下推排序
物理算子可下推时直接使用保序读算子,否则生成 Sort,带 Limit 时可能转为 TopN