PostgreSQL 优化器代码概览

简介
PostgreSQL 的开发源自上世纪80年代,它最初是 Michael Stonebraker 等人在美国国防部支持下创建的POSTGRE项目。上世纪末,Andrew Yu 等人在它上面搭建了第一个SQL Parser,这个版本称为Postgre95,也是加州大学伯克利分校版本的PostgreSQL的基石[1]。

我们今天看到的 PostgreSQL 的优化器代码主要是 Tom Lane 在过去的20年间贡献的,令人惊讶的是这20年的改动都是持续一以贯之的,Tom Lane 本人也无愧于“开源软件十大杰出贡献者”的称号。

但是从今天的视角,PostgreSQL 优化器不是一个好的实现,它用C语言实现,所以扩展性不好;它不是 Volcano 优化模型的[2],所以灵活性不好;它的很多优化复杂度很高(例如Join重排是System R[3]风格的动态规划算法),所以性能不好。

无论如何,PostgreSQL 是优化器的优秀实现和创新源头(想象 Greenplum 和 ORCA 等一系列项目),它的一些优化手段和代码结构在今天仍然是值得借鉴的,包括:

参数化路径,作用于indexed lookup join
分区裁剪和并行优化
强一致的cardinality estimation保证
本文尝试快速地浏览一遍 PostgreSQL 优化器的代码,和现代优化器比较优缺点。大部分的 PostgreSQL 优化器代码来自于 https://github.com/postgres/postgres/tree/master/src/backend/optimizer 。 我们提到现代优化器主要指的是 Apache Calcite 和 ORCA。

术语解释
Datum
Qual
Path
关键数据结构
查询
Query: Parse Tree,优化器的输入
RangeTblEntry: Parse Tree的一个节点,它描述了一个数据集的视图,这个数据集可能来源于某个子查询、Join、Values或任何一个简单关系代数表达式。Join实现需要把它的输入都表达为 RangeTblEntry (以下简称RTE)。
执行计划
PlannedStmt: 执行计划的顶层节点
PlannerInfo: 优化器的上下文信息。它是一个树形结构,用parent_root变量指向父节点。一个Query包含一个或多个PlannerInfo,每次Join切分一次树节点。它包含RelOptInfo的指针。
RelOptInfo: 优化器的核心数据结构,包含一个子查询的Path集合等信息。这个概念对应于ORCA的Group或Calcite中的Set。
Path: 区别于Parser称Relational Expression为Node,Optimizer称优化时的关系代数为Path。Path是物理计划,它包含优化器对于单个关系代数的理解,包括并行度、PathKey和cost。
PathKey: 排序属性。这个概念相当于Volcano中的Physical Property或Calcite中的Trait。因为 PostgreSQL 是单机数据库,仅用排序属性就可以表达所有算法的需求和实现特性。对于分布式数据库,通常还需要分布属性。
主流程
子查询上拉

因为优化的单元(RelOptInfo)是子查询,合并子查询可以简化优化流程。关联的过程包括:

pull_up_sublinks: 将可转换的 ANY/EXISTS 子句转换为 (anti-)semi-join 。一些优化器称这个过程为de-correlation。
pull_up_subqueries: 将可上拉的子查询上拉到当前查询,删除原来的子查询。如果子查询是一个 Join ,这个操作相当于打平 binary join 到 multi join。
EquivalenceClass解析

Equivalence Class(EC)是 qual 的术语,它指代的是 expression 的等价性。例如,expression

a = b AND b = c
则称 {a, b, c} 是一个EC。特别地,在 PostgreSQL 中,expression

a = b AND b = 5
只生成简化的EC:{a = 5} {b = 5}

EC是很关键的数据结构,它的应用场景包括:

在 Join 时,EC用来决定 Join Key,它决定了 Outer Join 简化和PathKey设定
在 Join 时决定 qual 穿越
决定参数化路径的参数列表
匹配主-外键约束,以便优化(Join的)cardinality estimation
EC是一个树形结构,每个节点是一个EC,并链接到它合并的父节点上。考虑a = b AND b = c的例子,最后的EC tree表达为

{a, b, c}
|- {a, b}
|- {b, c}
其中,每个EC内部的expression称为EquivalenceMember(EM)。

