pg查询处理流程
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了pg查询处理流程相关的知识,希望对你有一定的参考价值。
参考技术A并行查询使用多个后台进程,但后端进程基本上处理连接的客户端发出的所有查询。改后端有五个子系统组成。
解析器生成一个解析树,后续子系统可以从纯文本的 SQL 语句中读取该解析树。
如下面的查询:
解析树是其根节点是定义在 parsenodes.h中的 [SelectStmt](javascript:void(0))结构的树。
SELECT 查询的元素和解析树的相应元素编号相同。例如,(1) 是第一个目标列表的一个项目,它是表的“id”列,(4) 是 WHERE 子句,依此类推。
由于解析器在生成解析树时只检查输入的语法,因此只有在查询中出现语法错误时才会返回错误。
解析器不检查输入查询的语义。例如,即使查询包含不存在的表名,解析器也不会返回错误。语义检查由分析器/分析器完成。
分析器运行由解析器生成的解析树的语义分析并生成查询树。
查询树的根是定义在 parsenodes.h中的 [查询](javascript:void(0))结构;此结构包含其相应查询的元数据,例如此命令的类型(SELECT、INSERT 或其他)和几个叶子;每个叶子形成一个列表或树,并保存各个特定子句的数据。
述查询树简述如下。
重写器是实现 规则系统 的系统,必要时根据存储在 pg_rules系统目录中的规则变换查询树。
PostgreSQL 中的视图 是使用规则系统实现的。当视图由 CREATE VIEW 命令定义时,相应的规则会自动生成并存储在目录中。
假设已经定义了以下视图,并且对应的规则存储在 pg_rules 系统目录中。
当发出包含如下所示视图的查询时,解析器将创建解析树,如图所示。
在这个阶段,重写器将范围表节点处理为子查询的解析树,即对应的视图,存储在 pg_rules 中。
计划器从重写器接收查询树并生成可以由执行器最有效地处理的(查询)计划树。
PostgreSQL 中的计划器是基于纯成本优化的;它不支持基于规则的优化和提示。这个规划器是 RDBMS 中最复杂的子系统
与其他 RDBMS 一样,PostgreSQL 中的 EXPLAIN命令显示计划树本身。 如下所示。
他对应的计划树:
每个计划节点都有执行器需要处理的信息,单表查询的情况下,执行器从计划树的末端到根进行处理。
PostgreSQL 的查询优化是基于成本的。成本是无量纲值,它们不是绝对的绩效指标,而是比较运营相对绩效的指标。成本由 costsize.c 中定义的函数估算。执行器执行的所有操作都有相应的成本函数。例如,顺序扫描和索引扫描的成本分别由 cost_seqscan() 和 cost_index() 估算。
有三种成本,启动成本,执行成本以及总成本。其中总成本 = 启动成本 + 执行成本。
顺序扫描的成本由 cost_seqscan() 函数估算。
其中 seq_page_cost 、 cpu_tuple_cost 和 cpu_operator_cost 在 postgresql.conf 文件中设置,默认值分别为 1.0 、 0.01 和 0.0025 ,Ntuple和Npage分别是该表的所有元组和所有页的编号。
从运行成本估算可以看出,PostgreSQL 假设所有页面都将从存储中读取;也就是说,PostgreSQL 不考虑扫描的页面是否在共享缓冲区中。
虽然 PostgreSQL 支持 一些索引方法 ,例如 BTree、 GiST 、 GIN 和 BRIN ,但索引扫描的成本是使用常见的成本函数估算的:cost_index()。
索引扫描的启动成本是读取索引页以访问目标表中第一个元组的成本,它由以下等式定义:
Hindex是索引树的高度。
索引扫描的运行成本是表和索引的 cpu 成本和 IO(输入/输出)成本之和:
前三个成本定义如下:
其中 cpu_index_tuple_cost 和 random_page_cost 在 postgresql.conf 文件中设置(默认分别为 0.005 和 4.0); qual_op_cost粗略来说就是指数的评估成本,值为0.0025。选择性选择性是指定WHERE子句对索引的搜索范围的比例;它是一个从 0 到 1 的浮点数
查询谓词的选择率是通过直方图界值与高频值估计的,这些信息都储存在系统目录pg_staticstics中,并可通过pg_stats视图查询。
表中的每一列的高频值都在pg_stats视图的most_common_vals和most_common_freqs中成对存储。
排序路径会在排序操作中被使用。排序操作包括order by、归并连接的预处理操作,以及其他函数。函数cost_sort()用于估计排序操作的代价。如果能在工作内存中放下所有元组,那么排序操作会选用快速排序算法。否则就会创建临时文件,使用文件归并排序算法。
排序路径的启动代价就是对目标表的排序代价,因此代价就是O(Nsort) * Log 2 (Nsort),这里Nsort就是带排序的元组数。排序路径的运行代价就是读取已经排序好的元组的代价,因此代价就是O(Nsort)。
PostgreSQL中的计划器会执行三个步骤:
访问路径是估算代价时的处理单元。比如顺序扫描、索引扫描、排序,以及各种连接操作都有其对应的路径。访问路径只在计划器创建查询计划树的时候使用。最忌本的访问路径数据结构就是relation.h中定义的path结构体,相当于顺序扫描。所有其他的路径访问都基于该结构。
在创建计划树之前,计划器将线对PlannerInfo中的查询书进行一些预处理。预处理有很多步骤,本节值讨论和单表查询处理相关的主要步骤。
计划器对所有可能的访问路径进行代价估计,然后选择代价最小的那个。
在最后一步中,计划器按照代价最小的路径生成一颗计划树。
计划树的根节点是定义在plannodes.h中的Plannedstmt结构,包含19个字段,其中有4个代表性字段:
计划树包含各式各样的计划节点。PlanNode是所有计划节点的基类,其他计划节点都会包含PlanNode结构。比如顺序扫描节点SeqScanNode包含一个PlanNode和一个整型变量scanrelid。PlanNode包含14个字段,下面是7个代表性字段:
在单表查询的例子中,执行器从计划树中取出计划节点,按照自底向上的顺序进行处理,并调用节点相应的处理函数。
每个计划节点都有相应的函数,用于执行节点对应的操作。这些函数在src/backend/executor目录中。
理解执行器如何工作的最好方式,就是阅读explain命令的输出。
我们可以自底向上阅读explain的结果,来看一看执行器是如何工作的。
第六行:首先,执行器通过nodeSeqscan.c中定义的函数执行顺序扫描操作。
第四行:然后,执行器通过nodeSort.c中定义的函数,对顺序扫描的结果进行排序。
执行器在处理查询时会使用工作内存和临时缓冲区,两者都在内存中分配。如果查询无法在内存中完成,就会用到临时文件。
使用带有Analyze选项的explain,待解释的命令会真正执行,并显示实际结果行数、实际执行时间和实际内存使用量。
在第6行,explain命令显示执行器使用了10000KB的临时文件。临时文件会被临时创建在base/pg_tmp子目录中,并遵循如下命令规则:{“pgsql_tmp”}+ {创建本文件的postgres进程pid}.从0开始的序列号
比如,临时文件pgsql_tmp8903.5是pid为8903的postgres进程创建的第6个临时文件。
PostgreSQL中支持三种连接操作,分别是嵌套循环连接,归并连接和散列连接。在pg中,嵌套循环连接和归并连接有几种变体。
这三种连接方式都支持pg中所有的连接操作,注入inner join、 left/right outer join、 full outer join等。
循环嵌套连接不需要任何启动代价,因此:start-up cost = 0
运行代价和内外表尺寸的乘积成比例,即run cost是O(Nouter * Ninner), Nouter和Ninner分别是外表和内表的元组条数。run cost的定义如下:
Couter和Cinner分别是内表和外表顺序扫描的代价。
循环嵌套连接的代价总会被估计,但实际中很少会使用这种连接操作,因为它有几种更高效的变体。
在上面描述的循环嵌套连接中,每当读取一条外表中的元组时,都需要扫描内标中的所有元组。位每条外表元组对内标做全表扫描,这一过程代价高昂,pg支持一种物化嵌套循环连接,可以减少内标全表扫描的代价。
在运行嵌套循环连接之前,执行器会使用临时元组存储模块对内表进行一次扫描,将内表元组加载到工作或临时文件中。在处理内表元组时,临时元组存储比缓冲区管理器更为高效,特别是当所有的元组都能放入工作内存中。
qg内部提供了临时元组存储的模块,可用于各种操作,如五花膘、创建混合散列连接的批次等。该模块包含一系列函数,都在tuplestore.c中。这些函数用于从工作内存或临时文件读写元组。该工作内存还是临时文件取决于待存储元组的总数。
上面显示了执行器要进行的操作,执行器对这些计划节点的处理过程如下:
第7行:执行器使用顺序扫描,物化内部表tbl_b。
第4行:执行器执行嵌套循环连接操作,外表是tbl_a,内表是物化的tbl_b。
如果内表上有索引,且该索引能用于搜索满足连接条件的元组,那么计划器在外外表的每条元组搜索内标中的匹配元组时,会考虑使用索引进行直接搜索,以替代顺序扫描。这种变体叫做索引嵌套循环连接,如下图所示。虽然这种变体叫做“索引嵌套循环连接”,但是谁该算法基本上只需要在外表上循环一次,因此连接操作的执行非常高效。
与嵌套循环连接不同的是,归并连接只能用于自然连接与等值连接。
函数initial_cost_merge_join()和final_cost_merge_join()用于估计归并连接的代价。
归并连接的启动成本是内表与外表排序成本之和,因此其启动成本为:
这里Nouter和Ninner分别是外表和内标的元素条数,而运行代价是O(Nouter + Ninner)。
下图是归并连接的示意图。
如果所有元组都可以存储在内存中,那么排序操作就能在内存中进行,否则就是用临时文件。
第9行:执行器对内表tbl_b进行排序,使用顺序扫描(第11行)。
第6行:执行器对外表tbl_a进行排序,使用顺序扫描(第8行)。
第4行:执行器执行归并连接操作,外表是排序好的tbl_a,内表是排好序的tbl_b。
与嵌套循环连接类似,归并连接还支持物化归并连接,物化内表,使内表扫描更为高效。
下面是物化归并连接的explain结果,很容易发现,与普通归并连接的差异是第9行:Materialize。
与归并连接类似,hash连接只能用于自然连接与等值连接。
PostgreSQL中的散列连接的行为因表的大小而异。如果布标足够小(确切的说,内表大小不超过工作内存的25%),那么hash连接就是简单的两阶段内存hash连接,否则将会使用带倾斜批次的混合hash连接。
内存中的hash连接是在work_mem中处理的,在pg中,散列表区域被称作处理批次。一个批处理批次会有多个散列槽,内部称其为桶,桶的数量由nodeHash.c中定义的ExecChooseHashTableSize()函数所确定。桶的数量是2的整数次幂。
内存散列连接有两个阶段,分别是构建阶段和探测阶段。在构建阶段,内存表中的所有元组都会被插入到处理批次中;在探测阶段每条腕表元组都会与处理批次中的内表元组比较,如果满足连接条件,则将两条元组连接起来。
当内表的元组无法全部存储在工作内表中的单个处理批次时,pg使用带倾斜批次的混合散列连接算法,该算法时混合散列连接诶的一种变体。
在第一个构建和探测阶段postgresql准备多个批次,宇通的数目类似,处理批次的数据由函数ExecChooseHashTableSize()决定,也就是2的整数次幂。工作内存中智慧分配一个处理批次,而其他批次都以临时文件的形式创建。属于这些批次的元组将通过临时元组存储功能被写入到相应的文件中。
为了获取最佳计划树,计划器必须考虑各个索引与各种连接方法之间的所有可能组合。如果表的数量超过某个水平,该过程的代价就会因为组合爆炸而变得非常昂贵,以至于根本不可行。
如果表的数量小于12张,计划器可以使用动态规划来获取最佳计划。
PostgreSQL——查询优化——预处理
2021SC@SDUSC
目录
概述
我负责的PostgreSQL代码部分:查询的编译与执行
此篇博客的分析内容:查询优化——预处理
在上两篇博客中,我介绍了查询重写的过程,分析了查询重写过程的主要函数。在查询重写之后,就要进行查询优化了。在DBMS中,用户的查询请求可以采用不同的方案来执行。但是不同的方案之间的查询效率确是不一样的。查询效率这一点大型数据库中即存放了大数据集的数据库中十分关键,并且在分布式数据库中也很关键,会直接关联到用户体验感和功能效率。而本篇博客要介绍的查询优化——负责选择一种代价最小的执行方案。
查询优化
查询优化的最终目的是得到可被执行器执行的最优计划。查询优化的核心思想是:尽量先做选择操作,后做连接操作。因为在数据库的查询中,最耗时的是表的join操作,所以先做选择的操作可以尽量减少表连接的数据量。查询优化的整个过程可以分为预处理,生成路径和生成计划三个阶段。预处理实际上是对查询树(Query结构体)的进一步改造。在预处理的过程中,最重要的是提升子链接和提升子查询,推后表连接。在生成路径阶段,接收到改造后的查询树后,采用相应的动态规划算法,生成最优的连接路径和候选的路径连接表。在生成计划阶段用得到的最优路径,先生成基本计划树然后根据查询语句所对应的计划节点形成完整计划树。本篇博客先分析第一部分——预处理阶段
pg_plan_queries函数
查询分析完成后,它的最终产物——查询树链表将被移交给查询优化模块,。查询优化模块的入口函数是pg_plan_queries函数,它负责将查询树链表变成执行计划链表。pg_plan_queries函数通过调用下面介绍的pg_plan_query函数对每一个查询树进行处理,并将生成的返回。
pg_plan_queries(List *querytrees, int cursorOptions, ParamListInfo boundParams)
List *stmt_list = NIL;//初始化PlannedStmt结构体链表
ListCell *query_list;//查询树链表节点的临时变量
foreach(query_list, querytrees)//遍历查询树
Query *query = lfirst_node(Query, query_list);//取得第一个节点
PlannedStmt *stmt;//PlannedStmt结构体
//查询优化模块只会对非UTILITY命令进行处理。
if (query->commandType == CMD_UTILITY)//如果是CMD_UTILITY类型则不进行优化,如果不是才进行优化
//如果是CMD_UTILITY类型则不进行优化
stmt = makeNode(PlannedStmt);
stmt->commandType = CMD_UTILITY;
stmt->canSetTag = query->canSetTag;
stmt->utilityStmt = query->utilityStmt;
stmt->stmt_location = query->stmt_location;
stmt->stmt_len = query->stmt_len;
else//不是CMD_UTILITY类型则调用pg_plan_query函数进行优化
stmt = pg_plan_query(query, cursorOptions, boundParams);
//把处理后的PlannedStmt结构体stmt添加到结构体链表中
stmt_list = lappend(stmt_list, stmt);
//返回PlannedStmt结构体链表
return stmt_list;
pg_plan_queries函数调用的—— pg_plan_query函数
pg_plan_queries函数通过调用pg_plan_query函数对每一个查询树进行处理。
pg_plan_query(Query *querytree, int cursorOptions, ParamListInfo boundParams)
PlannedStmt *plan;//PlannedStmt结构体,最后pg_plan_query函数返回plan
//查询优化模块只会对非UTILITY命令进行处理。
//如果是CMD_UTILITY类型则不进行优化,如果不是才进行优化
if (querytree->commandType == CMD_UTILITY)
return NULL;
//Planner必须有一个快照,以防它调用用户定义的函数。
Assert(ActiveSnapshotSet());
TRACE_POSTGRESQL_QUERY_PLAN_START();
if (log_planner_stats)
ResetUsage();//记录统计数据
//调用优化器 planner函数
plan = planner(querytree, cursorOptions, boundParams);
if (log_planner_stats)
ShowUsage("PLANNER STATISTICS");
//返回PlannedStmt结构体plan
return plan;
pg_plan_queries函数返回的—— PlannedStmt结构体
typedef struct PlannedStmt
NodeTag type; //节点的标识符Tag
CmdType commandType; //计划所对应的命令类型,分别有select|insert|update|delete|utility
uint64 queryId; //查询树id
bool hasModifyingCTE; //WITH语句中是否存在insert|update|delete关键字
bool canSetTag; //是否需要设置命令结果标志
bool transientPlan; //当TransactionXmin改变时是否要重做计划
bool parallelModeNeeded; //是否需要并行
int jitFlags; //使用哪种形式的JIT
struct Plan *planTree; //计划树
List *rtable; //范围表
List *resultRelations; //计划中的结果关系,由结果关系的RTE索引构成
List *rootResultRelations;//INSERT/UPDATE/DELETE命令所影响的关系在rtable中的位置(index)
List *subplans; //子计划,可为空
Bitmapset *rewindPlanIDs; //需要回卷的子计划的索引信息
List *rowMarks; //用于select for update等
List *relationOids; //该计划需要的表的oid
List *invalItems; //计划所依赖的其他对象,用PlanInvalitem结构表示,其中记录了被依赖的对象所属的SysCache的ID以及其对应的系统表元组的TID
List *paramExecTypes; //执行的计划所需的参数类型
Node *utilityStmt; //定义游标时用来记录游标定义语句的分析树
int stmt_location; //SQL语句的起始位置
int stmt_len; //SQL语句的长度
PlannedStmt;
pg_plan_query函数调用的——planner函数
pg_plan_query函数中负责实际生成的是planner函数。
planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
PlannedStmt *result;//PlannedStmt结构体,最后以PlannedStmt结构体形式返回
if (planner_hook)
result = (*planner_hook) (parse, cursorOptions, boundParams);
else
result = standard_planner(parse, cursorOptions, boundParams);//调用standard_planner函数进入标准的查询规划处理,standard_planner函数会返回PlannedStmt 结构体供planner返回结果result
return result;
planner函数通过调用standard_planner函数进入标准的查询优化处理。函数standard_planner负责接受Query查询树及外部传递的参数信息,返回PlannedStmt结构体。(PlannedStmt结构体分析见上)但是由于planner函数又是通过调用不同的函数分别完成计划树的处理,优化,清理。所以,在这里我将主要分析planner函数调用的不同功能的函数。因为我们在这篇分析的是查询优化的预处理阶段,所以先分析subquery_planner函数,因为查询优化的预处理过程是由subquery_planner函数中的部分函数完成的。
standard_planner函数调用subquery_planner函数的代码:
//standard_planner函数调用subquery_planner函数进行查询优化的预处理阶段
root = subquery_planner(glob, parse, NULL,
false, tuple_fraction);
standard_planner函数调用的——subquery_planner函数
subquery_planner函数接受Query查询树,最后通过PlannedStmt结构体的形式返回Plan计划树。(Plan计划树被封装在PlannedStmt结构体中)subquery_planner函数负责创建计划,可以递归处理子查询。subquery_planner函数的工作分为两个部分:
1:依据消除冗余条件,减少查询层次,简化路径生成的基本思想,调用预处理函数对查询树进行处理
2:调用inheritance_planner 或者grouping_planner进入生成计划流程,此过程不对查询树做出实质性改变
subquery_planner函数执行流程
subquery_planner(PlannerGlobal *glob, Query *parse,
PlannerInfo *parent_root,
bool hasRecursion, double tuple_fraction)
//传入参数分析
//PlannerGlobal *glob:PlannerGlobal 类型的指针,记录做计划期间的全局信息,例如子计划,子计划的范围表等,每条查询语句有且仅有一个该变量。
//Query *parse:Query类型的指针,指向要生成计划的查询树
//PlannerInfo *parent_root:PlannerInfo类型的指针,指向父查询的规划器相关信息(首次调用为空)
// bool hasRecursion:bool类型,如果正在处理的是一个递归的with查询,则hasRecursion为真
//double tuple_fraction:double类型,表示计划扫描元组的比例。
PlannerInfo *root;//返回值
List *newWithCheckOptions;
List *newHaving;//Having子句
bool hasOuterJoins;//是否存在Outer Join
bool hasResultRTEs;
RelOptInfo *final_rel;
ListCell *l;//临时变量
//初始化信息,为该子查询创建一个PlannerInfo结构体,并为结构体赋初始值,最后subquery_planner函数返回root(PlannerInfo类型)
root = makeNode(PlannerInfo);
root->parse = parse;
root->glob = glob;
root->query_level = parent_root ? parent_root->query_level + 1 : 1;
root->parent_root = parent_root;
root->plan_params = NIL;
root->outer_params = NULL;
root->planner_cxt = CurrentMemoryContext;
root->init_plans = NIL;
root->cte_plan_ids = NIL;
root->multiexpr_params = NIL;
root->eq_classes = NIL;
root->append_rel_list = NIL;
root->rowMarks = NIL;
memset(root->upper_rels, 0, sizeof(root->upper_rels));
memset(root->upper_targets, 0, sizeof(root->upper_targets));
root->processed_tlist = NIL;
root->grouping_map = NULL;
root->minmax_aggs = NIL;
root->qual_security_level = 0;
root->inhTargetKind = INHKIND_NONE;
root->hasRecursion = hasRecursion;
//如果hasRecursion为真,则需要为该子查询新建一个worktable变量,并把它的标识符放在PlannerInfo结构体的wt_param_id字段中
if (hasRecursion)
//把worktable变量的标识符放在PlannerInfo结构体的wt_param_id字段中
root->wt_param_id = assign_special_exec_param(root);
else
root->wt_param_id = -1;
root->non_recursive_path = NULL;//不需要递归
if (parse->cteList)//判断查询树是否有with子句
//如果有一个WITH链表,使用查询处理每个链表,并为其构建一个initplan子计划结构。
SS_process_ctes(root);//处理With 语句
replace_empty_jointree(parse);
子链接和子查询的区别:子查询是一条完整的查询语句,而子链接是一条表达式,但是表达式内部也可以包含查询语句。直白点说呢就是:子查询是放在FROM子句里的而子链接则出现在WHERE子句或者HAVING子句中。
在subquery_planner函数里,调用pull_up_sublinks函数处理WHERE子句和JOIN/ON子句中的ANY和EXISTS类型的子链接。
subquery_planner函数调用pull_up_subqueries函数来提升子查询。当子查询仅仅是一个简单的扫描或者连接时,就会把子查询或者子查询的一部分合并到父查询中以进行优化。
1.在范围表中存在子查询。对于简单的子查询,直接调用pull_up_simple_subquery函数进行提升;而对于简单的UNION ALL子查询,调用pull_up_simple_union_all函数进行提升,其他的情况则不处理;
2.在FROM表达式中存在子查询。对于FROM列表中的每个节点都调用pull_up_subqueries递归处理;
3.连接表达式中的子查询。调用pull_up_subqueries函数递归地处理.
//如果原始的查询树有子链接,则调用pull_up_sublinks函数提升子链接
if (parse->hasSubLinks)
pull_up_sublinks(root);
//扫描RTE中的set-returning函数,如果可能,内联它们(生成下一个可能被上拉的子查询)。这里递归问题的处理方式与SubLinks相同。
inline_set_returning_functions(root);
//调用pull_up_subqueries函数提升子查询
pull_up_subqueries(root);
if (parse->setOperations)//判断查询树中有无集合操作
flatten_simple_union_all(root);//扁平化处理UNION ALL
//根据查询树中的相关信息,确定PlannerInfo中的相关信息如:hasJoinRTEs,hasOuterJoins,hasHavingQual
foreach(l, parse->rtable)//对查询树中每一个范围表单独处理
RangeTblEntry *rte = lfirst_node(RangeTblEntry, l);
switch (rte->rtekind)//根据范围表的类型,做不同的处理
case RTE_RELATION://表示RTE类型为普通表
if (rte->inh)//判断是否需要继承
rte->inh = has_subclass(rte->relid);
break;
case RTE_JOIN://表示RTE为链接类型
root->hasJoinRTEs = true;
if (IS_OUTER_JOIN(rte->jointype))//判断是否有外链接
hasOuterJoins = true;
break;
case RTE_RESULT://表示空的from子句
hasResultRTEs = true;//记录from子句为空
break;
default:
break;
if (rte->lateral)//判断是否有级联
root->hasLateralRTEs = true;
//预处理RowMark信息。 需要在子查询上拉(以便所有非继承的RTEs都存在)和继承展开之后完成(以便expand_inherited_tables可以使用这个信息来检查和修改)
preprocess_rowmarks(root);//预处理RowMark信息
//判断是否存在Having表达式
root->hasHavingQual = (parse->havingQual != NULL);
清除hasPseudoConstantQuals标记,该标记可能在distribute_qual_to_rels函数中设置
root->hasPseudoConstantQuals = false;
表达式的预处理工作主要由函数preprocess_expression完成。该函数采用递归扫描的方式处理PlannerInfo结构体里面保存的目标属性、HAVING子句、OFFSET子句、LIMIT子句和连接树jointree。总体来说做了以下这些事
1.调用flatten_join_alias_vars函数,用基本关系变量取代连接别名变量;
2.调用函数eval_const_expression进行常量表达式的简化,也就是直接计算出常量表达式的值。例如:“3+1 <> 4” 这种会直接被替换成“FALSE”;
3.调用canonicalize_qual函数对表达式进行规范化,主要是将表达式转换为最佳析取范式或者合取范式
4.调用函数make_subplan将子链接转换为子计划.
//调用preprocess_expres以上是关于pg查询处理流程的主要内容,如果未能解决你的问题,请参考以下文章