49.5. 规划器/优化器
规划器/优化器的任务是创建一个最佳的执行计划。一个给定的SQL查询(今后将是一个查询树)实际上可以以很多种不同的方式被执行,其中的每一种都会产生相同的结果集。如果在计算上可行,查询优化器将检查这些可能的执行计划中的每一个,最后选择其中被期望“跑得最快的”那一个。
注意: 在某些情况下,检查一个查询的每一种可能的执行方式会耗费非常多的时间和内存空间。特别是当查询涉及到大量连接操作时。为了能在合理的时间内决定一个合理的(不一定是最佳的)查询计划,当连接数量超过一个阈值(见geqo_threshold)时PostgreSQL使用了一种遗传查询优化器 (见第 58 章)。
规划器搜索过程实际上依靠称为路径的数据结构工作,它是一种缩短版的计划,其中只包含规划器做决定所需要的信息。当最低代价的路径被确定后,一个全功能的计划树将被建立并传递给执行器。这表示所期望的执行计划已经拥有足够的细节以供执行器执行它。在本节剩下的部分,我们将忽略路径和计划之间的区别。
49.5.1. 生成可能的计划
规划器/优化器从扫描查询中用到的每一个单独的关系(表)开始生成计划。可能的计划根据每一个关系上可用的索引决定。在一个关系上总是有执行一个顺序扫描的可能,因此一个顺序扫描计划总是会被创建。假设在一个关系上定义有一个索引(例如一个B-tree索引)并且查询包含限制relation.attribute OPR constant。如果relation.attribute正好匹配该B-tree索引的键并且OPR是该索引的操作符类之一,另一个使用B-tree索引扫描该索引的计划将被创建。如果还有索引存在且查询中的限制正好匹配一个索引的键,其他计划也会被考虑。如果有索引的顺序能匹配ORDER BY子句(如果有)或者对于归并连接有用(见下文),也会为该索引创建索引扫描计划。
如果查询需要连接两个或更多关系,在所有扫描单个关系的可能计划都被找到后,连接计划将会被考虑。三种可用的连接策略是:
嵌套循环连接: 对左关系找到的每一行都要扫描右关系一次。这种策略最容易实现但是可能非常耗时(但是,如果右关系可以通过索引扫描,这将是一个不错的策略。因为可以用左关系当前行的值来作为右关系上索引扫描的键)。
归并连接:在连接开始之前,每一个关系都按照连接属性排好序。然后两个关系会被并行扫描,匹配的行被整合成连接行。由于这种连接中每个关系只被扫描一次,因此它很具有吸引力。它所要求的排序可以通过一个显式的排序步骤得到,或使用一个连接键上的索引按适当顺序扫描关系得到。
哈希连接:右关系先被扫描并且被载入到一个哈希表,使用连接属性作为哈希键。接下来左关系被扫描,扫描中找到的每一行的连接属性值被用作哈希键在哈希表中查找匹配的行。
当查询涉及两个以上的关系时,最终结果必须由一个连接步骤树构成,每个连接步骤有两个输入。规划器会检查不同可能的连接序列来找到代价最小的那一个。
如果查询是用的关系数少于geqo_threshold,将使用一次接近穷举的搜索来查找最好的连接顺序。如果任何两个关系在WHERE条件中存在一个相应的连接子句(即存在类似于where rel1.attr1=rel2.attr2的限制),规划器会有限考虑它们之间的连接。没有任何连接子句的连接对只有在别无选择时才会被考虑,即一个关系没有任何可用的对于其他关系的连接子句。对规划器所考虑的每一个连接对会生成所有可能的计划,其中代价(被估计为)最低的一个将被选择。
当连接关系数超过geqo_threshold时,连接序列将考虑通过启发式方法来确定,详见第 58 章。否则处理将和前面相同。
成品计划树包含基本关系的顺序或索引扫描,外加所需的嵌套循环、归并或哈希连接节点,以及任何所需的辅助步骤,例如排序节点或聚集函数计算节点。这些节点中的大部分具有执行选择(丢弃不符合指定布尔条件的行)和投影(根据指定列值计算派生列,即标量表达式的计算)的能力。规划器的职责之一就是在计划树最合适的节点上附加来自于子句的选择条件和需要的输出表达式。