PG postgresql原理基础数据结构总结(数据库内核分析)

查询分析是查询编译的第一个模块,包含词法分析、语法分析(生成分析树)和语义分析(得到查询树)三个部分。它将用户输入的SQL命令转换为查询树(Query结构)。词法分析和语法分析分别借助词法分析工具Lex和语法分析工具Yacc(flex)来完成各自的工作。
pg_parse_query
gram.y搜索Stmt/SelectStmt/simple_select
simple_select:
SELECT opt_all_clause opt_target_list
into_clause from_clause where_clause
group_clause having_clause window_clause
{
SelectStmt *n = makeNode(SelectStmt);
n->targetList = $3;
n->intoClause = $4;
n->fromClause = $5;
n->whereClause = $6;
n->groupClause = $7;
n->havingClause = $8;
n->windowClause = $9;
$$ = (Node *)n;
}
上面定义了($3表示第3个部分opt_target_list,查询语句查询的目标列)简单select查询的语法(语法分析输出的语法树中包含SelectStmt结构体)。
qry->targetList = transformTargetList(pstate, stmt->targetList);
把语法格式的ResTarget转换为语义阶段Query结构体需要的TargetEntry(transformTargetEntry -> transformExpr 转换表达式/makeTargetEntry)。

例5.1
class:
classno, classname, gno 年级号,外键,引用grade表gno
student:
sno,sname,sex,age,nation,classno
course:
cno, cname, credit, priorcourse 先导课程
sc 成绩表
sno, cno, score
查询2005级各班高等数学的平均成绩,且只查询平均分在80以上的班级,并将结果按升序排列。
SELECT classno, classname, AVG(score) AS avg_score
FROM sc, (SELECT * FROM class WHERE class.gno=‘2005’) as sub
WHERE sc.sno IN (SELECT sno FROM student WHERE student.classno=sub.classno)
sc.cno IN (SELECT course.cno FROM course WHERE course.cname=‘高等数学’)
GROUP BY classno, classname
HAVING AVG(score) > 80.0
order by avg_score;

pg_analyze_and_rewrite对分析树进行
语义分析(parse_analyze -> transformSelectStmt/transformInsertStmt/transformDeleteStmt/transformUpdateStmt)
和重写(pg_rewrite_query)。

5.2.3 语义分析

该阶段会检查命令中是否有不符合语义规定的成分。例如,所使用的表、属性、过程函数等是否村子,聚集函数(SUM、AVG)是否可以合法使用等。其主要作用在于检查该命令是否可以正确执行。语义分析器会根据分析树中的内容得到更有利于执行的数据,例如,根据表名字得到其OID,根据属性名得到其属性号,根据操作符的名称得到其对应的计算函数等。
parse_analyze函数中,将根据命令类型分七种情况处理,生成查询树Query结构体。其中SELECT/INSERT/DELETE/UPDATE这四种情况所生成的查询树会经由查询重写和查询优化做进一步处理。该过程涉及两个重要的结构体:Query(用于存储查询树)和ParseState(用于记录语义分析的中间信息)。debug_print_parse、debug_print_rewritten、debug_print_plan
5.3节中将要讲到的规则也是以查询树的文本形式存储在系统表中。
FROM子句的语义分析处理
transformFromClause
FROM子句中出现的表、试图、子查询、函数或者连接表达式生成的各类数据结构包装而来。
transformFromClauseItem的主要工作是根据fromClause的Node产生一个或者多个RangeTableEntry结构(RTE)加入到ParseState的p_rtable字段执行的链表中,每一个RangeTableEntry结构就表示一个范围表,最终生成的查询树的rtable字段也将指向该链表。
RangeSubselect RTE_SUBQUERY transformRangeSubselect
RangeFunction RTE_FUNCTION transformRangeFunction
JoinExpr
目标属性的语义分析处理
transformTargetList
WHERE子句的语义分析处理
transformWherClause

5.3 查询重写

规则系统 pg_rewrite
定义重写规则
CREATE RULE DefineRule
删除重写规则
DROP RULE RemoveRewriteRuleById
对查询树进行重写
QueryRewrite
查询优化的核心是生成路径生成计划两个模块。由于在整个查询执行过程中,表连接操作的开销最大,因此,查询优化要处理的问题焦点在于如何计算最后的表连接路径

5.4 查询规划

