引言
在前两篇博客中我完成了对SQL parser内容学习和解读工作的总结以及SQL rewriter模块的简单概述。在本篇博客中我将对rewriteHandler.cpp文件中的QueryRewrite函数进行学习和解析。
文件路径
/src/gausskernel/optimizer/rewrite/rewriteHandler.cpp
函数QueryRewrite()
rewriteHandler.cpp文件是SQL查询重写过程的主模块文件,其主要的重写入口点函数就是本函数QueryRewrite。函数的完整源码和注释如下:
/*
* 函数功能:查询重写的主要入口点,通过查询重写系统对查询语句进行重写
* 入口参数:查询树Query Tree,经由parser处理所得或是由函数AcquireRewriteLocks扫描所得
* 出口参数:重写所得到的0个或多个查询。
*/
List* QueryRewrite(Query* parsetree)
{
uint64 input_query_id = parsetree->queryId;
List* querylist = NIL;
List* results = NIL; // 返回结果
ListCell* l = NULL;
CmdType origCmdType;
bool foundOriginalQuery = false;
Query* lastInstead = NULL;
// 断言抛出error信息
AssertEreport(parsetree->querySource == QSRC_ORIGINAL, MOD_OPT, "");
AssertEreport(parsetree->canSetTag, MOD_OPT, "");
/*
* Step 1
*
* 对查询树应用所有的非SELECT规则,得到0或多个查询
*/
querylist = RewriteQuery(parsetree, NIL);
/*
* Step 2
*
* 对每个查询应用RIR规则
*
* 这里也可以直接用原始的QueryID标记每个查询
*/
results = NIL;
foreach (l, querylist) {
Query* query = (Query*)lfirst(l);
query = fireRIRrules(query, NIL, false);
query->queryId = input_query_id;
results = lappend(results, query);
}
/*
* Step 3
*
* 判定查询结果支持设置command-result标签,然后更新canSetTag域。如果原先的
* 查询还在LIST中,设置command tag。否则,最后相同种类的INSTEAD查询被允许
* 设置tag。
*
* 注意:这些规则可能导致没有查询设置标记,查询分流必须基于未重写查询设置
* 一个默认的default标记来处理此问题。
*
* 断言用于确认结果列表中至多有一个查询被标记为canSetTag。如果不进行断言
* 检查,可以在找到一个原始查询时就跳出循环。
*/
origCmdType = parsetree->commandType;
foundOriginalQuery = false;
lastInstead = NULL;
foreach (l, results) {
Query* query = (Query*)lfirst(l);
if (query->querySource == QSRC_ORIGINAL) {
AssertEreport(query->canSetTag, MOD_OPT, "");
#ifndef PGXC
AssertEreport(!foundOriginalQuery, MOD_OPT, "");
#endif
foundOriginalQuery = true;
#ifndef USE_ASSERT_CHECKING
break;
#endif
} else {
AssertEreport(!query->canSetTag, MOD_OPT, "");
if (query->commandType == origCmdType &&
(query->querySource == QSRC_INSTEAD_RULE || query->querySource == QSRC_QUAL_INSTEAD_RULE))
lastInstead = query;
}
}
if (!foundOriginalQuery && lastInstead != NULL)
lastInstead->canSetTag = true;
if (CONVERT_STRING_DIGIT_TO_NUMERIC) {
foreach (l, results) {
(void)PreprocessOperator((Node*)lfirst(l), NULL);
}
}
return results;
}
函数是查询重写的主要入口点。其入口参数为一个查询树parseTree,在对查询树进行重写工作之后,返回重写所得到的0个或多个查询。值得注意的是,输入的参数parseTree要么是经过parser处理后得到的查询树直接传递而来(关于parser模块的工作原理可以查看笔者的往期博客),要么就需要经由函数AcquireRewriteLocks扫描,为查询中的所有关系获取适当的锁,以确保在重写和规划查询时,关系模式不会发生变化。
函数在进行了变量声明,并利用断言进行错误检查之后,分为3步对查询树进行重写工作:
- 对查询树应用所有的非SELECT规则;
- 对每个查询应用RIR规则;
- 设置command-result标记,并依此更新canSetTag字段。
接下来我们逐一进行分析。
Step 1
函数通过调用 RewriteQuery() 函数,对输入的查询应用“non-SELECT”规则。这里需要进行一些知识补充。我们知道openGauss数据库内核源于Postgres 9.2.4,那么我们先来看PG中对于查询重写“规则”的定义:
PG中的规则系统
规则系统是查询重写模块的核心内容。PG中的规则系统由一些列的规则组成,这些规则都存储在系统表pg_rewrite中。该表的结构如下所示,每一条记录即为一条规则。
/*
* src/include/catalog/pg_rewrite.h
*/
CATALOG(pg_rewrite,2618) BKI_SCHEMA_MACRO
{
NameData rulename;
Oid ev_class;
int2 ev_attr;
char ev_type;
char ev_enabled;
bool is_instead;
#ifdef CATALOG_VARLEN
pg_node_tree ev_qual;
pg_node_tree ev_action;
#endif
} FormData_pg_rewrite;
对于每条规则元组,字段ev_class表示该规则适用的表名,如果在该表的指定属性(ev_attr字段)上执行特定的命令(ev_type字段)且满足了规则的条件(ev_qual字段)时,用规则的动作(ev_action字段)替换原始命令的动作或将规则的动作附加在原始命令之前(或者之后)。
对于上述各个字段的功能描述总结如下表:
字段 | 数据类型 | 描述 |
---|---|---|
rulename | NameData | 规则名 |
ev_class | Oid | 使用该规则的表名称 |
ev_attr | int2 | 规则使用的属性 |
ev_type | char | 规则使用的命令类型:1=SELECT,2=UPDATE,3=INSERT,4=DELETE |
is_instead | bool | 如果是INSEAD规则,则为真 |
ev_qual | text (pg_node_tree) | 规则的条件表达式(WHERE子句) |
ev_action | text (pg_node_tree) | 规则动作的查询树(DO子句) |
根据pg_rewrite的不同属性,规则可以按两种方式分类:按照规则适用的命令类型分类,可以分成SELECT、UPDATE、INSERT和DELETE四种;按照规则执行动作的方式分类,可以分成INSTEAD(替代)规则和ALSO规则。
1. SELECT/INSERT/UPDATE/DELETE规则:
SELECT/INSERT/UPDATE/DELETE规则通过pg_rewrite表的ev_type属性来区分。它们的区别如下:SELECT规则中只能有一个动作,而且是不带条件的INSTEAD规则;而INSERT/UPDATE/DELETE规则:
- 可以没有动作,也可以有多个动作;
- 可以是INSTEAD规则,也可是ALSO规则;
- 可以使用伪关系NEW和OLD;
- 可是使用规则条件;
- 不修改查询树,而是重建新的零到多个查询树(原始的查询树会被丢弃)。
2. INSTEAD规则和ALSO规则:
这两个规则通过pg_rewrite表的is_instead属性来区分。INSTEAD规则就是直接用规则中定义的动作替代原始查询树中的对规则所在表的引用(例如对视图的处理)。而对于ALSO规则,情况就复杂一些:
- 对于INSERT,原始查询在规则动作执行之前完成,这样可以保证规则动作能引用插入的行;
- 对于UPDATE/DALETE,原始查询在规则动作之后完成,这样能保证规则动作可以引用将要更新或者删除的元组。
那么看到这里就清楚了,PG中的non-SELECT规则,实际上指的是INSERT/UPDATE/DELETE规则。
openGauss中的查询分类
在之前对analyze.cpp进行讲解的博客中我曾经简单地介绍了结构体Query的作用和定义。而在Query结构中有一commandType字段对查询指令(查询树)做了如下分类:
/*---/src/include/nodes/parsenodes_common.h---*/
typedef struct Query {
NodeTag type;
CmdType commandType; /* select|insert|update|delete|merge|utility */
……
} Query;
可以看到除了PG中的SELECT/INSERT/UPDATE/DELETE以外,还有utility和MERGE两个类型。其中utility并不是一个具体的语句,它包括notify,alter,copy等命令。这里我们对MERGE语句进行简单的介绍:
可以看到PG并不支持MERGE语句,但是openGauss是支持的。那么看到这里,我们就知道,函数调用RewriteQuery对输入的查询进行non-SELECT规则的应用,实际上是对INSERT/UPDATE/DELETE/MERGE类型查询应用重写规则进行重写。下面是函数RewriteQuery的部分源代码:
if (event != CMD_SELECT && event != CMD_UTILITY) {
……
if (event == CMD_INSERT) {
……
} else if (event == CMD_UPDATE) {
……
} else if (event == CMD_MERGE) {
/*
* Rewrite each action targetlist separately
*/
foreach (lc1, parsetree->mergeActionList) {
MergeAction* action = (MergeAction*)lfirst(lc1);
switch (action->commandType) {
case CMD_DELETE: /* Nothing to do here */
break;
case CMD_UPDATE:
action->targetList = rewriteTargetListMergeInto(action->targetList,
action->commandType,
rt_entry_relation,
parsetree->resultRelation,
NULL);
break;
case CMD_INSERT: {
action->targetList = rewriteTargetListMergeInto(action->targetList,
action->commandType,
rt_entry_relation,
parsetree->resultRelation,
NULL);
} break;
default: {
ereport(ERROR,
(errcode(ERRCODE_UNEXPECTED_NODE_STATE),
errmsg("unrecognized commandType: %d", action->commandType)));
} break;
}
}
}
Step 2
函数通过调用fireRIRrules函数对每一个查询(查询树)应用RIR规则。通过Step 1的分析,我们已经可以推测出,在函数fireRIRrules中会完成对SELECT语句查询重写规则的应用,因为non-SELECT的规则应用已经在Step 1中完成了。
下为函数fireRIRrules的部分源码,可以看到函数只对SELECT语句获取了可应用的RIR规则。不过值得一提的是,尽管函数并不负责对UPDATE/DELETE/MERGE语句的重写工作,但函数会对这三种语句应用行级安全策略,具体的细节我们这里不做展开。
// 收集我们必须使用的RIR规则
……
for (i = 0; i < rules->numLocks; i++) {
rule = rules->rules[i];
if (rule->event != CMD_SELECT) {
continue;
}
……
}
……
// 应用行级安全策略
if (SupportRlsOnNode &&
((parsetree->commandType == CMD_SELECT) || (parsetree->commandType == CMD_UPDATE) ||
(parsetree->commandType == CMD_DELETE) || (parsetree->commandType == CMD_MERGE))) {
……
}
}
在这里我们对RIR规则进行一点补充介绍。
Rewriter中的RIR规则
RIR规则(Rewriter Intermediate Representation rules)是数据库管理系统中重写器(Rewriter)组件中使用的一组转换规则。RIR规则定义了在重写过程中可以应用于查询计划的转换。这些规则允许重写器通过应用各种优化技术来修改原始查询计划,以实现更好的性能。
RIR规则通常包括查询简化、谓词下推、连接重新排序、索引选择和其他查询优化技术的规则。每个规则描述了可以应用于查询计划的特定优化或转换。重写器迭代地将这些规则应用于查询计划,直到无法再进行进一步优化为止。
通过利用RIR规则,重写器可以重新组织和优化查询计划,以最小化执行成本、减少磁盘访问次数、有效利用索引,并提高整体查询性能。需要注意的是,具体的RIR规则集可以因使用的数据库管理系统而异。不同的系统可能具有不同的规则和针对特定查询处理能力和底层存储引擎进行优化的方法。
那么我们就清楚了。在Step 2中函数会通过调用 fireRIRrules(),对输入的每一个查询树进行查询简化、谓词下推、连接重新排序、索引选择等策略的优化和重写过程。这一步完成后,SELECT语句的重写工作也已经完成。
Step 3
判定查询结果支持设置command-result标签,然后更新canSetTag域。如果原先的查询还在LIST中,设置command tag。否则,最后相同种类的INSTEAD查询被允许设置tag。这一步实际上是在重写工作完成之后,对所得结果的相应字段进行更新,并且利用断言进行错误检查的过程。Step 3完成后,函数将重写所得结果result以List * 的形式返回给调用者。
总结
在本篇博客中我对查询重写主模块rewriteHandler.cpp中的主入口点函数QueryRewrite的逻辑进行了详细的解读和相应的知识介绍。在下一篇博客中,我会继续对rewriteHandler.cpp的文件内容进行学习和解读。