真正的优化器
上边虽然说了那么多,但是都偏于理论,因为我是一个开发者不是一个研究员,所以我更喜欢说一些具体的例子。
让我们来看看SQLite优化器具体是怎么优化的。它非常简单,因此它也采用了最简单的优化方法:基于贪婪算法,并且应用一些额外的规则来尽可能减少执行计划的可能性:
- SQLite从来不会重新排序CROSS JOIN的表;
- join采用nested joins;
- outer joins通常是以它们出现的顺序被评估
- …
- 在3.8.0版本之前,使用Nearest neighbor(关系型数据库是如何工作的?(19)已经介绍过这个算法)算法来寻找最优计划。
- 从3.8.0版本开始,使用N Nearest Neighbors贪婪算法来寻找最优计划。
下边我们介绍下DB2这个企业级数据库是怎么优化的,选择它是因为DB2是我转向大数据之前使用的最后一个数据库。如果我们查看其官方文档,我们会发现其优化器分为7个不同的优化层次:
- 以下优化层次使用贪婪算法
- 最小优化(minimal optimization),使用索引扫描和nested-loop join避免一些查询重写。
- 低层次优化(low optimization)。
- 完整优化(full optimization)。
- 以下优化层次使用动态编程
- 适度优化(moderate optimization and rough approximation)。
- 完整优化( full optimization),使用所有的启发式手段。
- 完整优化( full optimization),并不使用任何启发式手段。
- 混合优化(maximal optimization),尝试所有可能的join顺序,包括笛卡尔积。
我们看到DB2同时使用了贪婪算法和动态编程,当然它们并不共享启发法,因为查询优化是一个数据库之所以强大的核心。
仅供参考,默认优化层次使5,默认优化器会适用如下特性:
- 所有可能的统计信息,包括值分布统计。
- 所有的查询重写规则被应用,但计算加强规则不会被应用,它只有在很少情况下才会被应用。
- 基于以下愿意你,使用动态编程join枚举。
- 限制组合内关系的使用。
- 限制笛卡尔积的使用。
- 很多访问方法,包括预读列表、index ANDing、物化查询表路由等。
默认DB2使用动态编程来限制需要试探的join顺序数量。其他的条件(GROUP BY, DISTINCT…)使用简单的规则来处理。
查询计划缓存
因为一个创建一个查询计划很耗时,所以大部分数据库都会缓存查询计划避免反复的计算相同的查询计划,这是一个很大的议题,因为数据库需要知道缓存的执行计划什么时候过期。基本的做法是维护一个时间(译者注:其实就是过期时间,类似Redis的过期时间)的阈值,如果在阈值内统计到一个表的数据变化那么这张表的查询计划缓存会被清理掉。
查询执行器
经过了之前介绍的漫长步骤,我们获到了一个经过优化的执行计划,最终被编译为一段可行性的代码。如果这个时候服务器有足够的资源(内存、CPU…),那么执行器就会执行这段代码。在执行计划中的操作(JOIN, SORT BY …)以串行或者并发发行执行。为了读或写数据,执行器需要和数据管理器交互,下一章节我们介绍数据管理器。