引言

在这篇文章中,我们将讨论 Vitess 查询规划器的一次优化变更及其如何增强优化过程。这种新模型的核心在于,让优化流程中的每一步都生成一个可执行的计划。这种方法具有多方面的优势,包括更简单的理解与推理、更方便的测试,以及可以在排序、分组和聚合中使用任意表达式的能力。


Vitess分布式查询规划器

VTGate是Vitess的代理组件,它接受用户的查询并为其规划如何在多个分片或键空间之间分发执行。VTGate查询计划的最底层是“路由”(Route),它是操作符的一种,能够将查询发送到一个或多个分片。
当某些运算可以被下推到路由层时,意味着这些工作将由MySQL来完成,而VTGate无需承担太多额外作业。优化的目标始终是尽可能多地将计算下推到更快的MySQL处理流程。这种方法帮助将计算卸载给MySQL,使VTGate层保持高效,同时降低Vitess与原生MySQL之间可能产生的兼容性差异风险,因为大部分工作由MySQL完成。

Vitess查询规划器的工作流程示意图

图例:一个代表VTGate的入口标识(形似大门)接受用户查询(以文档符号表示),VTGate通过箭头连接到树状图(象征查询计划)。树的分支下端以若干叶节点结束,每个叶节点标记为“Route”。从这些叶节点延伸出指向多个模拟数据库的箭头(以圆柱形符号表示,标记为“MySQL”),每条箭头注明“下推至MySQL”,强调主要工作由MySQL完成。


查询规划模型的变更

在传统的查询规划模型中,优化过程首先决定表之间的连接顺序。所谓“连接顺序”,是指表连接的次序,以构建最终的结果集。
连接顺序确定以后,规划器进入**Horizon Planning**阶段。Horizon操作符包括SELECT表达式、聚合、ORDER BYGROUP BYLIMIT。如果这些操作能够整体下推给MySQL,我们就无需在VTGate侧规划任何内容。但如果这些操作无法以整体形式下推给MySQL,我们就必须分别规划这些组件。
在一个本地数据库查询规划器中,这部分规划相对直观——我们添加必要的SortGroupByLimitProjection操作符即可完成。然而,在分布式查询规划器中,传输数据的开销使得尽量将这些操作下推到数据层变得至关重要。
如果想了解我们如何在分组与聚合操作中进行下推,可以参考我们的文章:[Vitess中的分组与聚合](Link-to-article)。
在新的规划器模型中,我们仍然执行与原模型一样的优化,但实现方式却大不相同。

旧模型 VS 新模型

在旧的模型中,优化过程采用自顶向下的方式执行——一次性规划完整的聚合操作及其所需的所有排序支持。规划过程开始于连接顺序树,进行大量逻辑计算后,输出一个新的树状结构以执行所需的聚合。期间,大量中间状态存储在参数、本地变量与栈中。
示意图:两个流程图,其中一个标题为“旧流程”,另一个标题为“新流程”。旧流程按步骤划分为:解析(Parse)、决定连接顺序(Determining Join Order)、Horizon Planning(模糊表示,以示难以检视)和可执行计划(Executable Plan)。新流程相比旧流程的“模糊”部分变得清晰,且新增了一步“Offset Planning”,位于Horizon Planning与最终的可执行计划之间。
在新的查询规划模型中,优化流程中的每一步都会生成一个可运行的计划。这意味着开发人员可以在任意阶段检查该计划,从而更好地理解每个步骤的优化过程。每一步的结果都是一个完整的树状结构计划,这使得识别潜在问题、非高效处或进一步优化的空间变得更容易。
每一步也变得更简单——这是一种树结构变换,以两个操作符作为输入,一个输入嵌套在另一个内,输出为替换两个输入的新子树。
这种改进不仅简化了优化过程,还提升了对每个优化步骤影响的推理能力。


新流程的其他优点

可视化改进

