数据库内核基于代价的优化器引擎

目录

概述

ORCA架构体系

Memo

Search&Job scheduler

基于代价的优化器引擎设计与实现

基于代价优化器面临的挑战

精准的统计信息和代价模型

海量的计划空间

结论


概述

传统的优化器引擎分两类,一类是基于规则的启发式计划模型(RBO),一类是基于代价的计划模型(CBO)。

RBO的计划模型更像是一位资深的DBA,定制几种优化情况,如果SQL满足这几种优化的情况,则进行特定的优化。例如下面的SQL案例

1
SELECT</wyn><wyn>  * </wyn><wyn>FROM</wyn><wyn>  TEST_RBO_1 T1 </wyn><wyn>WHERE</wyn><wyn>  T1.C1 IN (</wyn><wyn>SELECT</wyn><wyn>  T2.C1 </wyn><wyn>FROM</wyn><wyn>  TEST_RBO_2 T2 </wyn><wyn>WHERE</wyn><wyn>  T1.C1 = T2.C1)

计划如下所示:

这是一个典型的IN SELECT关联子查询转Join的案例,优化器使用的就是基于规则的启发式优化方式。

RBO优化器的特点,它只认规则,对数据不敏感。优化是局部贪婪,容易陷入局部优但是全局差的场景,容易受应用规则的顺序而生成迥异的执行计划,往往结果不是最优的。这里通过一个例子来说明RBO对于数据不敏感,会产生相对不是最优的计划例如下面的案例。

1
SELECT</wyn><wyn>  SUM(T1.C1) </wyn><wyn>FROM</wyn><wyn>  TEST_AGG T1 </wyn><wyn>GROUP BY </wyn><wyn>  T1.C1

上面这个分布式执行计划可以看到,通过多个节点的同时扫描数据,并且提前在节点做了一次AGG,让数据聚合在一起,再通过Hash分发的方式汇总到上一层计划,在把集群的数据最聚合一次。因为每一个节点上面存放的都不是全量的数据。这个计划有一个优势,就是提前做了一次AGG把大量的数据先聚合起来,减少了网络IO和CPU的资源。但是这里有一个场景,比如说我T1.C1里面存放的数据是唯一值呢,这样先做的AGG相当于会浪费性能。这就是基于规则的优化对于数据不敏感,产生执行计划低效的问题。

那么如何解决这些问题,数据库的优化器产生了CBO引擎,基于代价的计划引擎。我们以GPORCA优化器来阐述下CBO的执行过程。

目前业界也在讨论ABO引擎,基于AI智能的计划引擎,通过AI的技术让数据库产生更高效的计划,来适配更多的场景,这个不在本文的讨论范围呢。

ORCA架构体系

GPORCA是开源项目greenplum的下一代优化器,里面借鉴了Cascades Optimizer方式实现的一套优化器,ORCA实现了Enforcer机制、并行优化、优化退出机制、完整的测试以及对Cost Model不断学习改进框架。

ORCA的架构分成几大块:

Memo

用来存储执行计划的搜索空间的叫Memo。Memo就是一个非常高效的存储搜索空间的数据结构。它是一系列的集合(group)构成。每个group代表了执行计划的一个子表达式Group Expression(想对应与查询语句的一个子表达式)。不同的group又产生相互依赖的关系。根group就代表整个查询语句。

举个例子,假设语句是

1
selct * from table1 join table2 on (table1.col1 = table2.col1)

那memo就由3个group构成。根group就是join。Group1是table scan of table1, group2是table scan of table2. 每个group除了表达抽象的语句表达式,在优化过程中,还会加入具体的物理算子。

Search&Job scheduler

ORCA实现了一套算法来扫描Memo并计算得到预估代价最小的执行计划。搜索由job scheduler来调度和分配,调度会生成相应的有依赖关系或者可并行的搜索子工作。

这些工作主要分成三步

一、exploration,探索和补全计划空间,就是根据优化规则不断生成语义相同的逻辑表达式。举个例子,select * from a, b where a.c1 = b.c2 可以生成两个语义相同的逻辑表达式:a join b 和 b join a。

