52.5. 规划器/优化器#
规划器/优化器的任务是创建最佳执行计划。给定的 SQL 查询(以及查询树)实际上可以用多种不同的方式执行,每种方式都会产生相同的结果集。如果在计算上可行,查询优化器将检查这些可能的执行计划中的每一个,最终选择预计运行最快的执行计划。
注意
在某些情况下,检查查询可以执行的每种可能方式将花费大量时间和内存。特别是,当执行涉及大量联接操作的查询时,就会发生这种情况。为了在合理的时间内确定合理的(不一定是最优的)查询计划,PostgreSQL在联接数超过阈值时使用遗传查询优化器(请参阅第 62 章),请参阅geqo_threshold)。
规划器的搜索过程实际上使用称为路径的数据结构,它只是计划的精简表示,仅包含规划器做出决策所需的信息。在确定最便宜的路径后,将构建一个成熟的计划树以传递给执行器。这以执行器运行所需的足够细节表示所需的执行计划。在本节的其余部分中,我们将忽略路径和计划之间的区别。
52.5.1. 生成可能的计划#
规划器/优化器首先为查询中使用的每个单独关系(表)生成扫描计划。可能的计划由每个关系上可用的索引决定。始终有可能对关系执行顺序扫描,因此始终创建一个顺序扫描计划。假设在关系上定义了一个索引(例如 B 树索引),并且查询包含限制relation.attribute OPR constant
。如果relation.attribute
恰好与 B 树索引的键匹配,并且OPR
是索引的运算符类中列出的运算符之一,则使用 B 树索引创建另一个计划来扫描关系。如果存在更多索引,并且查询中的限制恰好与索引的键匹配,则将考虑更多计划。还为具有可以与查询的ORDER BY
子句(如果存在)匹配的排序顺序或可能对合并联接有用的排序顺序的索引生成索引扫描计划(见下文)。
如果查询需要联接两个或更多关系,则在为扫描单个关系找到所有可行计划后,将考虑联接关系的计划。三种可用的联接策略是
嵌套循环联接:对于左关系中找到的每一行,扫描一次右关系。此策略易于实现,但可能非常耗时。(但是,如果可以通过索引扫描扫描右关系,则这可能是一个好策略。可以将左关系当前行的值用作右关系索引扫描的键。)
合并联接:在联接开始之前,在联接属性上对每个关系进行排序。然后并行扫描两个关系,并将匹配的行组合起来形成联接行。这种联接具有吸引力,因为每个关系只需要扫描一次。所需的排序可以通过显式排序步骤或使用联接键上的索引按正确顺序扫描关系来实现。
哈希联接:首先扫描右关系并将其加载到哈希表中,使用其联接属性作为哈希键。接下来扫描左关系,并将找到的每一行的适当值用作哈希键,以在表中找到匹配的行。
当查询涉及两个以上的关系时,最终结果必须通过联接步骤树建立,每个步骤有两个输入。规划器检查不同的可能联接序列,以找到最便宜的一个。
如果查询使用的关系少于geqo_threshold,则会进行近乎穷举的搜索以找到最佳连接顺序。计划程序优先考虑任何两个关系之间的连接,因为在WHERE
限定符中存在相应的连接子句(即,存在类似于where rel1.attr1=rel2.attr2
的限制)。只有在没有其他选择时才会考虑没有连接子句的连接对,即,特定关系没有可用于任何其他关系的连接子句。计划程序考虑的每个连接对都会生成所有可能的计划,并选择(估计为)最便宜的计划。
当geqo_threshold
超过时,所考虑的连接顺序由启发式方法决定,如第 62 章中所述。否则,该过程是相同的。
完成的计划树由基本关系的顺序或索引扫描组成,加上嵌套循环、合并或哈希连接节点(根据需要),加上任何需要的辅助步骤,例如排序节点或聚合函数计算节点。这些计划节点类型中的大多数还具有执行选择(丢弃不满足指定布尔条件的行)和投影(基于给定列值计算派生列集,即,在需要时计算标量表达式)的附加能力。计划程序的职责之一是将WHERE
子句中的选择条件和所需输出表达式的计算附加到计划树的最合适节点。