生成 EC 的入口是 generate_base_implied_equalities ,它从 query_planner 调入。也就是说,EC是在规划Join的前一刻生成的,这个阶段解析EC的代价最小,但是也决定了EC只能应用于Join优化。

Join重排

(有关Join重排的背景知识可以参考我之前的文章 SQL优化器原理 - Join重排)

make_rel_from_joinlist是Join重排的入口,当前版本的 PostgreSQL 有三种算法:

你可以插入一个自定义的Join重排算法
GEQO: Genetic Optimization (基因算法,或遗传算法[4]),是一种非穷举的最优化算法实现
Standard:一个略微剪枝的动态规划算法。
默认在12路及以上的复杂Join中会打开GEQO。可以在postgresql.conf中修改参数

geqo = on
geqo_threshold = 12
控制GEQO设定。

现在让我们检查 Standard 算法。它的主入口在 join_search_one_level ,每次在已生成的局部计划的基础上:

按EC检查未加入的Join input,加入到生成的局部计划,这个操作仅产生 Left-deep-tree
从未加入局部计划的Join input里找到有EC的两个input,生成额外的局部计划,用于生成Bushy-tree
如果当前层找不到任何EC关联,生成笛卡尔积。
上述描述已经足够复杂,让我们总结一下 Standard 算法:

Standard 算法仍然是一个穷举的动态规划算法
它对 a-b/b-a 镜像去重,同时当EC存在时不考虑笛卡尔积,这些工程上的降级有效降低了搜索复杂度
路径生成和动态规划

如上所述,优化过程集中在对子查询(RelOptInfo)的重建过程,这可以理解为逻辑优化过程,这通常是跨关系代数操作符的、比较复杂的优化。事实上 PostgreSQL 也同步在做物理优化。

物理优化就是将 Path 加入 RelOptInfo。考虑Join,物理优化的入口在 populate_joinrel_with_paths。对每个JoinRel(Join RelOptInfo),考虑:

sort_inner_and_outer:两边排序的MergeJoin路径
match_unsorted_outer:Null-generating side不排序路径,包括 MergeJoin 和 NestedLoopJoin 。
hash_inner_and_outer:两边哈希的HashJoin路径。
有趣的点是HashJoin路径(hash_inner_and_outer),顾名思义,它要求Join两边都计算哈希值。在生成Path过程中,需要计算两边的参数信息。例如A join B on A.x = B.y,对于A来说,x是参数,对于B是y。如果选定A作为Probe side,一旦B上有y的索引,每次x的probe将以参数的形式传递给y的索引。通过调用 get_joinrel_parampathinfo 来产生参数信息。

路径生成的入口是add_path,每次生成路径,需要更新RelOptInfo的最佳路径和最小代价以便后续动态规划选择全局最优。

流程图

planner
|- subquery_planner 迭代的子查询优化
|- pull_up_sublinks de-correlation
|- pull_up_subqueries 子查询上拉
|- preprocess_expression Query/PlannerInfo 结构解析,常量折叠
|- remove_useless_groupby_columns
|- reduce_outer_joins Outer Join退化
|- grouping_planner
|- plan_set_operations SetOp优化
|- query_planner 子查询优化主入口
|- generate_base_implied_equalities 生成/合并EC
|- make_one_rel Join优化入口
|- set_base_rel_pathlists 生成Join RelOptInfo列表
|- make_rel_from_joinlist Join重排和规划
|- standard_join_search 标准Join重排算法
|- join_search_one_level
|- make_join_rel 生成JoinRel和对应的Path
|- create_XXX_paths Grouping、window等其他expression优化
讨论
扩展性和灵活性

首先,PostgreSQL 的优化器代码可以说非常复杂,这已经极大限制了它的扩展性和灵活性。如果看一眼这部分代码的更新日志,会发现里面的作者已经只有少数几个人。

一部分扩展性限制是由编程语言带来的,因为C语言本身不容易扩展,这意味着大部分时候想要添加一个新的Node或Path变得很不容易,你需要定义一系列的数据结构、Cardinality Estimation逻辑、并行逻辑和Path解释逻辑。并没有类似interface这样的抽象指导你该怎么做。虽然,PostgreSQL 的代码已经写得非常工整,而且也有很多的文章告诉你该怎么做(比如 Introduction to Hacking PostgreSQL 和 The Internals of PostgreSQL)。

