背景
He3DB for PostgreSQL是受Aurora论文启发,基于开源数据库PostgreSQL 改造的数据库产品。架构上实现计算存储分离,并进一步支持数据的冷热分层,大幅提升产品的性价比。
He3DB for PostgreSQL中查询规划用于选择SQL语句执行代价最小的方案。它在整个查询处理模块应该是在一个非常重要的地位上,这一步直接决定了查询的方式与路径,很大程度上影响了数据库查询的查询性能。
1、概述
下图大概的刻画了查询规划模块里主要的函数调用关系
查询规划的最终目的是得到可被执行器执行的最优计划,整个过程可分为预处理
、生成路径
和生成计划
三个阶段。
预处理
阶段是对查询树(Query结构体)的进一步改造,这种改造可通过SQL语句体现。在此过程中,最重要的是提升子链接和提升子查询。生成路径
阶段,接收到改造后的查询树后,采用动态规划算法或遗传算法,生成最优连接路径和候选的路径链表。生成计划
阶段,用得到的最优路径,首先生成基本计划树(查询语的SELECT…FROM…WHERE部分,),然后添加GROUPBY、HAVING和ORDER BY等子句所对应的计划节点形成完整计划树。
本文着重介绍预处理
阶段。预处理部分主要是对查询树Query中的范围表rtable 和连接树 jointree 等进行处理。主要分为三个阶段:提升子链接和子查询
、预处理表达式
、处理HAVING子句
。
2、提升子链接/子查询
SQL语句中一个“SELECT…FROM…WHERE”语句称为一个查询块,将一个查询块嵌套到另一个查询块的FROM子句、WHERE子句或HAVING子句中的查询称为嵌套查询
,其中被嵌入其他查询块中的查询块称为嵌套子查询。在PostgreSQL中子链接用来表示出现在表达式中的子查询与普通子查询的联系和区别。
子查询
:一条完整的查询语句。
子链接
:子链接是一条表达式,但是表达式内部也可以包含查询语句。
直观上来说就是:子查询是放在FROM子句里的而子链接则出现在WHERE子句或者HAVING子句中。
按关键字,嵌套查询可以分为以下几类:
EXISTS
:声明了EXISTS 的子查询。ALL
:声明了 ALL或 NOT IN的子查询。ANY
:声明ANY或IN的子查询。EXPR
:子查询返回一个参数给外层父查询。MULTIEXPR
:子查询返回多个参数给外层父查询,例如语句“SELECTFROMBWHERE(b1,3,'aa’)>SELECTfrom A;"中的子査询将向父查询返回多个属性值;ARRAY
:子查询是将某些值构成数组的表达式,例如:“SELECT ARRAY[1,2,3+4];”
举一个例子展示对子链接/子查询和子查询的处理,假设我们有这样一个SQL语句:
· SELECT D.dname
· FROM dept D
· WHERE D.deptno IN
· (SELECT E.deptno FROM emp E WHERE E.sal = 100);
从字面上看,如果该语句中的子查询被独立地规划,也就是说对于表dept中的每一个元组deptno值,都要搜索一遍emp表。显然这样的做法代价也非常大。但是如果我们把子查询提升并合并到父查询中,那么我们看看效果。
先做提升子链接
:
· SELECT D.dname
· FROM dept D , (SELECT E.deptno FROM emp E WHERE E.sal = 100) AS Sub
· WHERE D.deptno = Sub.deptno;
然后再做提升子查询
:
· SELECT D.dname
· FROM dept D ,emp E
· WHERE D.deptno = E.deptno and E.sal = 100;
可以看到,这样操作以后的SQL语句只要先做一下过滤(E.sal = 100),然后再把结果和dept表做一下连接即可,大大提高了查询效率。
2.1 子链接提升流程
void pull_up_sublinks(PlannerInfo *root)
{
Node *jtnode; /* 用于存储处理后的联合树(jointree)节点 */
Relids relids; /* 用于存储相关关系标识符的集合 */
/* 如果查询条件中包含行号(rownum),则禁止提升子链接 */
if (find_rownum_in_quals(root))
{
return; /* 如果找到行号,直接返回,不进行后续处理 */
}
/* 开始递归遍历联合树(jointree) */
jtnode = pull_up_sublinks_jointree_recurse(root,
(Node *) root->parse->jointree,
&relids);
/*
* root->parse->jointree 必须始终是一个 FromExpr,因此如果递归返回了一个裸的 RangeTblRef
* 或 JoinExpr,我们需要插入一个虚拟的 FromExpr。
*/
if (IsA(jtnode, FromExpr))
root->parse->jointree = (FromExpr *) jtnode; /* 如果 jtnode 是 FromExpr,直接赋值 */
else
root->parse->jointree = makeFromExpr(list_make1(jtnode),
在subquery_planner
函数里,调用pull_up_sublinks
函数处理WHERE子句和JOIN/ON子句中的ANY
和EXISTS
类型的子链接。
在用pull_up_sublinks
函数内部,调用pull_up_sublinks_jointree_recurse
函数递归地处理连接树jointree
:
- 对于RangeTblRef类型,直接返回;
- 对于FromExpr类型,递归调用pull_up_sublinks_jointree_recurse函数处理每个节点并调用pull_up_sublinks_qual_recurse函数处理约束条件;
- 对于JoinExpr类型,递归调用pull_up_sublinks_jointree_recurse函数处理左右子树并调用pull_up_sublinks_qual_recurse函数处理约束条件.
2.2 子查询提升
void
pull_up_subqueries(PlannerInfo *root)
{
/* 断言:确保root->parse->jointree是一个FromExpr类型,这是联合树(join tree)的顶层必须满足的条件 */
Assert(IsA(root->parse->jointree, FromExpr));
/* 递归开始时,没有包含的join和appendrel */
root->parse->jointree = (FromExpr *)
pull_up_subqueries_recurse(root, (Node *) root->parse->jointree,
NULL, NULL, NULL);
/* 断言:递归结束后,root->parse->jointree 仍然应该是一个FromExpr类型 */
Assert(IsA(root->parse->jointree, FromExpr));
}
static Node *
pull_up_subqueries_recurse(PlannerInfo *root, Node *jtnode,
JoinExpr *lowest_outer_join,
JoinExpr *lowest_nulling_outer_join,
AppendRelInfo *containing_appendrel)
{
/* 检查堆栈深度以避免栈溢出 */
check_stack_depth();
/* 检查查询是否被取消 */
CHECK_FOR_INTERRUPTS();
/* 如果条件中有rownum,设置hasRownumQual为true */
if(find_rownum_in_quals(root))
{
root->hasRownumQual = true;
}
/* 断言:确保jtnode不为空 */
Assert(jtnode != NULL);
if (IsA(jtnode, RangeTblRef))
{
int varno = ((RangeTblRef *) jtnode)->rtindex;
RangeTblEntry *rte = rt_fetch(varno, root->parse->rtable);
/*
* 检查是否是子查询RTE,并且是否可以提升。
* 如果是append-relation成员,除非is_safe_append_member返回true,否则不能提升。
*/
if (rte->rtekind == RTE_SUBQUERY &&
is_simple_subquery(root, rte->subquery, rte, lowest_outer_join) &&
(containing_appendrel == NULL ||
is_safe_append_member(rte->subquery)))
return pull_up_simple_subquery(root, jtnode, rte,
lowest_outer_join,
lowest_nulling_outer_join,
containing_appendrel);
/*
* 检查是否是简单的UNION ALL子查询,如果是,则将其展平为"append relation"。
* 无论这个查询本身是否是appendrel成员,都可以这样做。
*/
if (rte->rtekind == RTE_SUBQUERY &&
is_simple_union_all(rte->subquery))
return pull_up_simple_union_all(root, jtnode, rte);
/*
* 检查是否是简单的VALUES RTE。
* 不能在外部连接下或appendrel中提升VALUES。
*/
if (rte->rtekind == RTE_VALUES &&
lowest_outer_join == NULL &&
containing_appendrel == NULL &&
is_simple_values(root, rte))
return pull_up_simple_values(root, jtnode, rte);
/*
* 检查是否是FUNCTION RTE,如果可以内联,则提升。
*/
if (rte->rtekind == RTE_FUNCTION)
return pull_up_constant_function(root, jtnode, rte,
lowest_nulling_outer_join,
containing_appendrel);
/* 否则,在这个节点上不做任何操作。 */
}
else if (IsA(jtnode, FromExpr))
{
FromExpr *f = (FromExpr *) jtnode;
ListCell *l;
Assert(containing_appendrel == NULL);
/* 递归地转换所有子节点 */
foreach(l, f->fromlist)
{
lfirst(l) = pull_up_subqueries_recurse(root, lfirst(l),
lowest_outer_join,
lowest_nulling_outer_join,
NULL);
}
}
else if (IsA(jtnode, JoinExpr))
{
JoinExpr *j = (JoinExpr *) jtnode;
Assert(containing_appendrel == NULL);
/* 递归,注意在外部连接内部时传递正确的信息 */
switch (j->jointype)
{
case JOIN_INNER:
j->larg = pull_up_subqueries_recurse(root, j->larg,
lowest_outer_join,
lowest_nulling_outer_join,
NULL);
j->rarg = pull_up_subqueries_recurse(root, j->rarg,
lowest_outer_join,
lowest_nulling_outer_join,
NULL);
break;
case JOIN_LEFT:
case JOIN_SEMI:
case JOIN_ANTI:
j->larg = pull_up_subqueries_recurse(root, j->larg,
j,
lowest_nulling_outer_join,
NULL);
j->rarg = pull_up_subqueries_recurse(root, j->rarg,
j,
j,
NULL);
break;
case JOIN_FULL:
j->larg = pull_up_subqueries_recurse(root, j->larg,
j,
j,
NULL);
j->rarg = pull_up_subqueries_recurse(root, j->rarg,
j,
j,
NULL);
break;
case JOIN_RIGHT:
j->larg = pull_up_subqueries_recurse(root, j->larg,
j,
j,
NULL);
j->rarg = pull_up_subqueries_recurse(root, j->rarg,
j,
lowest_nulling_outer_join,
NULL);
break;
default:
elog(ERROR, "unrecognized join type: %d",
(int) j->jointype);
break;
}
}
else
elog(ERROR, "unrecognized node type: %d",
(int) nodeTag(jtnode));
return jtnode;
}
subquery_planner
函数调用pull_up_subqueries
函数,pull_up_subqueries
调用pull_up_subqueries_recurse
来提升子查询。当子查询仅仅是一个简单的扫描或者连接时,就会把子查询或者子查询的一部分合并到父查询中以进行优化。一般会分一下三种情况处理:
- 在范围表中存在子查询。对于简单的子查询,直接调用
pull_up_simple_subquery
函数进行提升;而对于简单的UNION ALL子查询,调用pull_up_simple_union_all
函数进行提升,其他的情况则不处理; - 在FROM表达式中存在子查询。对于FROM列表中的每个节点都调用
pull_up_subqueries
递归处理; - 连接表达式中的子查询。调用
pull_up_subqueries
函数递归地处理.
3.预处理表达式
static Node *
preprocess_expression(PlannerInfo *root, Node *expr, int kind)
{
/*
* 如果表达式为空,则快速返回。这在处理隐式 AND 结果格式时也是正确的转换。
*/
if (expr == NULL)
return NULL;
/*
* 如果查询包含任何 join RTEs(关系表条目),则用基础关系变量替换 join 别名变量。
* 我们必须首先这样做,因为我们可能从 joinaliasvars 列表中提取的任何表达式都未经预处理。
* 例如,如果我们在子链接处理之后这样做,从 join 别名扩展出来的子链接将不会被处理。
* 但是,我们可以在非横向 RTE 函数、VALUES 列表和 TABLESAMPLE 子句中跳过此步骤,因为它们不包含当前查询级别的任何 Vars。
*/
if (root->hasJoinRTEs &&
!(kind == EXPRKIND_RTFUNC ||
kind == EXPRKIND_VALUES ||
kind == EXPRKIND_TABLESAMPLE ||
kind == EXPRKIND_TABLEFUNC))
expr = flatten_join_alias_vars(root->parse, expr);
/*
* 简化常量表达式。
*/
if (kind != EXPRKIND_RTFUNC)
expr = eval_const_expressions(root, expr);
/*
* 如果它是 qual 或 havingQual,将其规范化。
*/
if (kind == EXPRKIND_QUAL)
{
expr = (Node *) canonicalize_qual((Expr *) expr, false);
#ifdef OPTIMIZER_DEBUG
printf("After canonicalize_qual()\n");
pprint(expr);
#endif
}
/*
* 检查任何带有常量数组的 ANY ScalarArrayOpExpr 并设置可能通过使用哈希查找而不是线性搜索来更快执行的
*/
if (kind == EXPRKIND_QUAL || kind == EXPRKIND_TARGET)
{
convert_saop_to_hashed_saop(expr);
}
/*
* 将 SubLinks 展开为 SubPlans。
*/
if (root->parse->hasSubLinks)
expr = SS_process_sublinks(root, expr, (kind == EXPRKIND_QUAL));
/*
* 如果它是 qual 或 havingQual,将其转换为隐式 AND 格式。(我们不想在 eval_const_expressions 之前这样做,
* 因为后者无法正确简化顶层 AND。另外,SS_process_sublinks 期望显式 AND 格式。)
*/
if (kind == EXPRKIND_QUAL)
expr = (Node *) make_ands_implicit((Expr *) expr);
return expr;
}
表达式的预处理工作由函数preprocess_expression
完成,处理的对象有:目标属性、HAVING子OFFSET和LIMIT 子句、连接树。preprocess_expression
采用递归扫描的方式对PlannerInfo 结构中的表达式进行处理,主要的工作包括:
- 用基本关系变量取代连接别名变量,此工作由函数
fatten_join_alias_vars
完成。 - 进行常量表达式的简化,此工作由函数
eval_const_expressions
完成。主要是将常量表达式的值直接计算出来替换掉原有的表达式,例如“2+2<>4”会被False替换,“xAND False<>False”会被 False 替换。 - 对表达式进行规范化,该工作由函数
canonicalize_qual
完成。这个步骤不会对目标属性进行处理,其目标是将表达式转化成为最佳析取范式或合取范式。 - 将子链接(SubLinks)转化成为子计划(SubPlans),对每一个子链接都调用函数
make_subplan
完成转换。
函数make_subplan
调用过程
SS_process_sublinks();
|->process_sublinks_mutator();
|->make_subplan();
函数make_subplan的处理过程如下:
- 首先复制子链接SubLink中的查询树Query,如果查询树是一个EXISTS型的子计划,那么则调用simplify_EXISTS_query函数对QUery副本进行处理;
- 调用subquery_planner函数为子链接生成计划,同时设置好参数tuple_fraction来告诉底层规划器需要去多少个元组。对于EXISTS型的子查询,设置该值为1.0,取回一个元组即可;对于ALL和ANY型的子查询,从概率上说大约取到50%的元组就可以得到结果,因此设置参数值为0.5;对于其他的查询,统一设置为0;
- 调用build_subplan函数将上一步生成的计划转换为SubPlan或者InitPlan的形式,并将当前查询层次的参数列表传递给它的子计划;
- 对于生成的是SubPlan而且是一个简单的EXISTS类型的查询,调用convert_EXISTS_to_ANY函数尝试将其转换为一个ANY类型的查询并创建查询计划.
4. 处理HAVING子句
对于HAVING子句来说,除了进行前面所提到的预处理外,还需要处理其中的每个条件。如果HAVING子句中没有聚集函数的话,那么它完全可以退化到WHERE子句中去,否则的话他将被写到查询树的HavingQual字段里面。
具体来说的话:
step1:初始化一个空的HAVING子句链表newHaving;
step2:扫描HAVING子句链表中的每一条HAVING子句:
- 如果HAVING子句中存在聚集函数、易失函数或者子计划中的一种或多种,则添加至newHaving链表中;
- 如果不包含GROUP子句,则添加至WHERE子句中;
- 其他情况,将这条HAVING子句添加至WHERE子句同时也保存至newHaving中.
step3:用处理后的HAVING、子句链表newHaving替换原来的HAVING子句链表.
举个例子,例如有如下SQL语句:
1.SELECT a FROM b WHERE a.c = 10 HAVING a.d > 5;
在HAVING子句里没有聚集,那么可以直接将"a.d > 5" 提升到WHERE子句中,即:
1.SELECT a FROM b WHERE a.c = 10 AND a.d > 5;
引用文献:
跟我一起读postgresql源码(四)——Planer(查询规划模块): https://www.cnblogs.com/flying-tiger/p/6021107.html
PostgreSQL 数据库内核分析
作者介绍
葛文龙,移动云数据库助理工程师,负责云原生数据库He3DB的研发。