在数据库查询中,最耗时的是表连接,查询优化的核心思想是“尽量先做选择操作,后做连接操作”,因为先做选择操作可以减少后面进行表连接的数据量。
提升子链接和提升子查询的目的在于将连接操作尽量推迟,并且在此过程中将子查询的WHERE子句与父查询合并。由于WHERE子句中各种行约束条件的执行代价不一样,尽量将各个行约束条件合并到同一个WHERE子句中,再重新确定其执行顺序,就可以降低选择操作的代价,该问题在“扫描计划”这一节中会介绍。PG将需要进行连接操作的表提升到同一个查询层次后,将考虑如何选择一种代价最小的连接方案,该问题在“生成路径”这一节会讲到。
预处理、生成路径、生成计划
预处理实际上是对查询树(Query结构体)的进一步改造。在此过程中,最重要的是提升子链接和提升子查询。
在生成计划阶段,用得到的最优路径,首先生成基本计划树,然后添加GROUP BY、HAVING和ORDER BY等子句所对应的计划节点完成完整计划树
pg_plan_query -> planner -> standard_planner -> subquery_planner
预处理按照“尽量先做选择操作后做连接操作”的思想改造查询树。
grouping_planner在执行过程中不再对查询树做变换。
SELECT查询所包含的信息主要由以下四部分:目标属性、范围表、表连接约束条件、行显示顺序约束条件。grouping_planner将这些信息进行规范化,传递给函数query_planner。
create_plan为路径生成基本计划树。
pull_up_sublinks
pull_up_subqueries
继承关系计划
一般计划
query_planner为一个基本查询创建路径,cheapest_path和sorted_path两条路径。
确定最优路径best_path
7)调用create_plan生成普通计划
8)根据生成的计划,结合用户条件进行包装:
GROUP BY
make_agg
order by
distinct
limit

预处理

在实际进行计划生成执行将对查询树做一些预处理,主要工作是提升子链接子查询、预处理表达式和HAVING子句等。
1.提升子连接/子查询
查询块 嵌套查询 嵌套子查询
SQL语句允许多层嵌套子查询,嵌套查询的一般处理方法是由里向外处理,即每个子查询在其父查询处理之前求解,子查询的结果用于建立父查询的查找条件。
子链接用来表示出现在表达式中的子查询。从直观上来说,子查询是出现在FROM子句中的,而子链接则出现在WHERE子句或HAVING子句中。
相关子查询是指该子查询的执行依赖于外层父查询的某些属性值。它需要接受父查询的参数,因此会设置相关标记,当参数改变时候,需要重置参数后重新执行一遍子查询得到新的结果。非相关子查询中子查询完全可以独立。
嵌套查询分类:
EXISTS
ALL: ALL 或 IN
EXPR 子查询返回一个参数给外层父查询
MULTIEXPR

select d.dname from dept D where d.deptno in (select E.deptno from emp E where e.sal=10000);
提升子链接
select d.dname from dept D, (select E.deptno from emp E where e.sal=10000) as sub
where d.deptno=sub.deptno;
提升子查询
select d.dname from dept D, emp E
WHERE d.deptno = e.deptno and e.sal=10000;
提升子查询之后,子查询被合并到父查询中,可以进行统一优化。
因此,在查询规划的预处理阶段,优化器会查找简单的子查询,把它们放到整个父查询连接树中,这个过程称为提升子查询
提升子查询通过递归调用函数pull_up_subqueries来实现。提升子查询时分以下三种情况处理:
1)范围表中存在子查询。如果是简单子查询,则调用函数pull_up_simple_subquery。
2)FROM表达式中的子查询。
2.预处理表达式
preprocess_expression
1)用基本关系变量取代连接别名变量,此工作由函数flatten_join_alias_vars完成。
2)进行常量表达式的简化,eval_const_expressions。
3)对表达式进行规范化,canonicalize_qual。最佳析取范式合取范式
4)将子链接(SubLinks)转化成为子计划(SubPlans),对每一个子链接都调用函数make_subplan完成转换。
3.预处理HAVING子句

5.4.3 生成路径