另一部分扩展性限制是优化器本身的结构带来的。现代的优化器基本都是Volcano Model[2]的(例如SQL Server和Oracle,就像他们声称的那样),而 PostgreSQL 没有实现为 Volcano Model 这种 Generic purpose,pluggable 的形式。影响包括:

无法做逻辑和物理优化的互操作。例如前文说到的,一个Join产生的EC必须和它紧跟的 RTE 结合才能产生 IndexedLookupJoin,而不像其他优化器可以把这个 EC (它在某种意义上已经是物理计划)下推到合适的逻辑计划上,指导它做物理计划转换。
不容易定制优化规则。
开发者关注的切片太大,开发一个优化规则除了关注优化本身,不得不学习其他优化规则的数据结构、动态规划更新、RelOptInfo新建和清理,甚至内存分配本身。
PostgreSQL 仍然提供了部分手写的 Plugin Point,包括:

可定制的Join重排算法
可定制的PathKey生成算法
定制的Join Path生成算法
等等。

性能

虽然没有实验,但是 PostgreSQL 在优化上的性能可以想像是比较好的,这很大程度是用灵活×××换来的。

首先,不像 Volcano Optimizer ,PostgreSQL 优化器不需要不断生成中间节点,它的 RelOptInfo 的数量是相对稳定的(约等于Join的数量)。它的最优计划搜索以 RelOptInfo 为单位,如果 Join 重排不产生大量 RelOptInfo ,搜索宽度很低。

其次,RelOptInfo 简化了大量跨 Relational Expression 优化的细节,比起 Calcite 这种按 Relational Expression 来组织等价路径集合的方案, 它的搜索宽度进一步降低了。从等价集合的数量看, PostgreSQL 的搜索宽度大概比 Calcite 要低一个数量级,当然,如上所述,这是用更多优化可能性作为交换的。

最后,PostgreSQL 在优化阶段糅合了很多业务逻辑,在提高代码阅读的难度同时,也相应加快的优化效率。在优化过程中,PostgreSQL会不间断地做常量折叠、PathKey去重、Union打平、子查询打平……这些操作不会应用在memo里。

对比 Calcite/Orca ,PostgreSQL 的优化更快,更适合事务性场景。不过我无法判断 Calcite/Orca 在做了适当的剪枝和优化规则糅合后,是否也能支持事务场景。

注释
[1] Brief History of PostgreSQL, https://www.postgresql.org/docs/current/history.html

[2] Graefe, G., & McKenna, W. J. (1993). The Volcano Optimizer Generator: Extensibility and Efficient Search. Proceedings of the Ninth International Conference on Data Engineering, (April), 209–218. https://doi.org/10.1109/ICDE.1993.344061

[3] Selinger, P. Griffiths, et al. "Access path selection in a relational database management system." Proceedings of the 1979 ACM SIGMOD international conference on Management of data. ACM, 1979.

[4] Steinbrunn, M., Moerkotte, G., & Kemper, A. (n.d.). Optimizing Join Orders, 1–55.

原文地址:http://blog.51cto.com/14031893/2351350

时间: 2024-08-30 10:23:51

PostgreSQL 优化器代码概览的相关文章

优化 PHP 代码建议

1.如果能将类的方法定义成static,就尽量定义成static,它的速度会提升将近4倍.2.$row[’id’] 的速度是$row[id]的7倍.3.echo 比 print 快,并且使用echo的多重参数(译注:指用逗号而不是句点)代替字符串连接 1.如果能将类的方法定义成static,就尽量定义成static,它的速度会提升将近4倍. 2.$row[’id’] 的速度是$row[id]的7倍. 3.echo 比 print 快,并且使用echo的多重参数(译注:指用逗号而不是句点)代替字符

Oracle_sql优化基础——优化器总结

