PostgreSQL7.0手册-开发者手册 -61. PostgreSQL 内部概貌
-----------------------------------------------------------------------------
规划器/优化器
规划器/优化器的任务是创建一个优化了的执行规划。它首先合并所有对出现在查询里的关系的扫描和联合的方法。这样创建的所有的路径都导致相同的结果,而优化器的任务就是计算每个路径的开销并且找出开销最小的那条路径。
生成可能的规划
规划器/优化器以查询里出现的关系上定义的索引的类型为基础,判断应该生成哪些规划。对一个关系总是可以进行一次顺序查找,所以总是会创建只使用顺序查找的规划。假设一个关系上定义着一个索引(例如 B-tree 索引),并且一条查询包含约束 relation.attribute OPR constant。如果 relation.attribute 碰巧匹配 B-tree 索引的关键字并且 OPR 又不是 '<>',则将会创建另一个使用 B-tree 索引扫描该关系的规划。如果还有别的索引,而且查询里面的约束又和那个索引的关键字匹配,则还会生成更多的规划。
在找完扫描一个关系的所有可能的规划后,接着创建联接各个关系的规划。规划器/优化器认为只有在每两个关系之间存在联接,对应地在 where 条件里存在一条 join 子句(也就是说,存在象 where rel1.attr1=rel2.attr2 这样的约束)。规划器/优化器为它们认为可能的所有的联接关系对生成一个规划。有三种可能的联接策略:
嵌套的反复联接(nested iteration join):对左边的关系里面找到的每条记录都对右边关系进行一次扫描。这个策略容易实现,但是可能会很耗费时间。
融合排序联接(merge sort join):在联接开始之前,每个关系都对联接字段进行排序。然后两个关系融合到一起,认为两个关系都对联接字段进行了排序。这种联合更有吸引力,因为每个关系都只用扫描一次。
哈希联接(hash join):右边的关系首先对它的联接字段进行哈希运算。然后扫描左边的关系,并将找到的每条记录的合适的值作为哈希键字用以定位右边关系里的记录。
规划的数据结构
我们在这里对规划里出现的节点做一些简单描述。图 \ref{plan} 显示了为例子 \ref{simple_select} 里的查询生成的规划。
规划的顶端节点是一个 MergeJoin 节点,它有两个下级节点,一个附加在域 lefttree 上,而另一个附加在域 righttree 上。每个子节点代表一个联接的关系。如上面所述,一个融合联接要求每个关系先被排序。这就是为什么我们在每个子规划里有一个 Sort 节点的原因。查询里附加的条件(s.sno > 2)被尽可能地向下推,并且附加在对应的子规划的叶节点 SeqScan 的 qpqual 域上。
附加在节点 MergeJoin 的域 mergeclauses 的列表包含关于联接属性的信息。 mergeclauses 列表里(还有 targetlist 里)出现的 VAR 节点的数据域 varno 的值 65000 和 65001 意味着不考虑当前节点的记录,而是使用下一"更深"节点(也就是说,子规划的顶端节点)的记录。
请注意图 \ref{plan} 里的每个 Sort 和 SeqScan 节点都有一个 targetlist,但因为空间不够,只有 MergeJoin 节点的画出来了。
规划器/优化器执行的另一个任务是填补 Expr 和 Oper 节点里的操作符id。如上所述,Postgres 支持广范围的不同的数据类型,甚至可以使用用户定义类型。为了能够维护这些数目巨大的函数和操作符,有必要把它们存放在系统表里。每个函数和操作符有一个唯一的操作符 id。根据在资格条件等地方使用的字段的类型,必须选用合适的操作符 id。
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
执行器
执行器接收由规划器/优化器传递过来的规划,然后开始处理顶端节点。在我们给出的例子里面(在例子 \ref{simple_select} 里给出的查询),顶端节点是一个 MergeJoin 节点。
在进行任何融合之前,首先要抓取两条记录(从每个子规划里抓一条)。所以执行器递归地调用它自己以处理子规划(它从附加在 lefttree 上的子规划开始)。新的顶端节点(左边子规划的顶端节点)是一个 SeqScan 节点,同样必须先抓取一条记录然后才能处理节点本身。执行器再次递归地调用自身,处理附加在 lefttree 的 SeqScan 节点上的子规划。
现在新的顶端节点是一个 Sort 节点。因此,要对整个关系进行排序。当第一次访问 Sort 节点时,执行器开始从 Sort 节点的子查询里抓取记录并把它们在一个临时关系里面(在内存或文件中)排序。(对 Sort 节点的进一步检查将总是从排好序的临时关系里返回一条记录。)
每当处理 Sort 节点需要新的记录时,都会为作为子规划附加上来的 SeqScan 节点递归地调用执行器。然后对该关系(在内部通过数据域 scanrelid 给出的值引用)进行扫描,检索下一条记录。如果记录满足附加在 qpqual 上的条件树则将其返回,否则抓取下一条记录知道条件被满足。如果处理到了关系的最后一条记录,则返回一个 NULL 指针。
在 MergeJoin 的 lefttree 返回一条记录后,用同样方法处理 righttree。如果两条记录都存在了,执行器就处理 MergeJoin 节点。每当有一个子规划需要一条新的记录时,都进行一次执行器的递归调用以获取记录。如果可以创建一条联合的记录,那么就把这条记录返回并且完成一次完整的规划树的处理。
现在,上面描述的步骤对每条记录执行一次,直到对 MergeJoin 节点的处理返回一个 NULL 指针,表示我们已经处理完毕时为止。
------------------------------------------------------------------------------
规划器/优化器
规划器/优化器的任务是创建一个优化了的执行规划。它首先合并所有对出现在查询里的关系的扫描和联合的方法。这样创建的所有的路径都导致相同的结果,而优化器的任务就是计算每个路径的开销并且找出开销最小的那条路径。
生成可能的规划
规划器/优化器以查询里出现的关系上定义的索引的类型为基础,判断应该生成哪些规划。对一个关系总是可以进行一次顺序查找,所以总是会创建只使用顺序查找的规划。假设一个关系上定义着一个索引(例如 B-tree 索引),并且一条查询包含约束 relation.attribute OPR constant。如果 relation.attribute 碰巧匹配 B-tree 索引的关键字并且 OPR 又不是 '<>',则将会创建另一个使用 B-tree 索引扫描该关系的规划。如果还有别的索引,而且查询里面的约束又和那个索引的关键字匹配,则还会生成更多的规划。
在找完扫描一个关系的所有可能的规划后,接着创建联接各个关系的规划。规划器/优化器认为只有在每两个关系之间存在联接,对应地在 where 条件里存在一条 join 子句(也就是说,存在象 where rel1.attr1=rel2.attr2 这样的约束)。规划器/优化器为它们认为可能的所有的联接关系对生成一个规划。有三种可能的联接策略:
嵌套的反复联接(nested iteration join):对左边的关系里面找到的每条记录都对右边关系进行一次扫描。这个策略容易实现,但是可能会很耗费时间。
融合排序联接(merge sort join):在联接开始之前,每个关系都对联接字段进行排序。然后两个关系融合到一起,认为两个关系都对联接字段进行了排序。这种联合更有吸引力,因为每个关系都只用扫描一次。
哈希联接(hash join):右边的关系首先对它的联接字段进行哈希运算。然后扫描左边的关系,并将找到的每条记录的合适的值作为哈希键字用以定位右边关系里的记录。
规划的数据结构
我们在这里对规划里出现的节点做一些简单描述。图 \ref{plan} 显示了为例子 \ref{simple_select} 里的查询生成的规划。
规划的顶端节点是一个 MergeJoin 节点,它有两个下级节点,一个附加在域 lefttree 上,而另一个附加在域 righttree 上。每个子节点代表一个联接的关系。如上面所述,一个融合联接要求每个关系先被排序。这就是为什么我们在每个子规划里有一个 Sort 节点的原因。查询里附加的条件(s.sno > 2)被尽可能地向下推,并且附加在对应的子规划的叶节点 SeqScan 的 qpqual 域上。
附加在节点 MergeJoin 的域 mergeclauses 的列表包含关于联接属性的信息。 mergeclauses 列表里(还有 targetlist 里)出现的 VAR 节点的数据域 varno 的值 65000 和 65001 意味着不考虑当前节点的记录,而是使用下一"更深"节点(也就是说,子规划的顶端节点)的记录。
请注意图 \ref{plan} 里的每个 Sort 和 SeqScan 节点都有一个 targetlist,但因为空间不够,只有 MergeJoin 节点的画出来了。
规划器/优化器执行的另一个任务是填补 Expr 和 Oper 节点里的操作符id。如上所述,Postgres 支持广范围的不同的数据类型,甚至可以使用用户定义类型。为了能够维护这些数目巨大的函数和操作符,有必要把它们存放在系统表里。每个函数和操作符有一个唯一的操作符 id。根据在资格条件等地方使用的字段的类型,必须选用合适的操作符 id。
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
执行器
执行器接收由规划器/优化器传递过来的规划,然后开始处理顶端节点。在我们给出的例子里面(在例子 \ref{simple_select} 里给出的查询),顶端节点是一个 MergeJoin 节点。
在进行任何融合之前,首先要抓取两条记录(从每个子规划里抓一条)。所以执行器递归地调用它自己以处理子规划(它从附加在 lefttree 上的子规划开始)。新的顶端节点(左边子规划的顶端节点)是一个 SeqScan 节点,同样必须先抓取一条记录然后才能处理节点本身。执行器再次递归地调用自身,处理附加在 lefttree 的 SeqScan 节点上的子规划。
现在新的顶端节点是一个 Sort 节点。因此,要对整个关系进行排序。当第一次访问 Sort 节点时,执行器开始从 Sort 节点的子查询里抓取记录并把它们在一个临时关系里面(在内存或文件中)排序。(对 Sort 节点的进一步检查将总是从排好序的临时关系里返回一条记录。)
每当处理 Sort 节点需要新的记录时,都会为作为子规划附加上来的 SeqScan 节点递归地调用执行器。然后对该关系(在内部通过数据域 scanrelid 给出的值引用)进行扫描,检索下一条记录。如果记录满足附加在 qpqual 上的条件树则将其返回,否则抓取下一条记录知道条件被满足。如果处理到了关系的最后一条记录,则返回一个 NULL 指针。
在 MergeJoin 的 lefttree 返回一条记录后,用同样方法处理 righttree。如果两条记录都存在了,执行器就处理 MergeJoin 节点。每当有一个子规划需要一条新的记录时,都进行一次执行器的递归调用以获取记录。如果可以创建一条联合的记录,那么就把这条记录返回并且完成一次完整的规划树的处理。
现在,上面描述的步骤对每条记录执行一次,直到对 MergeJoin 节点的处理返回一个 NULL 指针,表示我们已经处理完毕时为止。
------------------------------------------------------------------------------