对于一个执行计划来说,最重要的部分就是告诉查询执行模块如何取到要操作的元组。执行计划要操作的元组可以来自于一个基本表或者一系列基本表连接而成的“连接表”,当然一个基本表也可以看成是由它自身构成的连接表。由于表之间不同的连接方式和顺序,同一组基本表形成连接表的连接树会有多个,每一棵连接树在PG中都称为一条路径。因此,路径在查询的规划和执行过程中表示了从一组基本表生成最终连接表的方式。而查询规划的工作就是从一系列等效的路径中选取效率最高的路径,并形成执行计划。
生成路径的工作由函数query_planner来完成的。
RelOptInfo是贯穿整个路径生成过程的一个数据结构,生成路径的最终结果始终存放在其中,。
baserel 基本关系
joinrel 连接关系
pathlist记录了生成该RelOptInfo在某方面较优的路径,其中每一个节点都是一个Path结构的指针,Path结构也称为路径。T_SeqScan T_IndexScan
Path结构包含连接的方式、顺序,以及参加连接的baserel的访问方式等。
typedef struct Path {
List *pathkeys;
};
pathkeys 描述一个路径包含的排序信息。顺序扫描的路径的pathkey是NIL,表明没有已知的排序。如果有索引扫描的话,索引扫描路径的pathkeys描述了所选择的索引的排序键。

  1. 路径生成算法
    路径代表了对一个表或者多个表中数据的访问方式。由于单个表的访问方式(顺序、索引、TID)、两个表的连接方式(嵌套循环、归并、Hash)以及多个表间的连接顺序(左连接、右连接、布希连接)都有多种,因此访问一个表或者多个表的路径也会有多种,。
    动态规划算法
    1)初始:为每一个待连接的baserel生成基本关系访问路径,选出最优路径。
    2)归纳:已知第1~n-1层的路径,用下列方法生成第n层的关系(n>1):
    左连接 left-handed 外关系(左子树)是一个joinrel,而内关系(右子树)总是一个baserel。
    右连接right_handed 外关系总是一个baserel,而内关系则是一个joinrel。
    布希连接(bushy)指内外关系都能自行连接,也就是说内外关系都可以是joinrel。
    嵌套循环连接
    对左边关系的每个元组都要扫描右边关系的所有元组进行连接操作。如果右边的关系可以用索引扫描,那么这个策略可能就是个好策略。
    归并连接
    执行前会将没给个关系都按链家解决属性进行排序。然后对两个关系左并行扫描,匹配的元组就组合起来形成连接元组。
    每个关系只扫描一次。
    Hash连接
    首先扫描右边的关系,并把扫描到的元组用连接属性值作为Hash键装载进入一个Hash表,然后扫描左边的关系,并将找到的每个元组的连接属性值作为Hash键字来定位Hash表里匹配的元组。
    连接的顺序不同会导致中间关系的大小不同,间接导致执行时的磁盘I/O时间和CPU时间不同;连接的实现方法不同,直接导致磁盘I/O时间和CPU时间不同。选择路径时,我们需要同时考虑这两个因素,为不同的顺序和不同的连接方式形成的组合分别生成路径。而计算某一路径的代价时,将根据底层关系的大小和连接的实现方法,计算出总的代价,从而来评判路径的优劣。
    在路径生成过程中,每生成一个中间关系,我们要估算出它的大小、路径及其代价,供上层路径计算代价。
    路径的启动代价、路径总的执行代价 和路径的输出排序键。
    遗传算法
    动态规范算法是生成路径的默认算法。enable_geqo
    geqo_threshold 默认值12,即参加连接的基本关系数大于或等于12时就会采用遗传算法来生成路径。

  2. 路径生成总体流程
    make_one_rel
    1)调用set_base_rel_pathlists生成基本关系的访问路径, 对应动态规划算法中的第1层。
    2)调用make_rel_from_joinlist生成最终路径。
    3. 生成基本关系访问路径
    只在其RelOptInfo中保留“值得保留”的路径。所谓“值得保留”的路径应满足以下条件之一:
    总代价最优。
    启动代价最优。
    有最好的排序结果。
    set_subquery_pathlist
    set_function_pathlist
    set_values_pathlist T_ValuesScan
    set_workable_pathlist set_cte_pathlist
    set_plain_rel_pathlist:
    用于生成普通关系(非子查询,且无继承关系)的路径。
    create_seqscan_path 调用add_path加入到pathlist中。
    create_index_paths create_tidscan_paths
    set_cheapest设置最小启动代价和最小代价。

  3. 生成索引扫描路径
    当关系上涉及索引扫描时,要生成相应的索引路径。create_index_paths find_usable_index。
    find_usable_indexes
    生成TID扫描路径
    create_tidscan_paths

  4. 生成最终路径
    所有基本关系的路径都创建好之后,就可以把它们连接起来形成最终的连接关系,最终路径的生成由函数make_rel_from_joinlist实现。
    该函数会将joinlist中的基本关系连接起来生成最终的连接关系,并且为最终的连接关系建立RelOptInfo结构,。该函数的实质就是选择不同的连接方式作为中间节点,。
    使用的是PostgreSQL中标准的递归处理二叉树的方法。
    standard_join_search
    make_join_rel主要功能是生成连接路径,。
    match_unsorted_outer
    hash_inner_and_outer 生成hash连接路径。