与旧模型相比,新查询规划模型在优化步骤的可视化方面表现更佳。旧模型中,优化状态存储在栈与本地变量中,使得理解和可视化变得困难。而在新模型中,每一步都以完整的查询计划呈现,能够将优化过程展现得更加清晰。

测试的可行性

新的模型允许在同一条件下运行未优化计划和优化后的版本,并比较两者的结果。无论是否需要由VTGate侧使用其强大的evalengine支持来评估WHERE谓词,或者能否将其委托给底层数据库,最终结果应该保持不变。

表达式的灵活性

新查询规划模型支持在排序、分组和聚合中使用任意表达式。这为编写复杂查询提供了更大的灵活性,同时让开发人员能够构建更加高效和优化的查询。相比之下,旧模型在这些操作中对表达式的支持存在一定的限制。


示例查询与优化步骤

通过实例查询,我们可以展示新查询规划模型的优化步骤。这些步骤利用一种称为“固定点重写器”的工具——规划器会持续重写计划树,直到计划树停止变化。

示例查询

SELECT u.foo, ue.bar
FROM user u JOIN user_extra ue ON u.uid = ue.uid
ORDER BY u.baz

第一步

在规划的第一步中,我们得到以下操作符树:

Horizon
└── ApplyJoin (u.uid = ue.uid)
   ├── Route (Scatter on user)
   │   └── Table (user.user)
   └── Route (Scatter on user)
       └── Filter (:u_uid = ue.uid)
           └── Table (user.user_extra)

任何“Route”以下的内容都会被转化为SQL并下推给MySQL执行。

第二步

无法整体下推Horizon的情况下,我们将其拆分为部分组件:

Ordering (u.baz asc)
└── Projection (u.foo, ue.bar)
   └── ApplyJoin (u.uid = ue.uid)
       ├── Route (Scatter on user)
       │   └── Table (user.user)
       └── Route (Scatter on user)
           └── Filter (:u_uid = ue.uid)
               └── Table (user.user_extra)

Horizon被拆分为OrderingProjection操作符。

第三步

我们继续向下推操作——将Projection拆分并分别推送到连接两侧,并将Ordering推送到连接左侧:

ApplyJoin (u.uid = ue.uid)
├── Ordering (u.baz asc)
│   └── Projection (u.foo)
│       └── Route (Scatter on user)
│           └── Table (user.user)
└── Projection (ue.bar)
   └── Route (Scatter on user)
       └── Filter (:u_uid = ue.uid)
           └── Table (user.user_extra)

第四步

最终,我们成功将ProjectionOrdering下推至连接左侧的Route

ApplyJoin (u.uid = ue.uid)
├── Route (Scatter on user)
│   └── Ordering (u.baz asc)
│       └── Projection (u.foo)
│           └── Table (user.user)
└── Route (Scatter on user)
   └── Projection (ue.bar)
       └── Filter (:u_uid = ue.uid)
           └── Table (user.user_extra)

最终,VTGate的查询计划只剩一个连接操作。一个查询被发送到连接左侧,随后对右侧发起查询以获取结果。
两条查询如下:

-- 左侧查询
SELECT u.foo, u.uid, u.baz, weight_string(u.baz)
FROM `user` AS u
ORDER BY u.baz ASC;

-- 右侧查询
SELECT ue.bar
FROM user_extra AS ue 
WHERE ue.uid = :u_uid;

结论

Vitess的新查询规划模型相比旧模型,带来了诸多优势,使我们能够更轻松地理解与处理Vitess中最复杂的组件之一。通过每步生成可执行的计划、改进可视化,以及增加表达式灵活性,我们相信这个模型将成为一个可以长期发展的设计。
随着Vitess的持续发展,查询规划能力预计会迎来更多改进与优化。



Vitess的查询规划优化:分步解析插图

关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台

除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接

本文链接:https://choupangxia.com/2025/09/13/vitess-optimzing/