二、implementation,就是实例化逻辑表达式变成物理算子。比如, a join b 可以变成 a hash_join b 或者 a merge_join b。

三、优化,把计划的必要条件都加上,比如某些算子需要input被排过序,数据需要被重新分配,等等。然后对不同的执行计划进行算分,来计算最终预估代价。

Transformations

Plan transformation就是刚才优化中第一步exploration的详解,如何通过优化规则来补全计划空间。举个例子,下面就是一则优化规则 InnerJoin(A,B) -> InnerJoin(B,A)。这些transformation的条件通过触发将新的表达式,存放到Memo中的同一个group里。

Property enforcement

在优化过程中,有些算子的实现需要一些先决条件。比如,sortGroupBy需要input是排序过的。这时候就需要enforce order这个property。加入了这个property,ORCA在优化的过程中就会要求子节点能满足这个要求。比如要让子节点满足这个sort order property,一个可能的方法是对其进行排序,或者,有些子节点的算子可以直接满足条件,比如index scan。

Metadata Cache

数据库中表的元数据(column类型)等变动不会太大,因此Orca把表的元数据缓存在内存用来减少传输成本,只有当元数据发生改变时(metadata version改变时),再请求获取最新的元数据。

GPOS

为了可以运行在不同操作系统上,ORCA也实现了一套OS系统的API用来适配不同的操作系统包括内存管理,并发控制,异常处理和文件IO等等。

基于代价的优化器引擎设计与实现

这一个简化版的 TPCH q3,非常典型的三表 Join。其中表的特征如下:

  • customer 表按照 c_custkey 进行分区
  • orders 表按照o_orderkey 进行分区
  • lineitem 表按照 l_orderkey 进行分区
1
SELECT</wyn><wyn>    l_orderkey,</wyn><wyn>    o_orderdate,</wyn><wyn>    o_shippriority</wyn><wyn>FROM</wyn><wyn>    customer,</wyn><wyn>    orders,</wyn><wyn>    lineitem</wyn><wyn>WHERE</wyn><wyn>    c_custkey = o_custkey</wyn><wyn>    AND l_orderkey = o_orderkey

在进入到 CBO 之前,原始的执行计划如下图所示,customer 表和 orders 表先 Join,Join 的结果再和 lineitem 表 Join,然后输出结果。

初始化Memo空间阶段

首先需要把查询计划转换为 Group 和 Group Expression,并初始化 Memo 搜索空间。可以看到共有 6 个 Group,每个 Group 都有一个 Group Expression。

简单来说:对于逻辑等价的,可以产生相同结果的 Logical Expression 和 Physical Expression 的集合称为 Group,Group Expression 则包括 Logical Group Expression 和 Physical Group Expression,每一种 Group Expression 表示一种等价候选计划。

在这里,我们为了简化描述,只考虑 Join 的 Order,对于算子的物理实现如下所示:

  • Join算子物理实现分为Nested Loop、Hash join、Sort Merge Join。
  • Scan算子物理实现分为Index Scan, Seq Scan。
  • Output算子默认一种物理实现。

Exploration阶段

调度器调度搜索任务,执行搜索流程,扩展搜索空间。可以看到随着搜索算法的不断迭代。

可以看到,在应用了 Join 的结合律之后,新产生了 Group7 和 Group8 这俩个 Group。

Transformations阶段

同时应用了 Join 的交换律之后,Group5 和 Group3 里面新增了很多等价的 Logical Group Expression;同样 Group7 和 Group8 里面也有等价的 Logical Group Expression。

Implementation阶段

考虑三种分布式 Join 实现:Hash Join 和 NestLoop Join,Merge Sort Join 。Scan 实现 Index Scan、Seq Scan。这样就生成了完整的搜索空间。

Property enforcement阶段

对每一种物理执行计划,去 Enforce 必要的属性,从而满足分布式执行计划的要求,然后再调用代价估算模块,去计算每一种分布式计划的代价,并把代价最小的计划标记为最优解,也就是 Winner。当每一个 Group 的 Winner 都被计算出来之后,将每个 Winner 串接起来,就是最优的分布式执行计划。