优化器的基础: 1.Oracle里的优化器: 优化器是Oracle数据库中内置的一个核心子系统,优化器的目的就是按照一定的判断原则来得到它认为目标sql在当前情形下最高效的执行路径,也就是说是为了得到目标sql的执行计划. Oracle数据库的优化器分为:RBO和CBO两种类型: RBO:基于规则的优化器(在得到sql执行计划时,RBO所用的判断原则为一组内置的规则) CBO:基于成本的优化器(在得到sql执行计划时,CBO所用的判断原则为成本,它会从目标sql诸多可能的执行路线中选择成本值最小

优化PHP代码的40条建议

转: 简介 英文版权归Reinhold Weber所有,中译文作者yangyang(aka davidkoree).双语版可用于非商业传播,但须注明英文版作者.版权信息,以及中译文作者.翻译水平有限,请广大PHPer指正. 1.如果一个方法可静态化,就对它做静态声明.速率可提升至4倍.2.echo 比 print 快.3.使用echo的多重参数(译注:指用逗号而不是句点)代替字符串连接.4.在执行for循环之前确定最大循环数,不要每循环一次都计算最大值.5.注销那些不用的变量尤其是大数组,以便释

优化器--牛顿法总结

---这里记录下一些关于牛顿法来作为优化器的个人笔记 :) 关于牛顿法,先不说其中的概念,来简单看一个例子? 不用计算器,如何手动开一个值的平方根,比如计算{sqrt(a) | a=4 } ? 不用程序和代码如何求? ----比较简单有木有,直接上用公式来套就好了. xt = ( xt-1 + ( a / xt-1 ) ) / 2 我们看 sqrt(4) 这个值的区间在1<=sqrt(4)<=4里,写成这种形式吧[1,4],我们令x0 = 1, x = ( 1 + (4/1))/2 = 5/2

如何优化JAVA代码

通过使用一些辅助性工具来找到程序中的瓶颈,然后就可以对瓶颈部分的代码进行优化.一般有两种方案:即优化代码或更改设计方法.我们一般会选择后者,因为不去调用以下代码要比调用一些优化的代码更能提高程序的性能.而一个设计良好的程序能够精简代码,从而提高性能. 下面将提供一些在JAVA程序的设计和编码中,为了能够提高JAVA程序的性能,而经常采用的一些方法和技巧. 1.对象的生成和大小的调整. JAVA程序设计中一个普遍的问题就是没有好好的利用JAVA语言本身提供的函数,从而常常会生成大量的对象(或实例)

SQL Server 优化器特性导致的内存授予相关BUG

原文:SQL Server 优化器特性导致的内存授予相关BUG 我们有时会遇到一些坑,要不填平,要不绕过.这里为大家介绍一个相关SQL Server优化器方面的特性导致内存授予的相关BUG,及相关解决方式,也顺便回答下邹建同学的相关疑问. 问题描述 一个简单的查询消耗了匪夷所思的内存.(邹建同学发现的) 原文链接 Code create table test_mem ( id int identity(1,1) primary key, itemid int not null, date dat

优化 PHP 代码建议(转)

1.如果能将类的方法定义成static,就尽量定义成static,它的速度会提升将近4倍.2.$row['id'] 的速度是$row[id]的7倍.3.echo 比 print 快,并且使用echo的多重参数(译注:指用逗号而不是句点)代替字符串连接,比如echo $str1,$str2.4.在执行for循环之前确定最大循环数,不要每循环一次都计算最大值,最好运用foreach代替.5.注销那些不用的变量尤其是大数组,以便释放内存.6.尽量避免使用__get,__set,__autoload.7

Catalyst Optimizer优化器

Spark SQL的优化器Catalyst是易于扩展的.它同时支持基于规则(rule-based)和基于代价(cost-based)的优化方法. 在它内部,Catalyst包含了一个表示树和操作树的规则的通用库.在此框架下,目前实现了针对关系查询处理(如,表达式,逻辑查询计划)的库,和在处理查询执行不同阶段(分析,逻辑优化,物理优化,代码生成)的一些规则. Tree 在Catalyst主要的数据类型就是由节点对象组成的树.每个节点都有一个节点类型和0至多个孩子.新节点类型都是Scala里面Tre

优化PHP代码的40条建议(转载)

[size=5][color=Red](译文)优化PHP代码的40条建议[/color][/size] 40 Tips for optimizing your php Code 原文地址:http://reinholdweber.com/?p=3 英文版权归Reinhold Weber所有,中译文作者yangyang(aka davidkoree).双语版可用于非商业传播,但须注明英文版作者.版权信息,以及中译文作者.翻译水平有限,请广大PHPer指正. 1.    If a method ca