5.4.5 生成普通计划

optimize_minmax_aggregates
create_plan,该函数为最优路径创建计划,依据其路径节点类型的不同,分别调用不同的函数生成相应的计划。
扫描计划:
连接计划:
其它计划:Append Result 物化计划
create_seqscan_plan
create_nestloop_plan

5.4.5 生成完整计划

生成路径仅仅考虑基本查询语句信息,并没有保留诸如GROUP BY、ORDER BY等信息。
grouping_planner函数调用create_plan生成基本计划树后,则会依据查询树相关约束信息在前面生成的普通计划之上添加相应的计划节点完成完整计划。
创建聚集计划节点 make_agg
创建排序计划节点 make_sort

5.4.7 整理计划树

生成的完整计划经过计划树整理之后就可以交给查询执行器去执行了。set_plan_references。
为了方便执行器的执行,对计划树一些表达上的细节做最后的调整。

5.4.8 实例分析

5.2节的例5.1中介绍了如何对SQL语句进行查询分析最终生成查询树,现在在其基础上进一步介绍如何对查询树进行规划处理。

  1. 查询分析之后的查询树
  2. 提升子连接
    WHERE子句存在两个子链接,可以将其提升为范围表。
    SELECT classno, classname, AVG(score) AS avg_score
    FROM sc, (SELECT * FROM class WHERE class.gno=‘2005’) as sub,
    (SELECT course.cno FROM course WHERE course.cname=‘高等数学’) AS ANY_subquery
    WHERE sc.sno IN (SELECT sno FROM student WHERE student.classno=sub.classno)
    AND sc.cno=ANY_subquery.cno
    GROUP BY classno, classname
    HAVING AVG(score) > 80.0
    order by avg_score;
  3. 提升子查询 FROM子句的3个范围表有2个是子查询(非相关)的形式,将其提升到父查询。
    SELECT classno, classname, AVG(sc.score) AS avg_score
    FROM sc, class, course
    WHERE sc.sno IN (SELECT student.sno FROM student WHERE student.classno=class.classno)
    AND sc.cno=course.cno
    AND course.cname=‘高等数学’
    AND class.gno=‘2005’
    GROUP BY classno, classname
    HAVING AVG(score) > 80.0
    ORDER BY avg_score;
    4.生成路径
    查询树改造完毕之后将先生成路径,然后再进行计划的生成。路径生成按照先基本关系连接关系的顺序进行。
    (1)基本关系的路径
    首先处理的是相关子查询,它只针对基本关系student,不存在关系的连接。
    student的路径只有一种情况:顺序扫描。记student的顺序扫描路径为R1。

相关子查询路径生成之后将为父查询生成基本关系的路径,父查询包含三个基本关系,分别为sc、class和course,在范围表中对应的索引号为1、4、5。
(2)连接关系的路径
基本关系的路径生成之后,就需要利用动态规划算法来生成由sc、class、course连接而成的连接关系路径。
5.生成计划
按照后序遍历路径树,提取相应的约束信息(并按最佳执行顺序排序)、基本关系或连接关系的ID、目标属性等信息,创建相应的计划节点,生成计划树。

5.5 代价估计

评价路径优劣的依据是用系统表pg_statistic中的系统统计信息估计出的不同路径的代价(Cost)。
磁盘代价以从磁盘顺序存取一个页面的代价为单位,其它形式的代价都是相对磁盘存取代价来计算的。
seq_page_cost: 顺序存取页的代价,值为1.0。
random_page_cost: 4.0

你可能感兴趣的:(数据库,postgresql,数据库)