下图中红色意味着 Winner,表示的是在满足某种属性要求的情况下,代价最低最低的物理执行计划。

我们遍历每个 Group 的 Winner,将 Winner 串接起来,就形成了最优的分布式执行计划。

基于代价优化器面临的挑战

精准的统计信息和代价模型

统计信息和代价模型是查询优化器基础模块,它主要负责给执行计划计算代价。精准的统计信息和代价模型一直是数据库系统想要解决的难题,主要原因如下:

  • 统计信息:在数据库系统中,统计信息搜集主要存在两个问题。首先,统计信息是通过采样搜集,所以必然存在采样误差。其次,统计信息搜集是有一定滞后性的,也就是说在优化一个 SQL 查询的时候,它使用的统计信息是系统前一个时刻的统计信息。
  • 选择率计算和中间结果估计:选择率计算一直以来都是数据库系统的难点,学术界和工业界一直在研究能使选择率计算变得更加准确的方法,比如动态采样,多列直方图等计划,但是始终没有解决这个难题,比如连接谓词选择率的计算目前就没有很好的解决方法。
  • 代价模型:目前主流的数据库系统基本都是使用静态的代价模型,比如静态的 buffer 命中率,静态的 IO RT,但是这些值都是随着系统的负载变化而变化的。如果想要一个非常精准的代价模型,就必须要使用动态的代价模型。

海量的计划空间

复杂查询的计划空间是非常大的,在很多场景下,优化器甚至没办法枚举出所有等价的执行计划。下图展示了星型查询等价逻辑计划个数 (不包含笛卡尔乘积的逻辑计划),而优化器真正的计划空间还得正交上算子物理实现,基于代价的改写和分布式计划优化。在如此海量的计划空间中,如何高效的枚举执行计划一直是查询优化器的难点。比如下面的情况。

1
T1 JOIN T2

根据Join交换律 [T1, T2], [T2, T1]

根据物理实现算法 Nested Loop Join(NL), Hash Join(HJ), Sort Merge Join(SMJ)

[T1(NL), T2(NL)]、[T1(NL), T2(HJ)]、[T1(NL), T2(SMJ)]

[T1(HJ), T2(NL)]、[T1(HJ), T2(HJ)]、[T1(HJ), T2(SMJ)]

[T1(SMJ), T2(NL)]、[T1(SMJ), T2(HJ)]、[T1(SMJ), T2(SMJ)]

[T2(NL), T1(NL)]、[T2(NL), T1(HJ)]、[T2(NL), T1(SMJ)]

[T2(HJ), T1(NL)]、[T2(HJ), T1(HJ)]、[T2(HJ), T1(SMJ)]

[T2(SMJ), T1(NL)]、[T2(SMJ), T1(HJ)]、[T2(SMJ), T1(SMJ)]

普通的Join语句就有18种等价的情况,如果是复杂的场景,等价的情况会几何倍增长。如何在这些海量的等价计划中找到一个最优的计划并且找到最优计划的时间比较少是比较困难的,所以通常情况下只会找到一个相对最优的计划。

后续文章会说明这些情况业界内是如何解决的。

结论

数据库的大脑是优化器,优化器能够将所有信息串联在一起,通过理解系统中数据的相关性以及用户的企图,并通过机器的能力去充分的分析各种各样的环境,在分布式场景中以最高效的的方式实现对于用户运算的执行。所以对于数据敏感的代价优化器是非常重要的,基于代价的优化中有一些功能是非常重要的,本文没有明确的讲述包含代价估算、统计信息收集、选择率计算和中间结果估计、并行计划等等,再后续的文章中再进行描述。

参考资料

  1. The Cascades Framework for Query Optimization
  2. Orca: A Modular Query Optimizer Architecture for Big Data
  3. CMU SCS 15-721 (Spring 2019) : Optimizer Implementation (Part II)
分享大数据行业的一些前沿技术和手撕一些开源库的源代码

微信公众号名称:技术茶馆

微信公众号ID : Night_ZW