diff --git "a/TestTasks/grozurem/IKUN\344\270\215\345\220\203\351\270\241\350\202\211\351\230\237\344\274\215\346\200\273\347\273\223.md" "b/TestTasks/grozurem/IKUN\344\270\215\345\220\203\351\270\241\350\202\211\351\230\237\344\274\215\346\200\273\347\273\223.md" new file mode 100644 index 0000000000000000000000000000000000000000..e8978e71328a39b2e724f71fb09b71357d42a597 --- /dev/null +++ "b/TestTasks/grozurem/IKUN\344\270\215\345\220\203\351\270\241\350\202\211\351\230\237\344\274\215\346\200\273\347\273\223.md" @@ -0,0 +1,32 @@ +大家好!我们ikun不吃鸡肉队所主要评注的部分为SQL引擎的解析器部分(文件地址/src/common/backend/parse) + +SQL在数据库管理系统当中的编译过程符合编译器实现的常规过程,需要进行词法分析、语法分析以及句义分析。 +词法分析:从查询语句中识别出系统支持的关键字、标识符、操作符、终结符等,确定每个词固有的词性。 + +语法分析:根据SQL的标准定义语法规则,使用词法分析中产生的词去匹配语法规则,如果一个SQL能够匹配一个语法规则,则生成对应的抽象语法树。 + +语义分析:对抽象语法树进行有效性检查,检查语法树中对应的表、列、函数、表达式是否有对应的元数据,将抽象语法树转换为查询树。 + +解析器运作流程: +openGuass的SQL解析器执行SQL命令的人口函数是simple_ query。用户输人的SQL命令会作为字符串sql_ query_ string传给raw.parser函数,由raw_parser函数调base_yyparse进行词法分析和语法分析,生成语法树添加到链表parsetree_ list 中。完成语法分析后,对于parsetree_ list中的每棵语法树parsetree,openGuass会调用parse. analyze 函数进行语义分析,根据SQL命令的不同,执行对应的人口函数,最终生成查询树。 + +在博客中,我们分别介绍了: +parse_agg.cpp:用于在解析器中处理聚合和窗口函数。 +详解代码: +https://forum.gitlink.org.cn/forums/7360/detail +https://forum.gitlink.org.cn/forums/7371/detail +https://forum.gitlink.org.cn/forums/7365/detail +https://forum.gitlink.org.cn/forums/7769/detail +https://forum.gitlink.org.cn/forums/7785/detail +https://forum.gitlink.org.cn/forums/8162/detail +parse_node.cpp:为查询树制作解析节点。 +详解代码: +https://forum.gitlink.org.cn/forums/7366/detail +scansup.cpp:保存转换语义,删改语句的函数。 +详解代码: +https://forum.gitlink.org.cn/forums/7368/detail +kwlookup.cpp:判断信息的关键字,从而将关键词返回为具体的token给程序。 +详解代码: +https://forum.gitlink.org.cn/forums/7772/detail + +因参赛时间有限,加之小组成员课业繁忙,对于本次比赛译注工作并不完善,但我们在撰写博客时,仍然学到了很多新鲜的知识,接触到了华为高斯这个突出的数据库,我们对于数据库也有了基本的了解。我们会珍惜这次机会带给我们的经验与教训,我们会不断努力,深化对程序的理解,为使代码进一步方便人们的生活而努力! diff --git "a/TestTasks/grozurem/kwlookup.cpp\350\247\243\350\257\273.md" "b/TestTasks/grozurem/kwlookup.cpp\350\247\243\350\257\273.md" new file mode 100644 index 0000000000000000000000000000000000000000..98cf1b432e5257ebe5a990ed9f0038f66906d878 --- /dev/null +++ "b/TestTasks/grozurem/kwlookup.cpp\350\247\243\350\257\273.md" @@ -0,0 +1,54 @@ + 本篇博客主要解读kwlookup.cpp,本文件为工具型文件,只有ScanKeywordLookup()一个函数,主要功能为判断信息的关键字,从而将关键词返回为具体的token给程序。 + + ```* 返回指向ScanKeyword表项的指针,如果不匹配则返回NULL。  +  * 匹配是不区分大小写的。注意,我们故意使用简化的大小写转换,只将'A'-' Z'翻译成'a'-' z',即使我们在tolower()会产生更多或不同的翻译。这是为了符合SQL99规范,该规范规定即使非关键字标识符接收不同的大小写规范化映射,也要以这种方式匹配关键字。 +  / + const ScanKeyword* ScanKeywordLookup(const char* text, const ScanKeyword* keywords, int num_keywords) + { + int len, i; + char word[NAMEDATALEN] = {0}; + const ScanKeyword* low = NULL; + const ScanKeyword* high = NULL; + if (text == NULL) { + return NULL; + } + len = strlen(text); + /* 我们假设所有关键字都比NAMEDATALEN短。 */ + if (len >= NAMEDATALEN) { + return NULL; + } + /* + 应用仅限ascII的下标。我们一定不要使用tolower(),因为它可能在某些地区产生错误的翻译(如土耳其语)。 + */ + for (i = 0; i < len; i++) { + char ch = text[i]; + if (ch >= 'A' && ch <= 'Z') { + ch += 'a' - 'A'; + } + word[i] = ch; + } + word[len] = '\0'; + /* + *使用strcmp()比较执行二进制搜索。 + */ + low = keywords; + high = keywords + (num_keywords - 1); + while (low <= high) { + const ScanKeyword* middle = NULL; + int difference; + middle = low + (high - low) / 2; + difference = strcmp(middle->name, word); + if (difference == 0) { + return middle; + } + //如果middle->name比word短,返回-1 + else if (difference < 0) { + low = middle + 1; + } + //如果middle->name比word长,返回1 + else { + high = middle - 1; + } + } + return NULL; + } \ No newline at end of file diff --git "a/TestTasks/grozurem/parse_agg\350\247\243\350\257\273\357\274\210\344\270\200\357\274\211.md" "b/TestTasks/grozurem/parse_agg\350\247\243\350\257\273\357\274\210\344\270\200\357\274\211.md" new file mode 100644 index 0000000000000000000000000000000000000000..d63c4cb5b3e05d0220008ddf7c8c9a74cd1883ad --- /dev/null +++ "b/TestTasks/grozurem/parse_agg\350\247\243\350\257\273\357\274\210\344\270\200\357\274\211.md" @@ -0,0 +1,194 @@ +一、parse_agg.cpp的整体介绍 +parse_agg.cpp用于在解析器中处理聚合和窗口函数。 +下面将系统地介绍聚合和窗口函数(函数的定义和语法参考自https://www.yiibai.com/)。 +1.聚合函数 + 聚合函数的优点: +(1)除了 COUNT 以外,聚合函数忽略空值。 +(2)聚合函数经常与 SELECT 语句的 GROUP BY 子句一同使用。 +(3)所有聚合函数都具有确定性。任何时候用一组给定的输入值调用它们时,都返回相同的值。 +(4)标量函数:只能对单个的数字或值进行计算。主要包括字符函数、日期/时间函数、数值函数和转换函数这四类。 + +聚合函数主要分为以下几种: +(1)AVG()—— 返回集合的平均值 + + AVG( ALL | DISTINCT) + + ALL关键字指示AVG()函数计算所有值的平均值,而DISTINCT关键字强制函数仅对不同的值进行操作。 默认情况下,使用ALL选项 + +(2)COUNT()—— 返回集合中的项目数 + + COUNT ( [ALL | DISTINCT] column | expression | *) + +(3)MAX()—— 返回集合中的最大值 + + MAX(column | expression) + +(4)MIN()—— 返回集合中的最小值 + + MIN(column | expression) + +(5)SUM()—— 返回集合中所有或不同值的总和 + + SUM(ALL | DISTINCT column) + + 聚合函数对一组值执行计算并返回单一的值。除 COUNT 以外,聚合函数忽略空值,如果COUNT函数的应用对象是一个确定列名,并且该列存在空值,此时COUNT仍会忽略空值。聚合函数经常与SELECT语句的GROUP BY子句的HAVING一同使用。(来源于百度百科) + + SELECT c1, aggregate_function(c2) + FROM table + GROUP BY c1; + +聚合函数还有其他几种(该部分内容参考自https://blog.csdn.net/s00229295/article/details/96041555): +(6)count_big()返回指定组中的项目数量。 + 与COUNT()函数区别:count_big()返回bigint值,而COUNT()返回的是int值。 + +(7) grouping()产生一个附加的列。 + 当用cube或rollup运算符添加行时,输出值为1; + 当所添加的行不是由cube或rollup产生时,输出值为0. + +(8)binary_checksum() 返回对表中的行或表达式列表计算的二进制校验值,用于检测表中行的更改。 + +(9)checksum_agg() 返回指定数据的校验值,空值被忽略。 + +(10)checksum() 返回在表的行上或在表达式列表上计算的校验值,用于生成哈希索引。 + +(11)stdev()返回给定表达式中所有值的统计标准偏差。 + +(12)stdevp() 返回给定表达式中的所有值的填充统计标准偏差。 + +(13)var() 返回给定表达式中所有值的统计方差。 + +(14)varp()返回给定表达式中所有值的填充的统计方差。 + +2.窗口函数 + 窗口函数,也叫OLAP函数(Online Anallytical Processing,联机分析处理),可对数据库数据进行实时分析处理。窗口函数作用于一个数据集合。窗口函数的一个概念就是当前行,当前行属于某个窗口就是从整个数据集选取一部分数据进行聚合/排名等操作。(来源于百度百科) + + 窗口的概念非常重要,它可以理解为记录集合,窗口函数也就是在满足某种条件的记录集合上执行的特殊函数,对于每条记录都要在此窗口内执行函数,有的函数,随着记录不同,窗口大小都是固定的,这种属于静态窗口;有的函数则相反,不同的记录对应着不同的窗口,这种动态变化的窗口叫滑动窗口。 + +作用: + 解决排名问题 e.g.班级同学按照期末考试成绩排名 + 解决topN问题 e.g.班级前三获得保研名额 + +语法: + select 窗口函数 over (partition by 用于分组的列名, order by 用于排序的列名 + +分类: + 排名函数:ROW_NUMBER(),RANK(),DENSE_RANK() +(1)ROW_NUMBER()为结果集的分区中的每一行分配一个连续的整数。行号以每个分区中第一行的行号开头。 + + ROW_NUMBER() OVER ( + [PARTITION BY partition_expression, ... ] + ORDER BY sort_expression [ASC | DESC], ... + ) + + PARTITION BY子句将结果集划分为分区。ROW_NUMBER()函数分别应用于每个分区,并重新初始化每个分区的行号。PARTITION BY子句是可选的。如果未指定,ROW_NUMBER()函数会将整个结果集视为单个分区。ORDER BY子句定义结果集的每个分区中的行的逻辑顺序。ORDER BY子句是必需的,因为ROW_NUMBER()函数对顺序敏感。 + +(2)RANK()函数为结果集的分区中的每一行分配一个排名。 + + RANK() OVER ( + [PARTITION BY partition_expression, ... ] + ORDER BY sort_expression [ASC | DESC], ... + ) + + 首先,PARTITION BY子句划分应用该函数的结果集分区的行。其次, ORDER BY子句指定应用该函数每个分区中行的逻辑排序顺序。RANK()函数对于求解前N个和后N个报表很有用。 + +(3)DENSE_RANK()为结果集的分区中的每一行分配一个排名。与RANK()函数不同,DENSE_RANK()函数返回连续的排名值。如果每个分区中的行具有相同的值,则它们将获得相同的排名。 + + DENSE_RANK() OVER ( + [PARTITION BY partition_expression, ... ] + ORDER BY sort_expression [ASC | DESC], ... + ) + + DENSE_RANK()函数以ORDER BY子句定义的指定顺序应用于PARTITION BY子句定义的每个分区的行。它会在划分分区边界时重置等级。 + PARITION BY子句是可选的。如果省略它,该函数会将整个结果集视为单个分区。 + + 下面通过一个简单的例子来讨论DENSE_RANK()与RANK()的不同之处: + 下表为某小学一年二班的成绩单,老师需要使用DENSE_RANK()或RANK()函数对班级同学的成绩进行排序。 +姓名 分数 +张三 100 +李四 99 +jack 99 +小红 80 +小明 59 + +若使用DENSE_RANK()排序该班成绩,结果如下 +姓名 分数 排名 +张三 100 1 +李四 99 2 +jack 99 3 +小红 80 4 +小明 59 5 + +若使用RANK()排序该班成绩,结果如下 +姓名 分数 排名 +张三 100 1 +李四 99 2 +jack 99 2 +小红 80 4 +小明 59 5 + + 由此可见,当排序依据的数据相同时,两者会返回不同的数值。以该表为例,李四和jack的成绩都为99分,如果使用DENSE_RANK(),则两人的排名分别为第二和第三;但如果使用RANK(),两人为并联第二。也就是说,DENSE_RANK()在处理相同的等级时,等级的数值不会跳过,RANK()则跳过。 + 聚合函数:MAX(),MIN(),COUNT(),SUM(),AVG() + MAX(),MIN(),COUNT(),SUM(),AVG()等函数在聚合函数中已经提到,此处不再赘述。 + 向前向后取值:LAG(),LEAD() +(1)LAG()提供对当前行之前的指定物理偏移量的行的访问。换句话说,通过使用LAG()函数,可以从当前行访问上一行的数据或上一行之前的行,依此类推。LAG()函数对于将当前行的值与前一行的值进行比较非常有用。 + + LAG(return_value ,offset [,default]) + OVER ( + [PARTITION BY partition_expression, ... ] + ORDER BY sort_expression [ASC | DESC], ... + ) + + return_value - 基于指定偏移量的前一行的返回值。 返回值必须求值为单个值,不能是另一个窗口函数。 + offset - 从当前行返回的行数,用于访问数据。 offset可以是计算结果为正整数的表达式,子查询或列。如果未明确指定offset,则它的默认值为1。 + default - 是当offset超出分区范围时要返回的值。如果未指定,则默认为NULL。PARTITION BY子句将结果集的行分配到应用LAG()函数的分区。如果省略PARTITION BY子句,该函数会将整个结果集视为单个分区。 + ORDER BY子句指定应用LAG()函数的每个分区中行的逻辑顺序。 + +(2)LEAD()与LAG()语法以及用法基本相同,唯一区别就是LEAD()提供对当前行之后(LAG()提供对当前行之前)的指定物理偏移量的行的访问,此处不再赘述。 + 百分位:PERCENT_RANK() +(1)PERCENT_RANK()类似于CUME_DIST()函数。PERCENT_RANK()函数计算结果集的分区中值的相对位置。 + 取值函数:FIRST_VALUE(),LAST_VALUE(),NTH_VALUE() +(1)FIRST_VALUE()返回结果集的有序分区中的第一个值。 + + FIRST_VALUE ( scalar_expression ) + OVER ( + [PARTITION BY partition_expression, ... ] + ORDER BY sort_expression [ASC | DESC], ... + [rows_range_clause] + ) + + scalar_expression是针对结果集的有序分区的第一行的值计算的表达式。scalar_expression可以是计算为单个值的列,子查询或表达式。它不能是一个窗口函数。 + PARTITION BY子句将结果集的行分配到应用FIRST_VALUE()函数的分区中。 如果不使用PARTITION BY子句,FIRST_VALUE()函数会将整个结果集视为单个分区。 + ORDER BY子句指定应用FIRST_VALUE()函数的每个分区中行的逻辑顺序。 + rows_range_clause通过定义起点和终点进一步限制分区内的行。 + +(2)LAST_VALUE()与FIRST_VALUE()语法以及用法基本相同,唯一区别就是LAST_VALUE()返回结果集的有序分区中的最后一个值(FIRST_VALUE()返回结果集有序分区中的第一个值),此处不再赘述。 + +(3)NTH_VALUE()能够从有序行集中的第N行获取值。 + + NTH_VALUE(expression, N) + FROM FIRST + OVER ( + partition_clause + order_clause + frame_clause + ) + + NTH_VALUE()函数返回expression窗口框架第N行的值。如果第N行不存在,则函数返回NULL。N必须是正整数,例如1,2和3。 + FROM FIRST指示NTH_VALUE()功能在窗口帧的第一行开始计算。 + 分箱函数:ntile() +(1)NTILE()将有序分区的行分配到指定数量的大致相等的组或桶中。 它从一个开始为每个组分配一个桶号。对于组中的每一行,NTILE()函数分配一个桶号,表示该行所属的组。 + + NTILE(buckets) OVER ( + [PARTITION BY partition_expression, ... ] + ORDER BY sort_expression [ASC | DESC], ... + ) + + buckets - 行划分的桶数。 存储桶可以是表达式或子查询,其计算结果为正整数。 它不能是一个窗口功能。 + PARTITION BY子句将结果集的行分配到应用了NTILE()函数的分区中。ORDER BY子句指定应用NTILE()的每个分区中行的逻辑顺序。 + 如果行数不能被桶整除,则NTILE()函数返回两个大小的组,它们的差值为1。 较大的组总是按照OVER()子句中ORDER BY指定的顺序位于较小的组之前。 + 另一方面,如果行的总数可以被桶整除,则该函数在桶之间均匀地划分行。 + +总结: + 窗口函数语法:<窗口函数> over(partition by <用于分组的列名> order by <用于排序的列名>) + + 个人水平有限,感谢您的阅读。 diff --git "a/TestTasks/grozurem/parse_agg\350\247\243\350\257\273\357\274\210\344\270\211\357\274\211.md" "b/TestTasks/grozurem/parse_agg\350\247\243\350\257\273\357\274\210\344\270\211\357\274\211.md" new file mode 100644 index 0000000000000000000000000000000000000000..513984717c47afea39163abf391e10b509ad7712 --- /dev/null +++ "b/TestTasks/grozurem/parse_agg\350\247\243\350\257\273\357\274\210\344\270\211\357\274\211.md" @@ -0,0 +1,322 @@ +本篇博客主要讲解 parseCheckAggregates 与 parseCheckWindowFuncs两个函数。 + +3.parseCheckAggregates用于分析和检查,并找出使用有误的聚合函数。 + + /*分析和检查,并找出使用有误的聚合函数*/ + void parseCheckAggregates(ParseState* pstate, Query* qry) + { + /*链表等一系列变量的定义*/ + List* gset_common = NIL; /*创建交集*/ + List* groupClauses = NIL; /*GROUP BY子句*/ + List* groupClauseCommonVars = NIL; + bool have_non_var_grouping = false; /*用于判断是否存在非变量分组集*/ + List* func_grouped_rels = NIL; + ListCell* l = NULL; /*创建一个列表单元*/ + bool hasJoinRTEs = false; /*用于判断是否存在RTE_JOIN*/ + bool hasSelfRefRTEs = false; /*用于判断是否存在自引用CTE条目*/ + PlannerInfo* root = NULL; /*创建一个根结点*/ + Node* clause = NULL; + /*断言只有在发现聚合或分组时才调用此函数*/ + AssertEreport(pstate->p_hasAggs || qry->groupClause || qry->havingQual || qry->groupingSets, + MOD_OPT, + "only be called if we found aggregates or grouping"); + /*如果已经生成了分组集,则展开它们并找到所有集合的交集*/ + if (qry->groupingSets) { + /*4096并没有什么特别的含义,4096只是一种限制,数值是任意的,只是为了避免病态构造带来的资源问题*/ + List* gsets = expand_grouping_sets(qry->groupingSets, 4096); + if (gsets == NULL) /*如果集合为空,报告错误信息*/ + ereport(ERROR, + (errcode(ERRCODE_STATEMENT_TOO_COMPLEX), /*显示错误代码*/ + errmsg("too many grouping sets present (max 4096)"), /*报告错误信息*/ + /*判断分析器错误位置*/ + parser_errposition(pstate, + qry->groupClause ? exprLocation((Node*)qry->groupClause) + : exprLocation((Node*)qry->groupingSets)))); + /*交集通常是空的,因此通过设置最小集合来获得交集,以提高解决问题的效率*/ + gset_common = (List*)linitial(gsets); + /*如果交集不为空,则遍历所有分组集*/ + if (gset_common != NULL) { + for_each_cell(l, lnext(list_head(gsets))) { + /* + *用交集与分组集形成新的交集 + *再用上一次循环中获得的新交集与另一分组集形成新的交集 + */ + gset_common = list_intersection_int(gset_common, (List*)lfirst(l)); + /*直到交集为空或者遍历完所有分组集为止,结束循环并继续向下进行*/ + if (gset_common == NULL) { + break; + } + } + } + /* + *如果扩展中只有一个分组集,并且groupClause为非空(这意味着分组集也不是空的) + *那么我们可以舍弃分组集,并假设只有一个正常的GROUP BY + */ + if (list_length(gsets) == 1 && qry->groupClause) { + qry->groupingSets = NIL; + } + } + /*扫描范围表以查看是否存在连接(JOIN)或者自引用CTE条目*/ + hasJoinRTEs = hasSelfRefRTEs = false; + foreach (l, pstate->p_rtable) { + RangeTblEntry* rte = (RangeTblEntry*)lfirst(l); + if (rte->rtekind == RTE_JOIN) { + hasJoinRTEs = true; + } else if (rte->rtekind == RTE_CTE && rte->self_reference) { + hasSelfRefRTEs = true; + } + } + /* + *聚合不得出现在WHERE或JOIN/ON子句中。 + *此检查应首先出现,以传递适当的错误消息;否则,可能会将错误归结于目标列表中的某个无辜变量 + *如果问题在WHERE语句中,可能会产生误导 + */ + if (checkExprHasAggs(qry->jointree->quals)) { + ereport(ERROR, + (errcode(ERRCODE_GROUPING_ERROR), + errmsg("aggregates not allowed in WHERE clause"), + parser_errposition(pstate, locate_agg_of_level(qry->jointree->quals, 0)))); + } + if (checkExprHasAggs((Node*)qry->jointree->fromlist)) { + ereport(ERROR, + (errcode(ERRCODE_GROUPING_ERROR), + errmsg("aggregates not allowed in JOIN conditions"), + parser_errposition(pstate, locate_agg_of_level((Node*)qry->jointree->fromlist, 0)))); + } + /* + *GROUP BY子句中也不允许使用聚合 + *当进行此操作时,构建一个可接受的GROUP BY表达式列表,供check_ungrouped_columns()(用于检查未分组的列)使用 + */ + /*遍历查询树中的 GROUP子句*/ + foreach (l, qry->groupClause) { + SortGroupClause* grpcl = (SortGroupClause*)lfirst(l); + TargetEntry* expr = NULL; /*创建目标条目,用于获取目标列表*/ + expr = get_sortgroupclause_tle(grpcl, qry->targetList); /*找到匹配grpc1的目标列表并传递给expr*/ + /* + *如果expr为空,则不执行下一个if语句,直接进入下一次循环 + *也就是说,直到找到目标列表或者遍历完所有子集为止 + */ + if (expr == NULL) { + continue; + } + if (checkExprHasAggs((Node*)expr->expr)) { + ereport(ERROR, + (errcode(ERRCODE_GROUPING_ERROR), + errmsg("aggregates not allowed in GROUP BY clause"), + parser_errposition(pstate, locate_agg_of_level((Node*)expr->expr, 0)))); + } + groupClauses = lcons(expr, groupClauses); + } + /* + *如果涉及到连接别名变量,必须将它们展平到基础变量,以便正确地将别名变量和非别名变量视为相等 + *如果范围表条目中都不存在RTE_JOIN,便可以跳过这个步骤 + *使用planner的flatten_join_alias_vars例程对其进行展平 + *它需要一个PlannerInfo根节点,大部分是可以虚拟的 + */ + if (hasJoinRTEs) { + root = makeNode(PlannerInfo); + root->parse = qry; + root->planner_cxt = CurrentMemoryContext; + root->hasJoinRTEs = true; + groupClauses = (List*)flatten_join_alias_vars(root, (Node*)groupClauses); + } else + root = NULL; /*如果都不存在RTE_JOIN,则不执行上一个if当中的语句*/ + /* + *检测所有分组表达式,如果都是Vars,那么就不必在递归扫描中耗费大量精力 + *在展平别名后,在groupClauseCommonVars中分别跟踪所有分组集中包含的变量 + *因为这些变量是唯一可以用来检查函数相关性的变量 + */ + have_non_var_grouping = false; + foreach (l, groupClauses) { + TargetEntry* tle = (TargetEntry*)lfirst(l); + if (!IsA(tle->expr, Var)) { /*如果有一个分组表达式不是 Var*/ + have_non_var_grouping = true; + } else if (!qry->groupingSets || list_member_int(gset_common, tle->ressortgroupref)) { + /*否则,将tle->expr附加在groupClauseCommonVars之后*/ + groupClauseCommonVars = lappend(groupClauseCommonVars, tle->expr); + } + } + /* + *检查目标列表和HAVING子句中未分组的变量 + *检查resjunk tlist元素以及常规元素,会找到来自ORDER BY和WINDOW子句的未分组变量 + *还将检查分组表达式本身(都会通过测试) + *最终确定分组表达式,为此需要遍历原始(未展平)子句以修改节点 + */ + clause = (Node*)qry->targetList; /*查询树的目标属性*/ + finalize_grouping_exprs(clause, pstate, qry, groupClauses, root, have_non_var_grouping); + /* 如果存在RTE_JOIN则展平*/ + if (hasJoinRTEs) { + clause = flatten_join_alias_vars(root, clause); + } + /*检查未分组的列*/ + check_ungrouped_columns( + clause, pstate, qry, groupClauses, groupClauseCommonVars, have_non_var_grouping, &func_grouped_rels); + clause = (Node*)qry->havingQual; + finalize_grouping_exprs(clause, pstate, qry, groupClauses, root, have_non_var_grouping); + if (hasJoinRTEs) { + clause = flatten_join_alias_vars(root, clause); + } + check_ungrouped_columns( + clause, pstate, qry, groupClauses, groupClauseCommonVars, have_non_var_grouping, &func_grouped_rels); + /*根据规范,聚合不能出现在递归项中。*/ + if (pstate->p_hasAggs && hasSelfRefRTEs) { + ereport(ERROR, + (errcode(ERRCODE_INVALID_RECURSION), + errmsg("aggregate functions not allowed in a recursive query's recursive term"), + parser_errposition(pstate, locate_agg_of_level((Node*)qry, 0)))); + } + } + +4.parseCheckWindowFuncs用于解析检查,并找出使用有误的窗口函数。WHERE、JOIN/ON、HAVING、GROUP BY和窗口规范中语句中不可使用窗口函数,其他子句如RETURNING和LIMIT在这之前已经完成检测,无需再次检查。在调用此函数之前,必须完成以上提到的WHERE等子句的转换。 + + /*解析检查,并找出使用有误的窗口函数*/ + void parseCheckWindowFuncs(ParseState* pstate, Query* qry) + { + ListCell* l = NULL; + /*断言只有在检测到窗口函数时才调用此函数*/ + AssertEreport(pstate->p_hasWindowFuncs, MOD_OPT, "Only deal with WindowFuncs here"); + /*WHERE子句中不允许使用窗口函数*/ + if (checkExprHasWindowFuncs(qry->jointree->quals)) { + ereport(ERROR, + (errcode(ERRCODE_WINDOWING_ERROR), + errmsg("window functions not allowed in WHERE clause"), + parser_errposition(pstate, locate_windowfunc(qry->jointree->quals)))); + } + /*连接条件(JOIN)中不允许使用窗口函数*/ + if (checkExprHasWindowFuncs((Node*)qry->jointree->fromlist)) { + ereport(ERROR, + (errcode(ERRCODE_WINDOWING_ERROR), + errmsg("window functions not allowed in JOIN conditions"), + parser_errposition(pstate, locate_windowfunc((Node*)qry->jointree->fromlist)))); + } + /*HAVING子句中不允许使用窗口函数*/ + if (checkExprHasWindowFuncs(qry->havingQual)) { + ereport(ERROR, + (errcode(ERRCODE_WINDOWING_ERROR), + errmsg("window functions not allowed in HAVING clause"), + parser_errposition(pstate, locate_windowfunc(qry->havingQual)))); + } + /*GROUP BY子句中不允许使用窗口函数*/ + /*遍历groupClause*/ + foreach (l, qry->groupClause) { + SortGroupClause* grpcl = (SortGroupClause*)lfirst(l); + Node* expr = NULL; + expr = get_sortgroupclause_expr(grpcl, qry->targetList); + if (checkExprHasWindowFuncs(expr)) { + ereport(ERROR, + (errcode(ERRCODE_WINDOWING_ERROR), + errmsg("window functions not allowed in GROUP BY clause"), + parser_errposition(pstate, locate_windowfunc(expr)))); + } + } + /*窗口定义中不允许使用窗口函数*/ + /*检查window specifications的表达式中是否包含当前查询级别的窗口函数调用 */ + /*遍历查询树中的windowClause*/ + foreach (l, qry->windowClause) { + WindowClause* wc = (WindowClause*)lfirst(l); + ListCell* l2 = NULL; + foreach (l2, wc->partitionClause) { + SortGroupClause* grpcl = (SortGroupClause*)lfirst(l2); + Node* expr = NULL; + expr = get_sortgroupclause_expr(grpcl, qry->targetList); + if (checkExprHasWindowFuncs(expr)) { + ereport(ERROR, + (errcode(ERRCODE_WINDOWING_ERROR), + errmsg("window functions not allowed in window definition"), + parser_errposition(pstate, locate_windowfunc(expr)))); + } + } + /*遍历 orderClause*/ + foreach (l2, wc->orderClause) { + SortGroupClause* grpcl = (SortGroupClause*)lfirst(l2); + Node* expr = NULL; + expr = get_sortgroupclause_expr(grpcl, qry->targetList); /*查询与grpcl匹配的目标列表*/ + if (checkExprHasWindowFuncs(expr)) { + ereport(ERROR, + (errcode(ERRCODE_WINDOWING_ERROR), + errmsg("window functions not allowed in window definition"), + parser_errposition(pstate, locate_windowfunc(expr)))); + } + } + /*已在transformFrameOffset中检查startOffset和limitOffset*/ + } + } + + parseCheckAggregates 与 parseCheckWindowFuncs还包含一些函数或运算符的定义、语法以及一些使用时的注意事项,下面让我来为大家简单介绍一下: +(1)GROUPING SETS运算符,用于生成多个分组集。分组集是一组使用GROUP BY子句进行分组的列。通常,单个聚合查询定义单个分组集。 + 普通查询语句有时会很冗长,导致很难阅读。并且存在性能问题,因为数据库系统必须多次扫描库存表。为解决这些问题,SQL提供了GROUPING SETS。 + + SELECT + c1, + c2, + aggregate (c3) + FROM + table + GROUP BY + GROUPING SETS ( + (c1, c2), + (c1), + (c2), + () + ); + /*此查询定义了四个分组集(c1,c2),(c1),(c2)和()*/ + +(2)连接(JOIN)子句用于把来自两个或多个表的行结合起来。在实际的数据库应用中,经常需要从多个数据表中读取数据,这时就可以使用 SQL 语句中的连接(JOIN)子句,在两个或多个数据表中查询数据。(JOIN子句的相关内容参考自https://blog.csdn.net/liitdar/article/details/80817087?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166002259116782425171503%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=166002259116782425171503&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allbaidu_landing_v2~default-5-80817087-null-null.142) + +JOIN 的用法按照功能划分,可分为如下三类: + INNER JOIN(内连接,或等值连接):获取两个表中字段匹配关系的记录; + + LEFT JOIN(左连接):获取左表中的所有记录,即使在右表没有对应匹配的记录; + + RIGHT JOIN(右连接):与 LEFT JOIN 相反,用于获取右表中的所有记录,即使左表没有对应匹配的记录。 + +(3)集合的概念以及用法(集合的相关内容参考自https://blog.csdn.net/weixin_44618297/article/details/121163342?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166002581816782388042358%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=166002581816782388042358&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allbaidu_landing_v2~default-2-121163342-null-null.142) + + 并集:对于两个给定集合A、B,由两个集合所有元素构成的集合,叫做A和B的并集。 + 记作:AUB 读作“A并B” + 例:{ 3,5 }U{ 2,3,4,6 } = { 2,3,4,5,6 } + + set_union(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest); + + 交集:对于两个给定集合A、B,由属于A又属于B的所有元素构成的集合,叫做A和B的交集。 + 记作: A∩B 读作“A交B” + 例: A = { 1,2,3,4,5 },B = { 3,4,5,6,8 },A∩B = { 3,4,5 } + + set_intersection(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest); + + 差集:假设有集合A和B,所有属于A且不属于B的元素的集合被称为A与B的差集。 + 示例:对于集合A = { a, b, c, d }和集合B = { b, c, w },则A与B 的差集为{ a, d } + + set_difference(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest); + +(4)CTE表示公用表表达式,是一个临时命名结果集,始终返回结果集。它是为了简化SQL查询,而被标准SQL引入的。(CTE的相关内容参考自https://www.php.cn/mysql-tutorials-414584.html) + 公用表表达式(CTE)可以被认为是在单个SELECT,INSERT,UPDATE,DELETE或CREATE VIEW语句的执行范围内定义的临时结果集。CTE类似于派生表,因为它不作为对象存储,并且仅在查询期间持续。与派生表不同,CTE可以是自引用的,并且可以在同一查询中多次引用。 + CTE由表示CTE的表达式名称,AS关键字和SELECT语句组成。定义CTE后,可以在SELECT,INSERT,UPDATE或DELETE语句中像表或视图一样引用它。CTE也可以在CREATE VIEW语句中用作其定义SELECT语句的一部分。 + + WITH Expression_Name [ ( ColumnName [1,...n] ) ] + AS + ( CTE query definition ) + /*我们可以通过在SELECT,INSERT,UPDATE,DELETE或MERGE语句之前直接添加WITH子句来定义CTE。WITH子句中可以包含一个或多个逗号分隔的CTE*/ + SELECT + FROM expression_name; + + CTE有两种类型:递归和非递归。 + 递归CTE:是引用自身的常用表达式。 + 非递归CTE:顾名思义,不使用递归,即不参考自身。 + + 使用CTE的好处: + 可读性:CTE提高了可读性。而不是将所有查询逻辑都集中到一个大型查询中,而是创建几个CTE,它们将在语句的后面组合。这使您可以获得所需的数据块,并将它们组合在最终的SELECT中。 + 替代视图:您可以用CTE替换视图。如果您没有创建视图对象的权限,或者您不想创建一个视图对象,因为它仅在此一个查询中使用,这很方便。 + 递归:使用CTE会创建递归查询,即可以调用自身的查询。当您需要处理组织结构图等分层数据时,这很方便。 + 限制:克服SELECT语句限制,例如引用自身(递归)或使用非确定性函数执行GROUP BY。 + 排名:每当你想使用排名函数,如ROW_NUMBER(),RANK(),NTILE()等。 + +(5)为什么聚集函数不能出现在WHERE子句中? + 简单来说,聚集函数也叫列函数,它们都是基于整列数据进行计算的,是对一个确定的结果集作用。而WHERE子句是对数据行进行过滤,即正在形成确定结果集的过程中,聚合函数作用的前提“确定的结果集”并不满足,所以会产生冲突。 + 以上对于这个问题的解答仅仅是个人观点,如想了解更多,可以参考https://laurence.blog.csdn.net/article/details/6011269?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-6011269-blog-113189814.pc_relevant_multi_platform_featuressortv2dupreplace&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-6011269-blog-113189814.pc_relevant_multi_platform_featuressortv2dupreplace&utm_relevant_index=2 + +(6)为什么GROUP BY子句中也不允许使用聚合? + GROUP BY的作用只是单纯的根据哪列进行分组,不能有任何的条件,如果需要进一步的筛选需要加HAVING,GROUP BY子句使用聚合没有任何实际意义。 + 关于这个问题,我在网上也未找到明确解答,个人观点如有误,欢迎大家批评指正。 + + 个人水平有限,感谢您的阅读。 diff --git "a/TestTasks/grozurem/parse_agg\350\247\243\350\257\273\357\274\210\344\272\214\357\274\211.md" "b/TestTasks/grozurem/parse_agg\350\247\243\350\257\273\357\274\210\344\272\214\357\274\211.md" new file mode 100644 index 0000000000000000000000000000000000000000..a043d7a7035429fde0d61a75098211b9d24826bc --- /dev/null +++ "b/TestTasks/grozurem/parse_agg\350\247\243\350\257\273\357\274\210\344\272\214\357\274\211.md" @@ -0,0 +1,415 @@ +二、代码解读 +本篇博客主要讲解transformAggregateCall与transformWindowFuncCall两个函数。 +1.transformAggregateCall用于完成调用聚合的初始转换。 + 如果parse_func.c已将函数识别为聚合体,并已设置Aggref的所有字段,但args、aggorder、aggdistinct和aggrevelSup除外。传入的args列表已通过标准表达式转换,而传入的aggorder列表没有转换。通过插入TargetEntry节点将args列表转换为目标列表,然后转换aggorder和agg_distinct规范以生成SortGroupClause节点列表,这可能会导致向targetlist添加resjunk表达式。需要确定聚合实际属于哪个查询级别,相应地设置aggrevelsup,并在相应的pstate级别中将p_hasAggs标记为真。 + + /*完成调用聚合的初始转换*/ + void transformAggregateCall(ParseState* pstate, Aggref* agg, List* args, List* aggorder, bool agg_distinct) + { + /*链表等一系列变量的定义*/ + #define anyenum_typeoid 3500 + List* tlist = NIL; + List* torder = NIL; + List* tdistinct = NIL; + AttrNumber attno = 1; + int save_next_resno; + int min_varlevel; /*最低级别变量或聚合的级别*/ + ListCell* lc = NULL; /*创建列表单元,并设置为空*/ + #ifdef PGXC + HeapTuple aggTuple; + Form_pg_aggregate aggform; + #endif /* PGXC */ + if (AGGKIND_IS_ORDERED_SET(agg->aggkind)) { + /* + *有序集合aggs包含直接args和聚合args + *直接参数保存在第一个“numDirectArgs”参数处,聚集的args位于尾部,必须将它们分开 + */ + numDirectArgs = list_length(args) - list_length(aggorder); /*计算直接args的数量*/ + List* aargs = NIL; + ListCell* lc1 = NULL; /*创建列表单元*/ + ListCell* lc2 = NULL; + Assert(numDirectArgs >= 0); /*断言直接args的数量不少于0*/ + aargs = list_copy_tail(args, numDirectArgs); /*复制列表尾部*/ + agg->aggdirectargs = list_truncate(args, numDirectArgs); /*截取掉所有的直接args,获得聚合args*/ + /* + *我们应该保存有序集合agg的排序信息,因此需要构建一个包含聚合参数(Exprs列表)的tlist(通常只有一个目标条目) + *需要保存用于转换为SortGroupClause的关于顺序的目标 + */ + forboth(lc1, aargs, lc2, aggorder) + { + TargetEntry* tle = makeTargetEntry((Expr*)lfirst(lc1), attno++, NULL, false); /*创建目标条目*/ + tlist = lappend(tlist, tle); /*将目标条目tle附加到tlist之后*/ + /*保存用于转换为SortGroup子句的关于顺序的目标*/ + torder = addTargetToSortList(pstate, tle, torder, tlist, (SortBy*)lfirst(lc2), true); /*修正未知*/ + } + /*断言DISTINCT不能在有序集合agg中使用*/ + Assert(!agg_distinct); + } else { + /*普通的聚合没有直接args*/ + agg->aggdirectargs = NIL; + /*将Exprs的普通列表转换为目标列表,并且不需要为条目指定列名*/ + foreach (lc, args) { + Expr* arg = (Expr*)lfirst(lc); + TargetEntry* tle = makeTargetEntry(arg, attno++, NULL, false); + tlist = lappend(tlist, tle); + } + /* + *如果有一个ORDER BY,需要将它转换。 + *如果列出现在ORDER BY中,但不在arg列表中,则会将列添加到tlist + 它们将被标记为resjunk为真,以便稍后我们可以将它们与常规聚合参数区分开来 + *需要弄乱 p_next_resno,因为它将用于对任何新的目标列表条目进行编号 + *p_next_resno为int类型,表示下一个分配给目标属性的资源号 + */ + save_next_resno = pstate->p_next_resno; + pstate->p_next_resno = attno; + /*转换排序*/ + torder = transformSortClause(pstate, + aggorder, + &tlist, + true, /*修正未知*/ + true); /*强制执行SQL99规则*/ + /*如果我们有DISTINCT,将其转换为distinctList*/ + if (agg_distinct) { + tdistinct = transformDistinctClause(pstate, &tlist, torder, true); + /*如果对聚合散列添加过不同的执行程序支持,则删除此检查*/ + /*遍历tdistinct*/ + foreach (lc, tdistinct) { + SortGroupClause* sortcl = (SortGroupClause*)lfirst(lc); + /* + *如果属性所属源表的OID无效,则报告错误信息:不能使用无法识别类型的排序运算符 + *具有DISTINCT的聚合必须能够对其输入进行排序 + */ + if (!OidIsValid(sortcl->sortop)) { + Node* expr = get_sortgroupclause_expr(sortcl, tlist); + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_FUNCTION), + errmsg( + "could not identify an ordering operator for type %s", format_type_be(exprType(expr))), + errdetail("Aggregates with DISTINCT must be able to sort their inputs."), + parser_errposition(pstate, exprLocation(expr)))); + } + } + } + pstate->p_next_resno = save_next_resno; + } + /*使用转换结果更新Aggref*/ + agg->args = tlist; + agg->aggorder = torder; + agg->aggdistinct = tdistinct; + /*聚合的级别与其参数中最低级别变量或聚合的级别相同;或者如果它根本不包含变量,我们假设它是本地的*/ + min_varlevel = find_minimum_var_level((Node*)agg->args); + /* + *聚合不能直接包含同一级别的另一个聚合调用(尽管外部aggs可以) + *如果没有找到任何本地vars或aggs,我们可以跳过此检查 + *SQL规定聚合函数调用不能嵌套 + */ + if (min_varlevel == 0) { + if (pstate->p_hasAggs && checkExprHasAggs((Node*)agg->args)) { + ereport(ERROR, + (errcode(ERRCODE_GROUPING_ERROR), + errmsg("aggregate function calls cannot be nested"), + parser_errposition(pstate, locate_agg_of_level((Node*)agg->args, 0)))); + } + } + /*函数调用不能包含窗口函数调用*/ + if (pstate->p_hasWindowFuncs && checkExprHasWindowFuncs((Node*)agg->args)) { + ereport(ERROR, + (errcode(ERRCODE_GROUPING_ERROR), + errmsg("aggregate function calls cannot contain window function calls"), + parser_errposition(pstate, locate_windowfunc((Node*)agg->args)))); + } + if (min_varlevel < 0) { + min_varlevel = 0; + } + agg->agglevelsup = min_varlevel; + /*将正确的pstate标记为具有聚合*/ + while (min_varlevel-- > 0) + pstate = pstate->parentParseState; + pstate->p_hasAggs = true; + /* + *如果我们在LATERAL进行聚合查询时必须在它的FROM子句中,就说明在此聚合是不适宜的 + *FROM子句中不允许使用聚合 + */ + if (pstate->p_lateral_active) + ereport(ERROR, + (errcode(ERRCODE_GROUPING_ERROR), + errmsg("aggregates not allowed in FROM clause"), + parser_errposition(pstate, agg->location))); + #ifdef PGXC + /*PGXC Datanode聚合的返回数据类型应始终返回transition函数的结果 + *这是Coordinator上的collection函数所期望的 + *查找聚合定义并替换agg->aggtype + */ + aggTuple = SearchSysCache(AGGFNOID, ObjectIdGetDatum(agg->aggfnoid), 0, 0, 0); /*查找系统缓存*/ + if (!HeapTupleIsValid(aggTuple)) + ereport(ERROR, + /*报告错误信息:聚合的缓存查找失败*/ + (errcode(ERRCODE_CACHE_LOOKUP_FAILED), errmsg("cache lookup failed for aggregate %u", agg->aggfnoid))); + aggform = (Form_pg_aggregate)GETSTRUCT(aggTuple); + agg->aggtrantype = aggform->aggtranstype; + agg->agghas_collectfn = OidIsValid(aggform->aggcollectfn); + /* + *当视图包含avg函数时,我们需要确保升级成功 + *否则可能会导致类似错误:operator does not exist:bigint[]=integer + *例如:create view t1_v as select a from t1 group by a having avg(a) = 10; + */ + /*对于用户自定义的枚举类型,不要在此处替换agg->aggtype + *否则可能会导致错误:operator does not exist:operator does not exist: (user-defined enum type) = anyenum; + */ + if (IS_PGXC_DATANODE && !isRestoreMode && !u_sess->catalog_cxt.Parse_sql_language && !IsInitdb && + !u_sess->attr.attr_common.IsInplaceUpgrade && !IS_SINGLE_NODE && (anyenum_typeoid != agg->aggtrantype)) + agg->aggtype = agg->aggtrantype; + ReleaseSysCache(aggTuple); + #endif + } + +2.transformWindowFuncCall用于完成窗口函数调用的初始转换。 + 如果parse_func.c已将该函数识别为窗口函数,并已设置除winref之外的所有窗口函数字段,则必须将WindowDef添加到pstate(如果不是已经存在的WindowDef的副本),并设置winref链接到它;在pstate中标记p_hasWindowFuncs为真。与聚合不同,只需要考虑嵌套最紧密的pstate级别——根据SQL规范,没有“外部窗口函数”。 + + /*完成窗口函数调用的初始转换*/ + void transformWindowFuncCall(ParseState* pstate, WindowFunc* wfunc, WindowDef* windef) + { + /*窗口函数调用不能包含另一个(但aggs可以)*/ + if (pstate->p_hasWindowFuncs && checkExprHasWindowFuncs((Node*)wfunc->args)) { + ereport(ERROR, + (errcode(ERRCODE_WINDOWING_ERROR), + errmsg("window function calls cannot be nested"), /*窗口函数调用不能嵌套*/ + parser_errposition(pstate, locate_windowfunc((Node*)wfunc->args)))); + } + /* + *如果OVER子句只指定了一个窗口名,那么查找该WINDOW子句(最好是存在的) + *否则,请尝试匹配OVER子句的所有属性,并在p_windowdefs中创建一个新条目 + *如果仍未匹配到,就把它全部列出来 + */ + if (windef->name) { + Index winref = 0; + ListCell* lc = NULL; + AssertEreport(windef->refname == NULL && windef->partitionClause == NIL && windef->orderClause == NIL && + windef->frameOptions == FRAMEOPTION_DEFAULTS, + MOD_OPT, + ""); + foreach (lc, pstate->p_windowdefs) { + WindowDef* refwin = (WindowDef*)lfirst(lc); + winref++; + if (refwin->name && strcmp(refwin->name, windef->name) == 0) { + wfunc->winref = winref; + break; + } + } + if (lc == NULL) { /*未找到 window子句*/ + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("window \"%s\" does not exist", windef->name), /*窗口不存在*/ + parser_errposition(pstate, windef->location))); + } + } else { + Index winref = 0; + ListCell* lc = NULL; + foreach (lc, pstate->p_windowdefs) { + WindowDef* refwin = (WindowDef*)lfirst(lc); + winref++; + if (refwin->refname && windef->refname && strcmp(refwin->refname, windef->refname) == 0); /*匹配refname*/ + else if (!refwin->refname && !windef->refname); /*没有匹配到refname*/ + else + continue; + if (equal(refwin->partitionClause, windef->partitionClause) && + equal(refwin->orderClause, windef->orderClause) && refwin->frameOptions == windef->frameOptions && + equal(refwin->startOffset, windef->startOffset) && equal(refwin->endOffset, windef->endOffset)) { + /*发现重复的窗口规范*/ + wfunc->winref = winref; + break; + } + } + /*未找到 over子句属性*/ + if (lc == NULL) { + pstate->p_windowdefs = lappend(pstate->p_windowdefs, windef); + wfunc->winref = list_length(pstate->p_windowdefs); + } + } + pstate->p_hasWindowFuncs = true; + } + +transformAggregateCall与transformWindowFuncCall还包含一些函数或运算符的定义、语法以及一些使用时的注意事项,下面让我来为大家简单介绍一下: +(1)执行计划操作符Assert是一个物理运算符。Assert用于验证条件,例如,验证引用完整性或确保标量子查询返回一行。对于每个输入行,Assert运算符都要计算执行计划的Argument列中的表达式。如果此表达式的值为NULL,则通过Assert运算符传递该行,并且查询执行将继续。如果此表达式的值非空,则将产生相应的错误。 + +(2)lappend函数用于在列表(变量)后添加列表元素。lappend类似于append,区别在于lappend后面追加的是列表元素,而append后面添加的是字符串。 + + lappend varName ?value value value ...? + /* + *注意:是varname变量名(列表名),不是list + *追加的元素之间用空格隔开 + *如果varName不存在,会新建一个带value的list + */ + +(3)PostgreSQL Query Tree(如想了解请阅读https://www.jianshu.com/p/ef475048520c)。 + +(4)从表中查询数据时,可能会收到重复的行记录。为了删除这些重复行,可以在SELECT语句中使用DISTINCT子句。(DISTINCT子句的相关内容参考自https://www.yiibai.com/mysql/distinct.html) + + SELECT DISTINCT + columns + FROM + table_name + WHERE + where_conditions; + + 如果列具有NULL值,并且对该列使用DISTINCT子句,MySQL将保留一个NULL值,并删除其它的NULL值,因为DISTINCT子句将所有NULL值视为相同的值。 + 可以使用具有多个列的DISTINCT子句。 在这种情况下,MySQL使用所有列的组合来确定结果集中行的唯一性。 + 如果在SELECT语句中使用GROUP BY子句,而不使用聚合函数,则GROUP BY子句的行为与DISTINCT子句类似。一般而言,DISTINCT子句是GROUP BY子句的特殊情况。 DISTINCT子句和GROUP BY子句之间的区别是GROUP BY子句可对结果集进行排序,而DISTINCT子句不进行排序。如果将ORDER BY子句添加到使用DISTINCT子句的语句中,则结果集将被排序,并且与使用GROUP BY子句的语句返回的结果集相同。 + 可以使用具有聚合函数(例如SUM,AVG和COUNT)的DISTINCT子句中,在MySQL将聚合函数应用于结果集之前删除重复的行。 + 如果要将DISTINCT子句与LIMIT子句一起使用,MySQL会在查找LIMIT子句中指定的唯一行数时立即停止搜索。 + +(5)OrderedSet是一种可变的数据结构,它是列表和集合的混合体;它能记住条目的顺序;每个条目都有一个索引号;可以查到。(OrderedSet的相关内容参考自https://blog.csdn.net/M_arshal_/article/details/121229061) + set顾名思义是集合,里面不能包含重复的元素,接收一个list作为参数。 + 使用add(key)往集合中添加元素,重复的元素自动过滤。 + 通过remove(key)方法可以删除元素。 + set还可以像数学上那样求交集和并集。 + +(6)ORDER BY语句用于根据指定的列对结果集进行排序,默认按照升序对记录进行排序。如果希望按照降序对记录进行排序,可以使用DESC关键字。 + order by是用来写在where之后,给多个字段来排序的一个DQL查询语句。 + + select 字段列表/* from 表名 where 条件 order by 字段名1 asc/desc, 字段名2 asc/desc,....... + + select 字段列表/* from 表名 where 条件 order by 字段序号 asc/desc, 字段序号 asc/desc,....... (此时字段序号要从1开始) + + select 字段列表/* from 表名 where 条件 order by 字段别名1 asc/desc, 字段别名2 asc/desc,.......(这里类似于第一种,无非就是把字段名加了个别名来代替而已。) + +ORDER BY的方式: + asc 升序,可以省略,是数据库默认的排序方式。 + desc 降序,跟升序相反。 + +注意: + 要注意ORDER BY的原则,写在最前面的字段,他的优先级最高,也就是写法中第一个的字段名1的优先级最高,优先执行他的内容。 + ORDER BY返回的是游标而不是集合:SQL的理论其实是集合论,常见的类似求数据的交集、并集、差集都可以使用集合的思维来求解。集合中的行之间没有预先定义的顺序,它只是成员的一种逻辑组合,成员之间的顺序无关紧要。但是对于带有排序作用的ORDER BY子句的查询,它返回的是一个对象,其中的行按特定的顺序组织在一起,我们把这种对象称为游标。 + ORDER BY子句是唯一能重用列别名的一步。 + 谨慎使用ORDER BY 后面接数字的方式来进行排序:当查询的列发生改变,忘了修改ORDER BY列表。特别是当查询语句很长时,要找到ORDER BY与SELECT列表中的哪个列相对应会非常困难。请慎用ORDER BY加数字,尽量使用ORDER BY加列名或列别名。 + 表表达式不能使用ORDER BY排序。表表达式包括视图,内联表值函数,派生表(子查询)和公用表表达式(CTE)。 + TOP、OFFSET或FOR XML是可以使用ORDER BY的。 + +ORDER BY工作原理(如想了解更多,请点击https://zhuanlan.zhihu.com/p/380671457) + +(7)SQL语句的语法顺序和执行顺序(该部分内容参考自https://zhuanlan.zhihu.com/p/96733409) + 我们常见的SQL语法顺序如下: + + SELECT DISTINCT + (1)FROM [left_table] + (3) JOIN + (2) ON + (4)WHERE + (5)GROUP BY + (6)WITH + (7)HAVING + (10)ORDER BY + + 从上面可以看到SELECT在HAVING后才开始执行,这个时候SELECT后面列的别名只对后续的步骤生效,而对SELECT前面的步骤是无效的。所以如果你在WHERE,GROUP BY,或HAVING后面使用列的别名均会报错。 + +(8)SQL99标准(该部分内容参考自https://blog.csdn.net/qq_31929761/article/details/86976715) + + SQL99是什么? +(A)是操作所有关系型数据库的规则。 +(B)是第四代语言。 +(C)是一种结构化查询语言。 +(D)只需发出合法合理的命令,就有对应的结果显示。 + +SQL92/【99】标准的四大分类 : +(A)DML(数据操纵语言):select,insert,update,delete… +(B)DDL(数据定义语言):create table,alter table,drop table,truncate table… +(C)DCL(数据控制语言):grant 权限 to scott,revoke 权限 from scott… +(D)TCL(事务控制语言):commit,rollback,rollback to savepoint… + +SQL的特点: +(A)交互性强,非过程化。 +(B)数据库操纵能力强,只需发送命令,无需关注如何实现。 +(C)多表操作时,自动导航简单。 +(D)容易调试,错误提示,直接了当。 +(E)SQL强调结果。 + +(9)LATERAL(相关内容参考自https://www.cnblogs.com/ricklz/p/12122808.html以及https://blog.csdn.net/horses/article/details/86510905) + 我们先来看官方对lateral的定义:可以在出现于FROM中的子查询前放置关键词LATERAL。这允许它们引用前面的FROM项提供的列(如果没有LATERAL,每一个子查询将被独立计算,并且因此不能被其他FROM项交叉引用)。出现在FROM中的表函数的前面也可以被放上关键词LATERAL,但对于关键词的不可选的,在任何情况下函数的参数都可以包含前面FROM项提供额列的引用。一个LATERAL项可以出现在FROM列表项层,或者出现在一个JOIN树中。在后者如果出现在JOIN的右部, 那么可以引用在JOIN左部分的任何项。如果一个FROM项包含LATERAL交叉引用,计算过程中:对于提供交叉引用列的FROM项的每一行,或者 多个提供这些列的多个FROM项进行集合,LATERAL项将被使用该行或者行集中的列值进行计算。得到结 果行将和它们被计算出来的行进行正常的连接。对于来自这些列的源表的每一行或行集,该过程将重复。 + + 带有LATERAL的SQL的计算步骤:逐行提取被lateral子句关联(引用)的FROM或JOIN的ITEM(也叫source table)的记录(s)中的column(s);使用以上提取的columns(s),关联计算lateral子句中的ITEM;lateral的计算结果row(s),与所有from,join ITEM(S)正常的进行join计算;从1到3开始循环,直到所有的source table的行取尽。 + + LATERAL在OUTER JOIN中的使用限制(或定义限制):由于lateral的计算步骤是从source table逐条展开的,所以OUTER JOIN时只能使用source table 作为whole端,LATERAL内的ITEM不能作为WHOLE端。因此lateral只能在left join的右边或者right join的左边。因此不能是WHOLE端。 + + MySQL将FROM中的子查询称为派生表(Derived Table)。 + MySQL中的派生表存在一些限制: +(A)派生表不能是关联子查询。 +(B)派生表不能引用它所在的SELECT语句中的其他表。 +(C)在MySQL8.0.14之前,派生表不能引用它所在的SELECT语句外部的表。 + 简单来说,就是派生表必须能够单独运行,而不能依赖其他表。 + 从MySQL8.0.14 开始,派生表支持LATERAL关键字前缀,表示允许派生表引用它所在的FROM子句中的其他表。横向派生表能够完成普通派生表无法完成或者效率低下的操作。 + + MySQL从8.0.14 开始支持横向派生表,同时存在以下限制: +(A)横向派生表只能出现在FROM子句中,包括使用逗号分隔的表或者标准的连接语句(JOIN、INNER JOIN、CROSS JOIN、LEFT [OUTER] JOIN以及RIGHT [OUTER] JOIN)。 +(B)如果横向派生表位于连接操作的右侧,并且引用了左侧的表,连接类型必须为INNER JOIN、CROSS JOIN或者LEFT [OUTER] JOIN。 +(C)如果横向派生表位于连接操作的左侧,并且引用了右侧的表,连接类型必须为INNER JOIN、CROSS JOIN或者RIGHT [OUTER] JOIN。 +(D)如果横向派生表引用了聚合函数,那么该函数的聚合查询语句不能是横向派生表所在的FROM子句所属的查询语句。 +(E)根据SQL标准,表函数拥有一个隐式的LATERAL,这与MySQL8.0到MySQL8.0.14之前版本的实现一致。但是,根据标准,函数JSON_TABLE()之前不能存在LATERAL关键字,包括隐式的LATERAL。 + +总结: + (A)lateral 可以出现在FROM的列表项层,也可以出现在JOIN数树中,如果出现在JOIN的右部分,那么可以引用在JOIN左部分的任何项。 + (B)由于lateral的计算步骤是从source table逐条展开的,所以OUTER JOIN时只能使用source table 作为whole端,LATERAL内的ITEM不能作为WHOLE端。 + (C)LATERAL关键词可以在前缀一个SELECT FROM子项. 这能让SELECT子项在FROM项出现之前就引用到FROM项中的列(没有LATERAL的话, 每一个SELECT子项彼此都是独立的,因此不能够对其它的FROM 项进行交叉引用)。 + (D)当一个FROM 项包含LATERAL交叉引用的时候,查询的计算过程如下: 对于FROM项提供给交叉引用列的每一行,或者多个FROM像提供给引用列的行的集合,LATERAL 项都会使用行或者行的集合的列值来进行计算。计算出来的结果集像往常一样被加入到联合查询之中。这一过程会在列的来源表的行或者行的集合上重复进行。 + +(10)FROM子句指定SELECT语句查询及与查询相关的表或视图。在FROM子句中最多可指定256个表或视图,之间用逗号分隔。在FROM子句同时指定多个表或视图时,如果选择列表中存在同名列,这时应使用对象名限定这些列所属的表或视图。(相关内容参考自https://blog.csdn.net/weixin_42881768/article/details/104709257以及https://www.cnblogs.com/cjaaron/p/9204737.html) + +如何从表中查询一个字端的数据: + + select 字段名 from 表名; + +如何从表中查询多个字段的内容: + + select 字段名1 ,字段名2 from 表名; + +如何查询表中所有字段对应的值: + + /* + *标准写法:一个一个列出来 + *简单写法:*号可以代替所有的字段名(但是这样不知道里面有哪些字段) + */ + select * from 表名; + +字段的数学运算(SQL的数学运算): + + + - * / + +给字段起别名: + + select 字段名1 ,字段名2 别名 from 表名; + /* + *一个字段或者表达式只能有一个别名(也可以没有),别名会自动处理成大写 + *“别名”会原样显示所取的别名 + */ + +SQL中如何表达字符串: + + /* + *SQL中的字符串使用单引号表达(C里使用双引号表达字符串字面值) + * ‘a’ ‘hello world’ 都是字符串 + */ + select first_name from s_emp ; /*也是字符串*/ + +如何拼接字符串:字符串拼接符:| |(只针对oracle,其他的可以查一下) + +空值(NULL值)的处理: + 空值(NULL值)和任何值做运算结果都是NULL。 + 空值处理函数:nvl ( par1 , par2 )。可以处理任何类型的数据,但要求par1和par2的类型保持一致。这个函数,当par1为空时,返回par2的值;par1不为NULL时,返回par1的值。 + NULL要尽早处理。 + +数据的排重distinct:dstinct会把所有重复的数据剔除。联合排重(多字段排重,要两个字段都相同的才会剔除)。 + +(11)OID:按照GB/T 17969. 1(ISO/IEC 9834-1)的定义,对象是指“通信和信息处理世界中的任何事物,它是可标识(可以命名)的,同时它可被注册”。对象标识符(Object Identifier,OID)是与对象相关联的用来无歧义地标识对象的全局唯一的值,可保证对象在通信与信息处理中正确地定位和管理。通俗地讲,OID就是网络通信中对象的身份证。(来源于百度百科) + + 个人水平有限,感谢您的阅读。 diff --git "a/TestTasks/grozurem/parse_agg\350\247\243\350\257\273\357\274\210\344\272\224\357\274\211.md" "b/TestTasks/grozurem/parse_agg\350\247\243\350\257\273\357\274\210\344\272\224\357\274\211.md" new file mode 100644 index 0000000000000000000000000000000000000000..cdd9316a07cf985e497ea7dc87da892ed18c8187 --- /dev/null +++ "b/TestTasks/grozurem/parse_agg\350\247\243\350\257\273\357\274\210\344\272\224\357\274\211.md" @@ -0,0 +1,173 @@ +本篇博客主要讲解finalize_grouping_exprs与finalize_grouping_exprs_walker两个函数。 + +7.finalize_grouping_exprs扫描给定表达式树中的GROUPING()和相关调用,并验证和处理它们的参数。 + 这是从 check_ungrouped_columns 中分离出来的,因为它需要修改节点(适当的位置进行修改,而不是通过 mutator)。由于连接别名变量的扁平化, check_ungroupled_columns 可能只看到原始的一个副本。因此,我们在比较之前将每个单独的 GROUPING 参数展平。 + + static void finalize_grouping_exprs( + Node* node, ParseState* pstate, Query* qry, List* groupClauses, PlannerInfo* root, bool have_non_var_grouping) + { + check_ungrouped_columns_context context; + /*将给定表达式树中的数据传递给 context*/ + context.pstate = pstate; //当前解析状态 + context.qry = qry; //原始解析查询树 + context.root = root; //规划、优化信息根结点 + context.groupClauses = groupClauses; //group子句 + context.groupClauseCommonVars = NIL; + context.have_non_var_grouping = have_non_var_grouping; //判断是否含有未分组变量 + context.func_grouped_rels = NULL; + context.sublevels_up = 0; + context.in_agg_direct_args = false; //设定in_agg_direct_args的初始值为false + /* 调用 finalize_grouping_exprs_walkerGROUPING() 扫描给定表达式树中的 GROUPING() 和相关调用 */ + (void)finalize_grouping_exprs_walker(node, &context); + } + +8.finalize_grouping_exprs_walker扫描给定表达式树中的GROUPING()和相关调用。 + + static bool finalize_grouping_exprs_walker(Node* node, check_ungrouped_columns_context* context) + { + ListCell* gl = NULL; + /*如果节点为空,则返回 false*/ + if (node == NULL) { + return false; + } + /*如果节点类型为 Const 或者 Param,则返回 false*/ + if (IsA(node, Const) || IsA(node, Param)) { + return false; /*常数总是可以接受的*/ + } + /*如果节点类型为 Aggref*/ + if (IsA(node, Aggref)) { + Aggref* agg = (Aggref*)node; + /*如果找到与 context->sublevels_up 相同级别的聚合调用*/ + if ((int)agg->agglevelsup == context->sublevels_up) { + /* + *如果我们找到原始级别的聚合调用,不要递归到它的普通参数、ORDER BY 参数或filter过滤器中; + *此处不允许使用此级别的GROUPING表达式。但检查直接参数时,就当作它们未聚合 + */ + bool result = false; + AssertEreport(!context->in_agg_direct_args, MOD_OPT, ""); + context->in_agg_direct_args = true; + result = finalize_grouping_exprs_walker((Node*)agg->aggdirectargs, context); /*递归*/ + context->in_agg_direct_args = false; /*恢复设定的初始值 false,保证下一次调用正常*/ + return result; + } + /* + *我们也可以跳过更高级别聚合的参数,因为它们不可能包含我们关心的变量(参见transformAggregateCall)。 + *因此,我们只需要研究较低层次聚合的参数。 + */ + if ((int)agg->agglevelsup > context->sublevels_up) { + return false; + } + } + /*如果 node 节点类型为 GroupingFunc*/ + if (IsA(node, GroupingFunc)) { + GroupingFunc* grp = (GroupingFunc*)node; + /* 只需要在其所属的级别检查 GroupingFunc 节点,因为它们不能在参数中混合级别*/ + if ((int)grp->agglevelsup == context->sublevels_up) { + ListCell* lc = NULL; + List* ref_list = NIL; + foreach (lc, grp->args) { + Node* expr = (Node*)lfirst(lc); + Index ref = 0; + //如果根结点不为空,则展平表达式 + if (context->root != NULL) { + expr = flatten_join_alias_vars(context->root, expr); + } + /*每个表达式必须与当前查询级别的分组项匹配。与一般表达式不同,不允许函数依赖或外部引用。*/ + if (IsA(expr, Var)) { + Var* var = (Var*)expr; + /* 如果 var 与 context 查询级别相同 */ + if ((int)var->varlevelsup == context->sublevels_up) { + foreach (gl, context->groupClauses) { + TargetEntry* tle = (TargetEntry*)lfirst(gl); + Var* gvar = (Var*)tle->expr; + if (IsA(gvar, Var) && gvar->varno == var->varno && gvar->varattno == var->varattno && + gvar->varlevelsup == 0) { + ref = tle->ressortgroupref; + break; + } + } + } + } else if (context->have_non_var_grouping && context->sublevels_up == 0) { + foreach (gl, context->groupClauses) { + TargetEntry* tle = (TargetEntry*)lfirst(gl); + if (equal(expr, tle->expr)) { + ref = tle->ressortgroupref; + break; + } + } + } + /*GROUPING 的参数必须是关联查询级别的分组表达式*/ + if (ref == 0) { + ereport(ERROR, + (errcode(ERRCODE_GROUPING_ERROR), + errmsg("arguments to GROUPING must be grouping expressions of the associated query level"), + parser_errposition(context->pstate, exprLocation(expr)))); + } + ref_list = lappend_int(ref_list, ref); + } + grp->refs = ref_list; + } + /*如果高于当前查询级别,则返回false*/ + if ((int)grp->agglevelsup > context->sublevels_up) { + return false; + } + } + /*如果 node 节点为 Query 类型 */ + if (IsA(node, Query)) { + /*递归到子选择中*/ + bool result = false; + context->sublevels_up++; + result = query_tree_walker((Query*)node, (bool (*)())finalize_grouping_exprs_walker, (void*)context, 0); + context->sublevels_up--; + return result; + } + /*返回表达式查询树*/ + return expression_tree_walker(node, (bool (*)())finalize_grouping_exprs_walker, (void*)context); + } + +finalize_grouping_exprs与finalize_grouping_exprs_walker还包含一些函数或运算符的定义、语法以及一些使用时的注意事项,下面让我来为大家简单介绍一下: + +(1)accessor和mutator(accessor和mutator的相关内容参考自:https://zhidao.baidu.com/question/717134921821764125.html) + + 让某个变量只能通过公共方法来存取,这些变量就叫accessor或mutator。 + 比如:用户类有私有的成员变量密码key,由于key为私有private,所以我们无法直接在类外部直接使用或修改key,此时我们只能自定义成员函数getKey()和setKey()来实现获取和修改key。 + + #include + using namespace std; + class account{ + private: + string key; + public: + account(){ + key = "000000"; + } + ~account(){} + string getKey(){ + return key; + } + void setKey(string key){ + this->key = key; + } + }; + int main() + { + account wyf; + cout << wyf.getKey() << endl; + wyf.setKey("abcdef"); + cout << wyf.getKey() << endl; + } + + 这样在外部就根本不知道key在哪里存储,甚至key变量本身都可以不存在,对外只需要知道”key”这个名称,就能操作这个想象中的key变量。 + 此时”key”这个名称就成了一种accessor存取器或叫mutator变值器。 + accessor和mutator是C++类的封装特性的具体表现。 + +(2)flatten()函数。(flatten()的相关内容参考自:https://blog.csdn.net/kuan__/article/details/116987162) + flatten()是对多维数据的降维函数。 + flatten(),默认缺省参数为0,也就是说flatten()和flatte(0)效果一样。 + flatten(dim)表示,从第dim个维度开始展开,将后面的维度转化为一维。也就是说,只保留dim之前的维度,其他维度的数据全都挤在dim这一维。 + 比如一个数据的维度是(S0,S1,S2 … ,Sn ) (S0,S1,S2 … ,Sn)(S0,S1,S2 … ,Sn), flatten(m)后的数据为(S0,S1,S2 … ,Sm − 2,Sm − 1, Sm ∗ Sm + 1 ∗ Sm + 2 ∗ … ∗ Sn) (S0,S1,S2 … ,Sm - 2,Sm - 1,Sm * Sm + 1 * Sm + 2 * … * Sn)(S0,S1,S2 … ,Sm−2,Sm−1,Sm ∗ Sm + 1 ∗ Sm + 2 ∗ … ∗ Sn) + flatten只能适用于numpy对象,即array或者mat,普通的list列表不适用! + +(3)外部引用 outer reference。(out reference 的相关内容请参考:https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ms714316(v=vs.85) ) + + 个人水平有限,感谢您的阅读。 diff --git "a/TestTasks/grozurem/parse_agg\350\247\243\350\257\273\357\274\210\345\205\255\357\274\211.md" "b/TestTasks/grozurem/parse_agg\350\247\243\350\257\273\357\274\210\345\205\255\357\274\211.md" new file mode 100644 index 0000000000000000000000000000000000000000..235bd1b2de851f5ba0b82ed3b09a26382241684a --- /dev/null +++ "b/TestTasks/grozurem/parse_agg\350\247\243\350\257\273\357\274\210\345\205\255\357\274\211.md" @@ -0,0 +1,423 @@ +本篇博客主要讲解parse_agg.cpp中剩余的部分函数。 + + /* + *expand_groupingset_node 负责展开不同类型的节点 + *给定一个 GroupingSet 节点,将其展开并返回一个嵌套链表(储存链表的链表)。 + *对于 EMPTY 节点,返回一个储存空链表的链表。 + *对于 SIMPLE 节点,返回一个储存节点内容的链表。 + *对于 CUBE 和 ROLLUP 节点,返回拓展列表。 + *对于 SET 节点,递归拓展包含的 CUBE 和 ROLLUP 节点。 + */ + static List* expand_groupingset_node(GroupingSet* gs) + { + List* result = NIL; + /*检测节点类型*/ + switch (gs->kind) { + case GROUPING_SET_EMPTY: + result = list_make1(NIL); + break; + case GROUPING_SET_SIMPLE: + result = list_make1(gs->content); + break; + case GROUPING_SET_ROLLUP: { + List* rollup_val = gs->content; + ListCell* lc = NULL; + int curgroup_size = list_length(gs->content); + while (curgroup_size > 0) { + List* current_result = NIL; + int i = curgroup_size; + /*遍历储存节点内容的链表*/ + foreach (lc, rollup_val) { + GroupingSet* gs_current = (GroupingSet*)lfirst(lc); + /*种类出乎意料*/ + AssertEreport(gs_current->kind == GROUPING_SET_SIMPLE, MOD_OPT, "Kind is unexpected"); + /*将当前链表与储存节点内容的链表连接*/ + current_result = list_concat(current_result, list_copy(gs_current->content)); + /*如果完成了当前组的创建,结束循环*/ + if (--i == 0) { + break; + } + } + /*将连接后获得的链表拼接到 result 链表*/ + result = lappend(result, current_result); + --curgroup_size; + } + result = lappend(result, NIL); + } break; + case GROUPING_SET_CUBE: { + List* cube_list = gs->content; + int number_bits = list_length(cube_list); + uint32 num_sets; + uint32 i; + /*解析器的上限应该低得多*/ + AssertEreport(number_bits < 31, MOD_OPT, "parser should cap this much lower"); + num_sets = (1U << (unsigned int)number_bits); + for (i = 0; i < num_sets; i++) { + List* current_result = NIL; + ListCell* lc = NULL; + uint32 mask = 1U; + foreach (lc, cube_list) { + GroupingSet* gs_current = (GroupingSet*)lfirst(lc); + /*种类出乎意料*/ + AssertEreport(gs_current->kind == GROUPING_SET_SIMPLE, MOD_OPT, "Kind is unexpected"); + if (mask & i) { + current_result = list_concat(current_result, list_copy(gs_current->content)); + } + //mask扩大为原来的二倍 + mask <<= 1; + } + /*将所得链表拼接到 result 链表*/ + result = lappend(result, current_result); + } + } break; + case GROUPING_SET_SETS: { + ListCell* lc = NULL; + foreach (lc, gs->content) { + /*使用递归拓展包含的 CUBE 和 ROLLUP 节点*/ + List* current_result = expand_groupingset_node((GroupingSet*)lfirst(lc)); + result = list_concat(result, current_result); + } + } break; + default: + break; + } + return result; + } + /* 比较两个链表长度是否相等 */ + static int cmp_list_len_asc(const void* a, const void* b) + { + int la = list_length(*(List* const*)a); + int lb = list_length(*(List* const*)b); + return (la > lb) ? 1 : (la == lb) ? 0 : -1; + } + /* + *为聚合的转换和最终函数创建表达式树。 + *这些都是必需的,这样多态函数就可以在聚合中使用——如果没有表达式树,这些函数就不会知道它们应该使用的数据类型。 + *(然而,这些树永远不会被执行,所以我们可以略过一些正确性。) + * + *agg_input_types, agg_state_type,和agg_result_type标识聚合的输入、转换和结果类型。 + *这些都应该解析为实际类型(例如,任何类型都不应该是ANYELEMENT等) + *agginput_collation是聚合函数的输入排序规则。 + * + *transfnoid和finalfnoid标识要调用的函数;后者可能是InvalidOid。 + * + *指向构造树的指针将返回到*transnexpr和*finalfnexpr。如果没有finalf,则后者设置为NULL + */ + void build_aggregate_fnexprs(Oid* agg_input_types, int agg_num_inputs, Oid agg_state_type, Oid agg_result_type, + Oid agg_input_collation, Oid transfn_oid, Oid finalfn_oid, Expr** transfnexpr, Expr** finalfnexpr) + { + Param* argp = NULL; + List* args = NIL; + int i; + /* + *构建要在transfn FuncExpr节点中使用的参数列表。 + *我们只关心transfn可以在运行时使用get_fn_expr_argtype()发现实际的参数类型,所以可以使用与任何实际Param都不对应的Param节点。 + */ + argp = makeNode(Param); + argp->paramkind = PARAM_EXEC;/*运行时发现的实际参数类型*/ + argp->paramid = -1; + argp->paramtype = agg_state_type; /*标识聚合的转换类型*/ + argp->paramtypmod = -1; + argp->paramcollid = agg_input_collation; /*聚合输入的排序规则*/ + argp->location = -1; + args = list_make1(argp); + for (i = 0; i < agg_num_inputs; i++) { + argp = makeNode(Param); + argp->paramkind = PARAM_EXEC; + argp->paramid = -1; + argp->paramtype = agg_input_types[i];/*标识聚合的输入类型*/ + argp->paramtypmod = -1; + argp->paramcollid = agg_input_collation; + argp->location = -1; + args = lappend(args, argp); + } + *transfnexpr = + (Expr*)makeFuncExpr(transfn_oid, agg_state_type, args, InvalidOid, agg_input_collation, COERCE_DONTCARE); + /*如果没有最后一个函数,将*finalfnexpr设置为NULL*/ + if (!OidIsValid(finalfn_oid)) { + *finalfnexpr = NULL; + return; + } + /*为最终函数构建表达式树*/ + argp = makeNode(Param); + argp->paramkind = PARAM_EXEC; + argp->paramid = -1; + argp->paramtype = agg_state_type; + argp->paramtypmod = -1; + argp->paramcollid = agg_input_collation; + argp->location = -1; + args = list_make1(argp); + *finalfnexpr = + (Expr*)makeFuncExpr(finalfn_oid, agg_result_type, args, InvalidOid, agg_input_collation, COERCE_DONTCARE); + } + /* + *为聚合的转换和最终函数创建表达式树。 + *这些都是必需的,这样多态函数就可以在聚合中使用——如果没有表达式树,这些函数就不会知道它们应该使用的数据类型。 + *(然而,这些树永远不会被执行,所以我们可以略过一些正确性。) + * + *agg_input_types, agg_state_type和agg_result_type标识聚合的输入、转换和结果类型。 + *这些都应该解析为实际类型(例如,任何类型都不应该是ANYELEMENT等)。 + *agginput_collation是聚合函数的输入排序规则。 + * + *对于有序集聚合,请记住agg_input_types描述了聚合参数后面的直接参数。 + * + *transfnoid和finalfnoid标识要调用的函数;后者可能是InvalidOid + * + *指向构造树的指针将返回到*transnexpr和*finalfnexpr。如果没有finalfn,则后者设置为NULL。 + */ + void build_trans_aggregate_fnexprs(int agg_num_inputs, int agg_num_direct_inputs, bool agg_ordered_set, + bool agg_variadic, Oid agg_state_type, Oid* agg_input_types, Oid agg_result_type, Oid agg_input_collation, + Oid transfn_oid, Oid finalfn_oid, Expr** transfnexpr, Expr** finalfnexpr) + { + Param* argp = NULL; + List* args = NULL; + FuncExpr* fexpr = NULL; + int i; + /* + *构建要在transfn FuncExpr节点中使用的参数列表。 + *我们只关心transfn可以在运行时使用get_fn_expr_argtype()发现实际的参数类型,所以可以使用与任何实际Param都不对应的Param节点。 + */ + argp = makeParam(PARAM_EXEC, -1, agg_state_type, -1, agg_input_collation, -1); + args = list_make1(argp); + for (i = agg_num_direct_inputs; i < agg_num_inputs; i++) { + argp = makeParam(PARAM_EXEC, -1, agg_input_types[i], -1, agg_input_collation, -1); + args = lappend(args, argp); + } + fexpr = makeFuncExpr(transfn_oid, agg_state_type, args, InvalidOid, agg_input_collation, COERCE_EXPLICIT_CALL); + fexpr->funcvariadic = agg_variadic; + *transfnexpr = (Expr*)fexpr; + /*如果没有最后一个函数,将*finalfnexpr设置为NULL*/ + if (!OidIsValid(finalfn_oid)) { + *finalfnexpr = NULL; + return; + } + /*为最终函数构建表达式树*/ + argp = makeParam(PARAM_EXEC, -1, agg_state_type, -1, agg_input_collation, -1); + args = list_make1(argp); + if (agg_ordered_set) { + for (i = 0; i < agg_num_inputs; i++) { + argp = makeParam(PARAM_EXEC, -1, agg_input_types[i], -1, agg_input_collation, -1); + args = lappend(args, argp); + } + } + *finalfnexpr = + (Expr*)makeFuncExpr(finalfn_oid, agg_result_type, args, InvalidOid, agg_input_collation, COERCE_EXPLICIT_CALL); + /*finalfn目前从未被视为变量*/ + } + /* + *将groupingSets子句展开为分组集的展平链表。 + *返回的链表按长度(从短到长)排序。 + * + *这主要是为planner准备的,但在这里也使用它来做一些一致性检查。 + */ + List* expand_grouping_sets(List* groupingSets, int limit) + { + List* expanded_groups = NIL; + List* result = NIL; + double numsets = 1; + ListCell* lc = NULL; + if (groupingSets == NIL) { + return NIL; + } + foreach (lc, groupingSets) { + List* current_result = NIL; + GroupingSet* gs = (GroupingSet*)lfirst(lc); + current_result = expand_groupingset_node(gs); + /*此处para不应为NULL*/ + AssertEreport(current_result != NIL, MOD_OPT, "para should not be NULL here"); + numsets *= list_length(current_result); + if (limit >= 0 && numsets > limit) { + return NIL; + } + expanded_groups = lappend(expanded_groups, current_result); + } + /* + *在expanded_groups的子列表之间进行笛卡尔积。 + *同时,从单个分组集中删除任何重复的元素(但我们不能更改集合的数量) + */ + foreach (lc, (List*)linitial(expanded_groups)) { + result = lappend(result, list_union_int(NIL, (List*)lfirst(lc))); + } + for_each_cell(lc, lnext(list_head(expanded_groups))) + { + List* p = (List*)lfirst(lc); + List* new_result = NIL; + ListCell* lc2 = NULL; + foreach (lc2, result) { + List* q = (List*)lfirst(lc2); + ListCell* lc3 = NULL; + foreach (lc3, p) { + new_result = lappend(new_result, list_union_int(q, (List*)lfirst(lc3))); + } + } + result = new_result; + } + if (list_length(result) > 1) { + int result_len = list_length(result); + List** buf = (List**)palloc(sizeof(List*) * result_len); + List** ptr = buf; + foreach (lc, result) { + *ptr++ = (List*)lfirst(lc); + } + qsort(buf, result_len, sizeof(List*), cmp_list_len_asc); + result = NIL; + ptr = buf; + while (result_len-- > 0) + result = lappend(result, *ptr++); + pfree_ext(buf); + } + return result; + } + /* + *transformGroupingFunc转换GROUPING表达式 + * + *GROUPING()的行为非常类似于聚合。 + *levels和nesting的处理与aggregates相同。我们也为这些表达式设置了p_hasAggs。 + */ + Node* transformGroupingFunc(ParseState* pstate, GroupingFunc* p) + { + ListCell* lc = NULL; + List* args = p->args; + List* result_list = NIL; + bool orig_is_replace = false; + GroupingFunc* result = makeNode(GroupingFunc); + if (list_length(args) > 31) { + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ARGUMENTS), + /*GROUPING的参数必须少于32个*/ + errmsg("GROUPING must have fewer than 32 arguments"), + parser_errposition(pstate, p->location))); + } + orig_is_replace = pstate->isAliasReplace; + /*分组不支持Alias Replace。*/ + pstate->isAliasReplace = false; + foreach (lc, args) { + Node* current_result = NULL; + current_result = transformExpr(pstate, (Node*)lfirst(lc)); + /*稍后检查表达式的可接受性*/ + result_list = lappend(result_list, current_result); + } + pstate->isAliasReplace = orig_is_replace; + result->args = result_list; + result->location = p->location; + pstate->p_hasAggs = true; + return (Node*)result; + } + /* + *check_windowagg_can_shuffle检查windowagg是否可以打乱次序 + */ + bool check_windowagg_can_shuffle(List* partitionClause, List* targetList) + { + if (partitionClause == NIL) { + return true; + } + ListCell* l = NULL; + foreach (l, partitionClause) { + SortGroupClause* grpcl = (SortGroupClause*)lfirst(l); + TargetEntry* expr = get_sortgroupclause_tle(grpcl, targetList, false); + if (expr == NULL) { + continue; + } + if (checkExprHasAggs((Node*)expr->expr)) { + return false; + } + } + return true; + } + /* + *获取聚合参数类型 + *获取传递给聚合调用的实际数据类型,并返回实际参数的数量。 + * + *给定一个Aggref,提取输入参数的实际数据类型。 + *对于有序集agg,Aggref包含直接参数和聚合参数,并且直接参数保存在聚合参数之前。 + * + *数据类型加载到inputTypes[]中,它必须引用长度为FUNC_MAX_ARGS的数组。 + */ + int get_aggregate_argtypes(Aggref* aggref, Oid* inputTypes, int func_max_args) + { + int narg = 0; + ListCell* lc = NULL; + /* + *如果是有序的set agg,aggref->aggdirectargs不为空。 + *因此,我们需要首先处理直接参数。 + */ + foreach (lc, aggref->aggdirectargs) { + inputTypes[narg] = exprType((Node*)lfirst(lc)); + narg++; + if (narg >= func_max_args) { + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + /*函数最多可以有%d个参数*/ + errmsg("functions can have at most %d parameters", func_max_args))); + } + } + /* + *然后获取由普通agg和有序集agg包含的聚合参数。 + */ + foreach (lc, aggref->args) { + TargetEntry* tle = (TargetEntry*)lfirst(lc); + /*忽略普通聚合的排序列*/ + if (tle->resjunk) { + continue; + } + inputTypes[narg] = exprType((Node*)tle->expr); + narg++; + if (narg >= func_max_args) { + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + /*函数最多可以有%d个参数*/ + errmsg("functions can have at most %d parameters", func_max_args))); + } + } + /*narg的值不大于func_max_args*/ + return narg; + } + /* + *resolve_aggregate_transtype解决聚集转换类型 + *当agg接受ANY或多态类型时,标识聚合调用的转换状态值的数据类型。 + * + *此函数用于解析多态聚合的状态数据类型。 + *aggtranstype通过搜索pg_aggregate目录以及get_aggregate_argtypes提取的实际参数类型来传递。 + */ + Oid resolve_aggregate_transtype(Oid aggfuncid, Oid aggtranstype, Oid* inputTypes, int numArguments) + { + /*仅在转换状态为多态时解析实际类型*/ + if (IsPolymorphicType(aggtranstype)) { + Oid* declaredArgTypes = NULL; + int agg_nargs = 0; + /*获取agg函数的参数和结果类型*/ + (void)get_func_signature(aggfuncid, &declaredArgTypes, &agg_nargs); + Assert(agg_nargs <= numArguments); + aggtranstype = enforce_generic_type_consistency(inputTypes, declaredArgTypes, agg_nargs, aggtranstype, false); + pfree(declaredArgTypes); + } + return aggtranstype; + } + /*如果宏定义启动了多节点,该函数生效*/ + /*寻找group by 子句的行数*/ + #ifndef ENABLE_MULTIPLE_NODES + static void find_rownum_in_groupby_clauses(Rownum *rownumVar, check_ungrouped_columns_context *context) + { + bool haveRownum = false; + ListCell *gl = NULL; + /*如果没有未分组参数并且聚合级别不为0*/ + if (!context->have_non_var_grouping || context->sublevels_up != 0) { + foreach (gl, context->groupClauses) { + Node *gnode = (Node *)((TargetEntry *)lfirst(gl))->expr; + if (IsA(gnode, Rownum)) { + haveRownum = true; + break; + } + } + /*如果没有行数信息,则报告错误信息*/ + if (haveRownum == false) { + ereport(ERROR, (errcode(ERRCODE_GROUPING_ERROR), + /*ROWNUM必须出现在GROUP BY子句中或在聚合函数中使用*/ + errmsg("ROWNUM must appear in the GROUP BY clause or be used in an aggregate function"), + parser_errposition(context->pstate, rownumVar->location))); + } + } + } + #endif + + 个人水平有限,感谢您的阅读。 diff --git "a/TestTasks/grozurem/parse_agg\350\247\243\350\257\273\357\274\210\345\233\233\357\274\211.md" "b/TestTasks/grozurem/parse_agg\350\247\243\350\257\273\357\274\210\345\233\233\357\274\211.md" new file mode 100644 index 0000000000000000000000000000000000000000..74d357817ff3ad187ce36e0f08cd45b1a0c2b71a --- /dev/null +++ "b/TestTasks/grozurem/parse_agg\350\247\243\350\257\273\357\274\210\345\233\233\357\274\211.md" @@ -0,0 +1,312 @@ +本篇博客主要讲解check_ungrouped_columns与check_ungrouped_columns_walker两个函数。 +5.check_ungrouped_columns扫描给定表达式树中的未分组变量(未列在Group子句列表中且不在聚合函数的参数内的变量),如果发现任何错误,则报告错误信息。我们假设给定的子句已被适当地转换为解析器输出,这意味着可以使用check_ungrouped_columns_walker(本篇博客后续会讲解该函数)。 + 此函数在主查询中识别分组表达式,但在子查询中仅识别分组变量。例如,这种方式通常不予考虑,但可以允许: + + SELECT + (SELECT x FROM bar where y = (foo.a + foo.b)) + FROM foo + GROUP BY a + b; + + 困难在于需要考虑不同的sublevels_up。这似乎需要完全自定义equal(),这将更难实现。 + + static void check_ungrouped_columns(Node* node, ParseState* pstate, Query* qry, List* groupClauses, + List* groupClauseCommonVars, bool have_non_var_grouping, List** func_grouped_rels) + { + check_ungrouped_columns_context context; + /*将给定表达式树中的数据传递给 context*/ + context.pstate = pstate; //当前解析状态 + context.qry = qry; //原始解析查询树 + context.root = NULL; //规划、优化信息根结点 + context.groupClauses = groupClauses; //group子句 + context.groupClauseCommonVars = groupClauseCommonVars; + context.have_non_var_grouping = have_non_var_grouping; //判断是否含有未分组变量 + context.func_grouped_rels = func_grouped_rels; + context.sublevels_up = 0; + context.in_agg_direct_args = false; //设定in_agg_direct_args的初始值为false + /*调用 check_ungrouped_columns_walker() 扫描给定表达式树中的未分组变量*/ + (void)check_ungrouped_columns_walker(node, &context); + } + +6.check_ungrouped_columns_walker扫描给定表达式树中的未分组变量 + + static bool check_ungrouped_columns_walker(Node* node, check_ungrouped_columns_context* context) + { + ListCell* gl = NULL; + /*如果节点为空,则返回 false*/ + if (node == NULL) { + return false; + } + /*如果节点类型为 Const 或者 Param,则返回 false*/ + if (IsA(node, Const) || IsA(node, Param)) { + return false; /*常数总是可以接受的*/ + } + /*如果节点类型为 Aggref*/ + if (IsA(node, Aggref)) { + Aggref* agg = (Aggref*)node; + /*如果找到与 context->sublevels_up 相同级别的聚合调用*/ + if ((int)agg->agglevelsup == context->sublevels_up) { + /*对于有序集合agg,其直接参数不应位于聚合内。 + *如果我们找到原始级别的聚合调用(如果它在外部查询中,context应该是相同的) + *不要递归到它的普通参数、ORDER BY参数或filter过滤器中;未分组变量没有错误。 + *我们在上下文中使用in_agg_direct_args来帮助为直接参数中的未分组变量生成有用的错误消息。 + */ + bool result = false; /*标记返回值为 false*/ + /*如果有序集合 agg 内含有直接参数,则报告错误信息*/ + if (context->in_agg_direct_args) { + ereport(ERROR, (errcode(ERRCODE_INVALID_AGG), errmsg("unexpected args inside agg direct args"))); + } + context->in_agg_direct_args = true; + result = check_ungrouped_columns_walker((Node*)agg->aggdirectargs, context); /*递归*/ + context->in_agg_direct_args = false; /*恢复设定的初始值 false,保证下一次调用正常*/ + return result; + } + /* + *我们也可以跳过更高级别聚合的参数,因为它们不可能包含我们关心的变量(参见transformAggregateCall)。 + *因此,我们只需要研究较低层次聚合的参数。 + */ + if ((int)agg->agglevelsup > context->sublevels_up) { + return false; + } + } + /*如果 node 节点类型为 GroupingFunc*/ + if (IsA(node, GroupingFunc)) { + GroupingFunc* grp = (GroupingFunc*)node; + /*单独处理 GroupingFunc,此级别无需重新检查*/ + if ((int)grp->agglevelsup >= context->sublevels_up) { + return false; + } + } + /*如果我们有任何不是简单变量的 GROUP BY 项,请检查子表达式作为一个整体是否匹配任何GROUP BY项。 + *我们需要在每个递归级别执行此操作,以便在到达 GROUPed-BY 表达式之前识别它们。 + *但是这仅适用于外部查询级别。 + */ + if (context->have_non_var_grouping && context->sublevels_up == 0) { + foreach (gl, context->groupClauses) { + TargetEntry* tle = (TargetEntry*)lfirst(gl); + //如果子表达式与 node 匹配,返回 false,此时已完成,无需继续循环 + if (equal(node, tle->expr)) { + return false; + } + } + } + #ifndef ENABLE_MULTIPLE_NODES + /*如果存在 ROWNUM,则它必须出现在 GROUP BY 子句中或用于聚合函数*/ + if (IsA(node, Rownum)) { + find_rownum_in_groupby_clauses((Rownum *)node, context); + } + #endif + /*如果有原始查询级别的未分组变量,则出现问题(来自原始查询级别以下或以上的变量无关紧要)。*/ + /*如果 node 节点类型为Var*/ + if (IsA(node, Var)) { + Var* var = (Var*)node; + RangeTblEntry* rte = NULL; + char* attname = NULL; + if (var->varlevelsup != (unsigned int)context->sublevels_up) { + return false; /*如果不是原始查询级别则没有问题*/ + } + /* 如果没有进行上面的匹配,执行以下代码*/ + if (!context->have_non_var_grouping || context->sublevels_up != 0) { + foreach (gl, context->groupClauses) { + Var* gvar = (Var*)((TargetEntry*)lfirst(gl))->expr; + if (IsA(gvar, Var) && gvar->varno == var->varno && gvar->varattno == var->varattno && + gvar->varlevelsup == 0) + return false; + } + } + /*检查变量是否在功能上依赖于 GROUP BY 列。 + *如果是这样的话,我们可以允许使用Var,因为对于这个表来说,分组实际上是一个空操作。 + *但是,这种推断取决于表的一个或多个约束,因此我们必须将这些约束添加到查询的 constraintDeps 列表中。 + *因为如果删除了约束,它在语义上不再有效。 + *因此,在引发错误之前,这将是最后一次检查,如果不成功,还需要添加依赖项 + * + *因为这是一个非常耗时的检查,并且对于表的所有列都会有相同的结果,所以在 func_grouped_rels 列表中证明了一些RTE的依赖关系。 + *此测试还防止我们向constraintDeps列表中添加重复条目。*/ + if (list_member_int(*context->func_grouped_rels, var->varno)) { + return false; + } + /*断言*/ + AssertEreport( + var->varno > 0 && (int)var->varno <= list_length(context->pstate->p_rtable), MOD_OPT, "Var is unexpected"); + rte = rt_fetch(var->varno, context->pstate->p_rtable); + if (rte->rtekind == RTE_RELATION) { + if (check_functional_grouping( + rte->relid, var->varno, 0, context->groupClauseCommonVars, &context->qry->constraintDeps)) { + *context->func_grouped_rels = lappend_int(*context->func_grouped_rels, var->varno); + return false; + } + } + /*如果找到未分组的局部变量,生成错误信息*/ + attname = get_rte_attribute_name(rte, var->varattno); + /*如果RTE被重写,则修改attname*/ + char* orig_attname = attname; //拷贝一份原始的attname + //如果RTE被重写 + if (IsSWCBRewriteRTE(rte)) { + attname = strrchr(attname, '@'); //在 attname 所指向的字符串中搜索最后一次出现字符 @ 的位置 + attname = (attname != NULL) ? (attname + 1) : orig_attname;//如果不为空则向后移动1个字符,否则恢复原来的attname + } + /*错误信息报告*/ + if (context->sublevels_up == 0) { + ereport(ERROR, + (errcode(ERRCODE_GROUPING_ERROR), + errmsg("column \"%s.%s\" must appear in the GROUP BY clause or be used in an aggregate function", + rte->eref->aliasname, + attname), + context->in_agg_direct_args + ? errdetail("Direct arguments of an ordered-set aggregate must use only grouped columns.") + : 0, + rte->swConverted ? errdetail("Please check your start with rewrite table's column.") : 0, + parser_errposition(context->pstate, var->location))); + } else { + ereport(ERROR, + (errcode(ERRCODE_GROUPING_ERROR), + errmsg("subquery uses ungrouped column \"%s.%s\" from outer query", rte->eref->aliasname, attname), + parser_errposition(context->pstate, var->location))); + } + if (attname != NULL) { + pfree_ext(attname); + } + } + /*如果 node 节点类型为 Query*/ + if (IsA(node, Query)) { + /*递归到子选择中*/ + bool result = false; + context->sublevels_up++; + result = query_tree_walker((Query*)node, (bool (*)())check_ungrouped_columns_walker, (void*)context, 0); + context->sublevels_up--; + return result; + } + /*返回表达式树*/ + return expression_tree_walker(node, (bool (*)())check_ungrouped_columns_walker, (void*)context); + } + +check_ungrouped_columns与check_ungrouped_columns_walker还包含一些函数或运算符的定义、语法以及一些使用时的注意事项,下面让我来为大家简单介绍一下: + +(1)约束。SQL 约束用于规定表中的数据规则。如果存在违反约束的数据行为,行为会被约束终止。(约束的相关内容参考自:https://www.runoob.com/sql/sql-constraints.html) + SQL CREATE TABLE + CONSTRAINT 语法: + + CREATE TABLE table_name + ( + column_name1 data_type(size) constraint_name, + column_name2 data_type(size) constraint_name, + column_name3 data_type(size) constraint_name, + .... + ); + +在 SQL 中,我们有如下约束: + + NOT NULL - 指示某列不能存储 NULL 值。 + NOT NULL 约束强制字段始终包含值。这意味着,如果不向字段添加值,就无法插入新记录或者更新记录。 + + UNIQUE - 保证某列的每行必须有唯一的值。 + + PRIMARY KEY - NOT NULL 和 UNIQUE 的结合。确保某列(或两个列多个列的结合)有唯一标识,有助于更容易更快速地找到表中的一个特定的记录。 + UNIQUE 和 PRIMARY KEY 约束均为列或列集合提供了唯一性的保证。 + PRIMARY KEY 约束拥有自动定义的 UNIQUE 约束。 + 请注意,每个表可以有多个 UNIQUE 约束,但是每个表只能有一个 PRIMARY KEY 约束。 + PRIMARY KEY 约束唯一标识数据库表中的每条记录。 + 主键必须包含唯一的值。 + 主键列不能包含 NULL 值。 + 每个表都应该有一个主键,并且每个表只能有一个主键。 + + FOREIGN KEY - 保证一个表中的数据匹配另一个表中的值的参照完整性。 + + CHECK - 保证列中的值符合指定的条件。 + 如果对单个列定义 CHECK 约束,那么该列只允许特定的值。 + 如果对一个表定义 CHECK 约束,那么此约束会基于行中其他列的值在特定的列中对值进行限制。 + + DEFAULT - 规定没有给列赋值时的默认值。 + 如果没有规定其他的值,那么会将默认值添加到所有的新记录。 + +约束可以在创建表时规定(通过 CREATE TABLE 语句),或者在表创建之后规定(通过 ALTER TABLE 语句)。 + +(2)依赖项就是设定项目所依赖的项目,以决定具体生成解决方案时项目编译的顺序(一般一个解决方案会有很多项目组成)。通常来说,依赖项取决于这个项目引用的组件和项目,系统可以自己决定。 + +(3)依赖关系(依赖关系的相关内容参考自:https://www.cnblogs.com/shiyh/p/12036296.html和https://blog.csdn.net/Findyoulucky/article/details/106589517) + +数据依赖 +数据依赖指的是通过一个关系中属性间的相等与否体现出来的数据间的相互关系,其中最重要的是函数依赖和多值依赖。 + +函数依赖 +设R(U)是一个属性集U上的关系模式,X和Y是U的子集。 +若对于R(U)的任意一个可能的关系r,r中不可能存在两个元组在X上的属性值相等, 而在Y上的属性值不等, 则称 “X函数确定Y” 或 “Y函数依赖于X”,记作X→Y。 +X称为这个函数依赖的决定属性集(Determinant)。 +Y=f(x) + + 平凡函数依赖 + 当关系中属性集合Y是属性集合X的子集时(Y?X),存在函数依赖X→Y,即一组属性函数决定它的所有子集,这种函数依赖称为平凡函数依赖。 + + 非平凡函数依赖 + 当关系中属性集合Y不是属性集合X的子集时,存在函数依赖X→Y,则称这种函数依赖为非平凡函数依赖。 + + 完全函数依赖 + 设X,Y是关系R的两个属性集合,X’是X的真子集,存在X→Y,但对每一个X’都有X’!→Y,则称Y完全函数依赖于X。 + + 部分函数依赖 + 设X,Y是关系R的两个属性集合,存在X→Y,若X’是X的真子集,存在X’→Y,则称Y部分函数依赖于X。 + + 传递函数依赖 + 设X,Y,Z是关系R中互不相同的属性集合,存在X→Y(Y !→X),Y→Z,则称Z传递函数依赖于X。 + +说明: + 函数依赖不是指关系模式R的某个或某些关系实例满足的约束条件,而是指R的所有关系实例均要满足的约束条件。 + 函数依赖是语义范畴的概念。只能根据数据的语义来确定函数依赖。 +例如“姓名→年龄”这个函数依赖只有在不允许有同名人的条件下成立 + 数据库设计者可以对现实世界作强制的规定。例如规定不允许同名人出现,函数依赖“姓名→年龄”成立。所插入的元组必须满足规定的函数依赖,若发现有同名人存在, 则拒绝装入该元组。 + 例: Student(Sno, Sname, Ssex, Sage, Sdept) + 假设不允许重名,则有: + Sno → Ssex, Sno → Sage , Sno → Sdept, + Sno ←→ Sname, Sname → Ssex, Sname → Sage + Sname → Sdept + 但Ssex -\→Sage + 若X→Y,并且Y→X, 则记为X←→Y。 + 若Y不函数依赖于X, 则记为X-\→Y。 + 在关系模式R(U)中,对于U的子集X和Y, + 如果X→Y,但Y 不为 X的子集,则称X→Y是非平凡的函数依赖 + 若X→Y,但Y 为 X的子集, 则称X→Y是平凡的函数依赖 + 例:在关系SC(Sno, Cno, Grade)中, + 非平凡函数依赖: (Sno, Cno) → Grade + 平凡函数依赖: (Sno, Cno) → Sno + (Sno, Cno) → Cno + 部分函数依赖: 若x->y 并且,存在X的真子集x1,使得x1->y,则 y部分依赖于 x。 + 完全函数依赖:若x->y并且,对于x的任何一个真子集x1,都不存在x1->y 则称y完全依赖于x。 + +用函数依赖定义key: + + 超键(super key):在关系中能唯一标识bai元组du的属性集称为关系模式zhi的超键 + + 候选键(candidate key):不含有多dao余属性的超键称为候选键 + + 主键(primary key):用户选作元组标识的一个候选键程序主键 + 比如一个小范围的所有人,没有重名的,考虑以下属性:身份证 姓名 性别 年龄 + 身份证唯一,所以是一个超键; + 姓名唯一,所以是一个超键; + (姓名,性别)唯一,所以是一个超键; + (姓名,性别,年龄)唯一,所以是一个超键; + 这里可以看出,超键的组合是唯一的,但可能不是最小唯一的; + 身份证唯一,而且没有多余属性,所以是一个候选键; + 姓名唯一,而且没有多余属性,所以是一个候选键 + 这里可以看出,候选键是没有多余属性的超键 + 一个关系的所有候选键中选择一个用来唯一确定关系的元组,这个候选键成为主键 + + 如何用函数依赖来定义key呢? + 如果K是superkey,当且仅当K ——> R成立。【注意】K ——>R中R表示关系模式,可以用属性集合来表示,即R={A1,A2,….,An} + candidate key:如果K是candidate key,当且仅当K ——> R 成立而且不存在α包含于 K,且 α ——> R成立(不存在K的一个子集α且α ——> R 成立) + + 闭包:给定一个函数依赖集合F,能够从F逻辑推断的的所有函数依赖的集合称作F的闭包,记做F+ + +总结: + 函数依赖可以表达无法用superkey表达的约束。 + 函数依赖是精化和建立“好”关系模式的形式化方法,可以解决数据冗余问题、修改异常问题、Null问题。 + +(4)RTE, 即“运行时环境( Runtime Environment )”的缩写。指的是软件程序被执行时,该程序处于的状态。在这种状态下,程序可以将指令发送到计算机的处理器,并访问计算机的内存(RAM)和其他系统资源。(RTE的相关内容参考自:https://zhuanlan.zhihu.com/p/94648324) + +(5)strrchr()(strrchr()的相关内容参考自:https://www.runoob.com/cprogramming/c-function-strrchr.html) + C 库函数 char *strrchr(const char *str, int c) 在参数 str 所指向的字符串中搜索最后一次出现字符 c(一个无符号字符)的位置。 + + char *strrchr(const char *str, int c) + + 参数:str – C 字符串。c – 要搜索的字符。以 int 形式传递,但是最终会转换回 char 形式。 + + 返回值:该函数返回 str 中最后一次出现字符 c 的位置。如果未找到该值,则函数返回一个空指针。 + + 个人水平有限,感谢您的阅读。 diff --git "a/TestTasks/grozurem/scansup.pp\350\247\243\350\257\273.md" "b/TestTasks/grozurem/scansup.pp\350\247\243\350\257\273.md" new file mode 100644 index 0000000000000000000000000000000000000000..5646b34ac8d345a566fc88e334b5cbe93499a897 --- /dev/null +++ "b/TestTasks/grozurem/scansup.pp\350\247\243\350\257\273.md" @@ -0,0 +1,83 @@ +scansup的函数: + + scanstr函数:将语句中的转义字符(如换行符\n)从显示形式转换成实际形式。 + + //scanstr部分代码节选 + for (i = 0, j = 0; i < len; i++) { + if (s[i] == '\'') { + i++; + newStr[j] = s[i];//说明不是转义字符 + } else if (s[i] == '\\') { + i++; + switch (s[i]) { + case 'b': + newStr[j] = '\b'; + break; + case 'f': + newStr[j] = '\f'; + break; + case 'n': + newStr[j] = '\n'; + break; + case 'r': + newStr[j] = '\r'; + break; + case 't': + newStr[j] = '\t'; + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': { + int k; + unsigned long octVal = 0; + for (k = 0; s[i + k] >= '0' && s[i + k] <= '7' && k < 3; k++){ + octVal = (octVal << 3) + (s[i + k] - '0'); + } + i += k - 1; + newStr[j] = ((char)octVal); + } break; + default: + newStr[j] = s[i]; + break; + } /* switch */ + } /* s[i] == '\\' */ + else { + newStr[j] = s[i]; + } + j++; + } + newStr[j] = '\0'; + + downcase_truncate_identifier():对未加引号的标识符进行适当的大小写转换和截断。可选的截断警告。 + + for (i = 0; i < len; i++) { + unsigned char ch = (unsigned char)ident[i]; + if (ch >= 'A' && ch <= 'Z') { + ch += 'a' - 'A'; + } else if (enc_is_single_byte && IS_HIGHBIT_SET(ch) && isupper(ch)) { + ch = tolower(ch); + } + result[i] = (char)ch; } + + void truncate_identifier(char* ident, int len, bool warn) + 可以定性地修改制定的字符串 + + bool scanner_isspace(char ch):如果flex scanner识别出字符空白,则返回TRUE + + bool scanner_isspace(char ch) + { + /* This must match scan.l's list of {space} characters */ + if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' || ch == '\f') { + return true; + } + return false; + } + +``` +总结:scansup文件的作用: +保存转换语义,删改语句的函数,有助于程序正常的识别语句,并且能够灵活的工作。是基础的一个工具型cpp文件。 diff --git "a/TestTasks/grozurem/\347\216\213\345\256\207\351\243\236\344\270\252\344\272\272\346\200\273\347\273\223.md" "b/TestTasks/grozurem/\347\216\213\345\256\207\351\243\236\344\270\252\344\272\272\346\200\273\347\273\223.md" new file mode 100644 index 0000000000000000000000000000000000000000..2ca900d5fc0aa85361713f500a5b93ae54c8e715 --- /dev/null +++ "b/TestTasks/grozurem/\347\216\213\345\256\207\351\243\236\344\270\252\344\272\272\346\200\273\347\273\223.md" @@ -0,0 +1,250 @@ +IKUN不吃鸡肉队——王宇飞个人总结 + 我是北京工业大学IKUN不吃鸡肉队的队员王宇飞,从参赛开始至比赛结束我一直致力于SQL引擎中的查询解析部分的解读与赏析,即parser文件夹(文件地址/src/common/backend/parse/parse_agg.cpp)。以下是我的一些学习总结: + +一、openGauss(因个人水平有限,并未深入了解openGauss所有模块,相关内容参考自https://zhuanlan.zhihu.com/p/365418014) + openGauss数据库是华为深度融合在数据库领域多年经验,结合企业级场景要求推出的新一代企业级开源数据库。openGauss是关系型数据库,采用客户端/服务器,单进程多线程架构;支持单机和一主多备部署方式,同时支持备机可读、双机高可用等特性。 + openGauss采用木兰宽松许可证v2发行,提供面向多核架构的极致性能、全链路的业务、数据安全、基于AI的调优和高效运维的能力。openGauss内核源自PostgreSQL,深度融合华为在数据库领域多年的研发经验,结合企业级场景需求,持续构建竞争力特性。同时,openGauss也是一个开源、免费的数据库平台,鼓励社区贡献、合作。 + + 上图为openGauss的逻辑结构图,下面我将简单介绍一下整体结构: +1.线程管理 + +(1)业务处理线程:业务处理线程负责处理客户端请求的任务。 +(2)日志写线程:日志写线程在openGauss中被命名为WalWriter线程。该线程负责将内存中的预写日志(WAL)页数据刷新到预写日志文件中,确保那些已提交的事务都被永久记录,不会丢失。 +(3)数据页写线程:数据页写线程在openGauss数据库中应该包含两个线程——PageWriter和BgWriter。pagewriter线程负责将脏页数据拷贝至双写(double-writer)区域并落盘,然后将脏页转发给bgwriter子线程进行数据下盘操作,这样可以防止该现象的发生,因为如果发生数据页”折断”的问题,就会从双写空间里找到完整的数据页进行恢复。bgwriter线程(BgWriter)主要负责对共享缓冲区的脏页数据进行下盘操作,目的是让数据库线程在进行用户查询时可以很少或者几乎不等待写动作的发生(写动作由后端写线程完成)。 +(4)检查点线程:检查点线程(Checkpointer)一般会周期性的发起数据库检查点,检查点(CHECKPOINT)是一个事务日志中的点,所有数据文件都在这个点被更新,然后将数据脏页刷新到磁盘的数据文件中,确保数据库一致。 +(5)统计线程:统计线程在openGauss数据库中被命名为StatCollector,该线程负责统计openGauss数据库的信息。 +(6)日志发送线程和日志接收线程:日志发送线程在openGauss中被命名为WalSender,这个线程主要是在openGauss主备环境中,主节点上运行,发送预写日志给备节点。 +(7)清理线程:对过期数据进行清理,并回收存储空间。 +(8)归档线程:归档线程在openGauss数据库中被命名为WalWriter,当数据库归档周期(archive_timeout)到达的时候,由postmaster调用归档线程(WalWriter),强制切换预写日志,并执行归档操作。 +(9)管理线程:管理线程也就是指postmaster线程,在openGauss中被命名为GaussMaster,是消息转发中心。 +(10)还有JOB线程(JobScheduler)、系统日志线程等等。 +2.通信管理 + +(1)通信协议处理:这部分主要涉及的是openGauss数据库所使用的前端和后端协议,根据连接的状态不同,存在几种不同的子协议,如:启动、查询、函数调用、COPY、终止等。 +(2)控制命令信号处理:信号是一种软件中断机制,openGauss数据库线程之间的通讯是离不开这些信号的,比如常见的SIGTERM、SIGQUIT、SIGCHILD、SIGUSR1、SIGUSR2等,这些信号在openGauss的线程源代码中随处可见。 +3.SQL引擎 + +(1)SQL解析:当客户端发送SQL语句,服务端业务处理线程接收后,首先会对接收到的SQL语句进行解析,这些解析依次包括词法解析(将用户输入的SQL语句拆解成单词(Token)序列,并识别出关键字、标识、常量等,确定每个词固有的词性)、语法解析(根据SQL的标准定义语法规则,使用词法分析中产生的词去匹配语法规则,如果一个SQL语句能够匹配一个语法规则,则生成对应的抽象语法树)、语义解析(对语法树进行有效检查,检查语法树中对应的表、列、函数、表达式等是否有对应的元数据,将抽象语法树转换为逻辑执行计划)。 +(2)SQL查询重写:当SQL语句生成逻辑执行计划后,即到了SQL查询重写阶段,利用已有语句特征和关系代数运算(如,交换律、结合律、分配律等)来生成更高效的等价语句。 +(3)SQL优化:SQL优化依赖于SQL的查询重写,它根据生成的高效等价SQL语句,枚举不同的候选执行路径,这些执行路径互相等价但是执行效率不同,经过对他们进行执行代价的计算,最终获得一个最优的执行路径。这里所说的执行代价比如:处理一条元组的CPU代价、加载一个数据页的IO代价、如果是分布式数据库,数据元组的传输代价。 至于根据哪些统计信息计算这些代价呢? 比如:根据表的元组数、字段宽度、NULL记录比率、distinct值、MCV值(表每一列的高频词汇)、HB值(直方图,不包含MCV值)等表的特征值,以及一定的代价计算模型,计算出每一个执行步骤的不同执行方式的输出元组数和执行代价(cost)。 +(4)SQL执行:SQL执行工作由SQL执行器完成,而SQL执行器在数据库的整个体系结构中起承上启下的作用,上联优化器,下联存储。当SQL执行器接收到优化器返回的执行计划树后,遍历整个执行树,根据每个算子的不同特征进行初始化(比如:HashJoin这个算子,在初始化阶段会进行Hash表的初始化,这个初始化主要是内存分配)。 初始化完毕后就进入了执行阶段,执行器会对执行树进行迭代遍历,通过从磁盘读取数据,根据执行树的具体逻辑完成查询语义。 +(5)DDL命令处理:DDL的命令也需要执行基本的词法解析、语法解析和语义解析等操作,但是基本不需要做什么优化处理。DDL命令在被解析完毕后,查询对应的数据字典后就可以开始执行更新操作。 +4.存储引擎 + +(1)行存引擎:openGauss的行存引擎是将表按行存储到硬盘分区上,支持高并发读写、低时延。 +(2)列存引擎:列存引擎主要面向OLAP场景设计,OLAP即就是在线联机分析处理,常见于分析决策型使用场景。 +(3)内存引擎:内存表、也就是指MOT内存引擎,作为在openGauss中与传统基于磁盘的行存储、列存储并存的一种高性能存储引擎,基于全内存态的数据存储,为openGauss提供了高吞吐的实时数据处理分析能力和极低的事务处理延时,在不同的业务负载场景下,可以达到其他引擎事务处理能力的3~10倍。 +(4)CSN快照:CSN(Commit Sequence Number)即就是待提交事务的序列号(一个64位无符号自增长整数),常用于多版本可见性判断和MVCC机制,在openGauss内部使用CSN作为逻辑的时间戳,模拟数据库内部的时序。 +(5)空闲空间管理:定期对历史数据进行清理,以保证数据库的健康运行。 +(6)锁管理器:锁管理器对事务并发访问过程中数据库对象的加锁操作进行管理,判断两个事务访问同一个对象的时候加的锁的类型是否相容,是否允许事务在相应对象上加锁。锁管理器对事务并发过程中使用的锁进行记录、追踪和管理。 +(7)大内存缓冲区管理:大内存缓冲区介于数据存储引擎和外部文件系统之间,常用来同外部文件系统进行page页面交换并作缓冲,对内存共享页面的脏页进行LRU算法淘汰并刷盘,保证内存使用的高效,减少磁盘的访问。 +(8)索引管理:索引可以有效提升数据的访问效率,索引管理主要管理的是索引结构,包括索引创建、更改、删除等。 +(9)存储管理适配:存储管理适配指的就是对存储介质层的管理,对不同的存储介质进行适配封装,对上层数据页面访问屏蔽底层真正存储系统的差异,例如管理HDD的使用、管理SSD的使用。 +(10)还有日志管理等等。 +5.安全管理 +高安全是openGauss数据库给企业带来的重要价值之一,为了有效保障用户隐私数据、防止信息泄露,构建了由内而外的数据库安全保护措施。 openGauss的安全机制充分考虑了数据库可能的接入方,包括DBA、用户、应用程序以及通过攻击途径连接数据库的攻击者等。 +6.客户端驱动 + +7.通用组件 + +8.工具 + +9.硬件&OS平台 + + +二、个人博客 + 比赛期间,围绕parse_agg.cpp文件细节撰写了六篇代码解读与赏析博客: + openGauss:对于parse_agg.cpp的部分解读(1) + openGauss:对于parse_agg.cpp的部分解读(2) + openGauss:对于parse_agg.cpp的部分解读(3) + openGauss:对于parse_agg.cpp的部分解读(4) + openGauss:对于parse_agg.cpp的部分解读(5) + openGauss:对于parse_agg.cpp的部分解读(6) +1.SQL概述 + SQL引擎作为数据库系统的入口,主要承担了对SQL语言进行解析、优化、生成执行计划的作用。对于用户输入的SQL语句,SQL引擎会对语句进行语法/语义上的分析以判断是否满足语法规则等,之后会对语句进行优化以便生成最优的执行计划给执行器执行。故SQL引擎在数据库系统中承担着承上启下的作用,是数据库系统的“大脑”。 + SQL引擎负责对用户输入的SQL语言进行编译,生成可执行的执行计划,然后将执行计划交给执行引擎进行执行。SQL引擎整个编译的过程如下图所示,在编译的过程中需要对输入的SQL语言进行词法分析、语法分析、语义分析,从而生成逻辑执行计划,逻辑执行计划经过代数优化和代价优化之后,产生物理执行计划。 + + 通常可以把SQL引擎分成SQL解析和查询优化两个主要的模块,openGauss中参照SQL语言标准实现了大部分SQL的主要语法功能,并结合应用过程中的具体实践对SQL语言进行了扩展,具有良好的普适性和兼容性。openGauss的查询优化功能也主要分成了逻辑优化和物理优化两个部分,从关系代数和物理执行两个角度对SQL进行优化,进而结合自底向上的动态规划方法和基于随机搜索的遗传算法对物理路径进行搜索,从而获得较好的执行计划。 +2.流程解析(相关内容参考自https://blog.csdn.net/GaussDB/article/details/116132257和https://blog.csdn.net/GaussDB/article/details/119594313) + 比赛期间,我们小组主要针对SQL引擎的查询解析(parse)部分进行阅读解析,下面我将为您介绍查询解析的整体流程。 + 执行SQL命令的入口函数是exec_simple_query。用户输入的SQL命令会作为字符串sql_query_string传给raw_parser函数,由raw_parser函数调用base_yyparse进行词法分析和语法分析,生成语法树添加到链表parsetree_list中。完成语法分析后,对于parsetree_list中的每一颗语法树parsetree,openGuass会调用parse_analyze函数进行语义分析,根据SQL命令的不同,执行对应的入口函数,最终生成查询树。 + + + + SQL语句在数据库管理系统中的编译过程符合编译器实现的常规过程,需要进行词法分析、语法分析和语义分析,获得查询解析树或者逻辑计划: + ·词法分析:从查询语句中识别出系统支持的关键字、标识符、操作符、终结符等,确定每个词自己固有的词性。词法文件由scan.l(/src/common/backend/parse/scan.l)文件定义,并由通过flex工具采用Lex编译生成scan.cpp文件(从parser目录的Makefile中可以看到编译的命令)。 + 词法分析将一个SQL划分成多个不同的token,每个token都有自己的词性,词性分为以下几种:keyword(关键字)、IDENT(标识符)、operator(操作符)、ICONST/FCONST/SCONST/BCONST/XCONST(常量)等。scan.l在处理token时,会优先处理关键字,在kwlist中通过二分法进行查找(kwlist中的关键字按照字母的顺序排序,二分法查找效率极高)。如果未匹配到关键字,才会去定义标识符的文件中去寻找匹配项。 + · 语法分析:根据SQL语言的标准定义语法规则,使用词法分析中产生的词去匹配语法规则,如果一个SQL语句能够匹配一个语法规则,则生成对应的抽象语法树(abstract synatax tree,AST)。openGauss定义了bison工具能够识别的语法文件gram.y(/src/common/backend/parse/gram.y),同样可以在Makefile中可以通过bison工具对gram.y进行编译,生成gram.cpp文件。 + + 一条简单的查询语句由以下子句组成:去除行重复的distinctClause、目标属性targetList、SELECT INTO子句intoClause、FROM子句fromClause、WHERE子句whereClause、GROUP BY子句groupClause、HAVING子句havingClause、窗口子句windowClause和plan_hint子句。在成功匹配simple_select语法结构后,将会创建一个Statement结构体(Statement结构体通常以Stmt作为命名后缀,用来保存语法分析结果),将各个子句进行相应的赋值。这个结构体可以看作一个多叉树,每个叶子结点都表达了该查询语句中的一个语法结构,对应到gram.y中。除了基本形式,还可以表示为VALUES子句、关系表达式多个SELECT语句的集合操作等形式,这些形式会被进一步递归处理,最终都会被转换为基本形式,便于统一处理。 + 对于任何复杂的SQL语句,都可以拆解为多个基本的SQL命令执行。在完成词法分析和语法分析后,raw_parser函数会将所有的语法分析树封装为一个List结构,名为raw_parse_tree_list,返回给exec_simple_query函数,用于后面的语义分析、查询重写等步骤,该List中的每个ListCell包含一个语法树。 + + ·语义分析:语义分析模块在词法分析和语法分析之后执行,用于检查SQL命令是否符合语义规定,能否正确执行。该模块对语法树(AST)进行检查与分析,检查AST中对应的表、列、函数、表达式是否有对应的元数据(指数据库中定义有关数据特征的数据,用来检索数据库信息)描述,基于分析结果对语法树进行扩充,输出查询树。 + 负责语义分析的是parse_analyze函数,位于analyze.cpp下。parse_analyze会根据词法分析和语法分析得到的语法树,生成一个ParseState结构体用于记录语义分析的状态,再调用transformStmt函数,根据不同的命令类型进行相应的处理,最后生成查询树。 + ParseState保存了许多语义分析的中间信息,如原始SQL命令、范围表、连接表达式、原始WINDOW子句、FOR UPDATE/FOR SHARE子句等。该结构体在语义分析入口函数parse_analyze下被初始化,在transformStmt函数下根据不同的Stmt存储不同的中间信息,完成语义分析后再被释放。 + 在语义分析过程中,语法树parseTree使用Node节点进行包装。Node结构只有一个类型为NodeTag枚举变量的字段,用于识别不同的处理情况。transformStmt函数会根据NodeTag的值,将语法树转化为不同的Stmt结构体,调用对应的语义分析函数进行处理。 + 处理目标属性的入口函数是transformTargetList,函数的传参包括结构体ParseState和目标属性链表targetlist。transformTargetList会调用transformTargetEntry来处理语法树下目标属性的每一个ListCell,最终将语法树ResTarget结构体的链表转换为查询树TargetEntry结构体的链表,每一个TargetEntry表示查询树的一个目标属性。 + FROM子句由transformFromClause函数进行处理,最后生成范围表。该函数的主要传参除了结构体ParseState,还包括分析树SelectStmt的fromClause字段。fromClause是List结构,由FROM子句中的表、视图、子查询、函数、连接表达式等构成,由transformFromClauseItem函数进行检查和处理。transformFromClauseItem会根据fromClause字段的每个Node生成一个或多个RangeTblEntry结构,加入ParseState的p_rtable字段指向的链表中,最终生成查询树的rtable字段也会指向该链表。 + 处理WHERE子句的入口函数是transformWhereClause,该函数调用transformExpr将分析树SelectStmt下whereClause字段表示的WHERE子句转换为一颗表达式树,然后将ParseState的p_joinlist所指向的链表和从WHERE子句得到的表达式树包装成FromExpr结构,存入查询树的jointree。 + transformStmt函数完成语义分析后会返回查询树。一条SQL语句的每个子句的语义分析结果会保存在Query的对应字段中。查询树结构体定义如下: + + typedef struct Query { + NodeTag type; + CmdType commandType; // 命令类型 + QuerySource querySource; // 查询来源 + uint64 queryId; // 查询树的标识符 + bool canSetTag; // 如果是原始查询,则为false;如果是查询重写或者查询规划新增,则为true + Node* utilityStmt; // 定义游标或者不可优化的查询语句 + int resultRelation; // 结果关系 + bool hasAggs; // 目标属性或HAVING子句中是否有聚集函数 + bool hasWindowFuncs; // 目标属性中是否有窗口函数 + bool hasSubLinks; // 是否有子查询 + bool hasDistinctOn; // 是否有DISTINCT子句 + bool hasRecursive; // 公共表达式是否允许递归 + bool hasModifyingCTE; // WITH子句是否包含INSERT/UPDATE/DELETE + bool hasForUpdate; // 是否有FOR UPDATE或FOR SHARE子句 + bool hasRowSecurity; // 重写是否应用行级访问控制 + bool hasSynonyms; // 范围表是否有同义词 + List* cteList; // WITH子句,用于公共表达式 + List* rtable; // 范围表 + FromExpr* jointree; // 连接树,描述FROM和WHERE子句出现的连接 + List* targetList; // 目标属性 + List* starStart; // 对应于ParseState结构体的p_star_start + List* starEnd; // 对应于ParseState结构体的p_star_end + List* starOnly; // 对应于ParseState结构体的p_star_only + List* returningList; // RETURNING子句 + List* groupClause; // GROUP子句 + List* groupingSets; // 分组集 + Node* havingQual; // HAVING子句 + List* windowClause; // WINDOW子句 + List* distinctClause; // DISTINCT子句 + List* sortClause; // ORDER子句 + Node* limitOffset; // OFFSET子句 + Node* limitCount; // LIMIT子句 + List* rowMarks; // 行标记链表 + Node* setOperations; // 集合操作 + List *constraintDeps; + HintState* hintState; + …… + } Query; + +下图为:语法树内存组织结构图和查询树内存组织结构图 + + + +3.函数解析 +我主要负责阅读解析parse_agg.cpp(文件地址/src/common/backend/parse/parse_agg.cpp),parse_agg.cpp用于在解析器中处理聚合和窗口函数,下面我来为大家简单解读一下该文件中的部分函数。 +(1)1.transformAggregateCall + 该函数用于完成调用聚合的初始转换。如果parse_func.c已将函数识别为聚合体,并已设置Aggref的所有字段,但args、aggorder、aggdistinct和aggrevelSup除外。传入的args列表已通过标准表达式转换,而传入的aggorder列表没有转换。通过插入TargetEntry节点将args列表转换为目标列表,然后转换aggorder和agg_distinct规范以生成SortGroupClause节点列表,这可能会导致向targetlist添加resjunk表达式。需要确定聚合实际属于哪个查询级别,相应地设置aggrevelsup,并在相应的pstate级别中将p_hasAggs标记为真。 + + ParseState* pstate //解析期间状态信息 + Aggref* agg //引用目标聚合函数 + List* args //目标聚合参数列表 + List* aggorder //聚合命令列表 + bool agg_distinct //是否包含DISTINCT + + 该函数首先对传进来的列表进行检测,如果为有序集,则将直接args和聚合args分开并将直接参数保存在第一个“numDirectArgs”参数处,然后通过numDirectArgs计算直接args的数量并且断言直接args的数量不少于0。接着需要构建一个包含聚合参数(Exprs列表)的tlist(通常只有一个目标条目)用于保存有序集合agg的排序信息,并创建torder保存用于转换为SortGroupClause的关于顺序的目标(注意:DISTINCT不能在有序集合agg中使用)。如果不是有序集,那么没有直接args,将Exprs的普通列表转换为目标列表(不需要为条目指定列名)。再进行下一步之前,需要打乱p_next_resno(p_next_resno为int类型,表示下一个分配给目标属性的资源号),因为它将用于对任何新的目标列表条目进行编号。接着进行转换排序操作,得到一个转换结果。在此处检测是否有DISTINCT,如果包含DISTINCT将其转换为distinctList。如果属性所属源表的OID无效,则报告错误信息:不能使用无法识别类型的排序运算符。也就是说:具有DISTINCT的聚合必须能够对其输入进行排序(如果对聚合散列添加过不同的执行程序支持,则可以省略该步骤)。 + 跳出if-else结构,使用转换结果更新Aggref。首先设置聚合的级别与其参数中最低级别变量或聚合的级别相同,如果根本不包含变量,将其设置为本地vars(注意:SQL规定聚合函数调用不能嵌套,并且不能包含窗口函数调用)。最后,将正确的pstate标记为具有聚合,函数调用结束。 + +2.transformWindowFuncCall + 该函数用于完成窗口函数调用的初始转换。如果parse_func.c已将该函数识别为窗口函数,并已设置除winref之外的所有窗口函数字段,则必须将WindowDef添加到pstate,并设置winref链接到它;在pstate中标记p_hasWindowFuncs为真。与聚合不同,只需要考虑嵌套最紧密的pstate级别——根据SQL规范,没有“外部窗口函数”。 + + ParseState* pstate //解析期间状态信息 + WindowFunc* wfunc //窗口函数结构体 + WindowDef* windowdef //窗口函数的原始表示 + + 如果OVER子句只指定了一个窗口名,那么查找该WINDOW子句。如果指定的窗口名不止一个,匹配OVER子句的所有属性。如果未查找到指定WINDOW子句,则提示用户窗口不存在,并在p_windowdefs列表中创建一个新条目。最后将pstate->p_hasWindowFuncs标记为true,函数调用结束。 + +3.parseCheckAggregates + 该函数用于用于分析和检查,并找出使用有误的聚合函数。 + + ParseState* pstate //解析期间状态信息 + Query* qry //查询树 + + 只有在发现聚合或者groupClause时才调用该函数。如果已经生成了分组集,则展开它们并找到所有集合的交集。如果交集为空,报告错误信息并判断分析器错误位置(交集通常是空的,因此通过设置最小集合来获得交集,以提高解决问题的效率)。如果交集不为空,则遍历所有分组集,用交集与分组集形成新的交集,再用上一次循环中获得的新交集与另一分组集形成新的交集 ,直到交集为空或者遍历完所有分组集为止,结束循环并继续向下进行。如果扩展中只有一个分组集,并且groupClause为非空(这意味着分组集也不是空的),那么我们可以舍弃分组集,并假设只有一个正常的GROUP BY。然后扫描范围表以查看是否存在连接(JOIN)或者自引用CTE条目,如果存在连接(JOIN),则标记hasJoinRTEs为true;如果存在自引用CTE条目,则标记hasSelfRefRTEs为true。如果存在连接(JOIN)或者自引用CTE条目,必须将它们展平到基础变量(使用planner的flatten_join_alias_vars例程对其进行展平,需要一个PlannerInfo根节点),以便正确地将别名变量和非别名变量视为相等。在展平别名后,在groupClauseCommonVars中分别跟踪所有分组集中包含的变量,因为这些变量是唯一可以用来检查函数相关性的变量。 + 检测聚合是否出现在WHERE、JOIN/ON、GROUP BY子句以及递归(检测所有分组表达式,如果都是Vars,那么就不必在递归扫描中耗费大量精力)中,如果发现,则传递错误信息(该检查应最先出现,否则可能会将错误归结于目标列表中的某个无关变量)。检查目标列表和HAVING子句中未分组的变量,检查resjunk tlist元素以及常规元素,会找到来自ORDER BY和WINDOW子句的未分组变量,同时检查分组表达式本身(都会通过测试),还需要遍历原始(未展平)子句以修改节点,最终确定分组表达式,函数调用结束。 + + 4.parseCheckWindowFuncss + 该函数用于解析检查,并找出使用有误的窗口函数。WHERE、JOIN/ON、HAVING、GROUP BY和窗口规范语句中不可使用窗口函数,其他子句如RETURNING和LIMIT在这之前已经完成检测,无需再次检查。在调用此函数之前,必须完成以上提到的WHERE等子句的转换。 + + ParseState* pstate //解析期间状态信息 + Query* qry //查询树 + + 只有在检测到窗口函数时才调用此函数,WHERE、HAVING、GROUP BY子句、连接条件(JOIN)、窗口定义中不允许出现窗口函数。如果发现,则报告错误信息。 + +5.check_ungrouped_columns和finalize_grouping_exprs + check_ungrouped_columns用于扫描给定表达式树中的未分组变量(未列在Group子句列表中且不在聚合函数的参数内的变量),如果发现任何错误,则报告错误信息。我们假设给定的子句已被适当地转换为解析器输出,这意味着可以使用check_ungrouped_columns_walker。 + finalize_grouping_exprs扫描给定表达式树中的GROUPING()和相关调用,并验证和处理它们的参数。由于连接别名变量的扁平化,check_ungroupled_columns可能只看到原始的一个副本。因此,我们在比较之前将每个单独的 GROUPING 参数展平。 + +6.check_ungrouped_columns_walker + 该函数用于扫描给定表达式树中的未分组变量,返回值为bool类型。首先对节点类型进行判断,如果节点为空,则返回false。如果节点类型为 Const 或者 Param,则返回 false。如果节点类型为Aggref,若找到与context->sublevels_up相同级别的聚合调用,标记返回值为false,如果有序集合 agg 内含有直接参数,则报告错误信息(可以跳过更高级别聚合的参数,因为其中不可能包含我们关心的变量。因此,我们只需要研究较低层次聚合的参数)。如果 node 节点类型为 GroupingFunc,则返回false。如果节点类型为Var,如果不是原始查询级别则返回false,若是原始查询级别还需进行其他检测。如果找到未分组的局部变量,生成错误信息。如果RTE被重写,则修改attname。如果节点类型为Query,则递归到子选择中。 + 最后返回表达式树,函数调用结束。 + +7.finalize_grouping_exprs_walker + 该函数的流程与功能check_ungrouped_columns_walker类似,此处不再赘述。 + +8.expand_groupingset_node + 该函数负责展开不同类型的节点。给定一个GroupingSet节点,将其展开并返回一个嵌套链表(储存链表的链表)。 + ·对于 EMPTY 节点,返回一个储存空链表的链表。 + ·对于 SIMPLE 节点,返回一个储存节点内容的链表。 + ·对于 CUBE 和 ROLLUP 节点,返回拓展列表。 + ·对于 SET 节点,递归拓展包含的 CUBE 和 ROLLUP 节点。 + +9.int cmp_list_len_asc + 该函数用于比较两个链表长度是否相等。 + +10.build_aggregate_fnexprs和build_trans_aggregate_fnexprs + build_aggregate_fnexprs 与 build_trans_aggregate_fnexprs 为聚合的转换和 final 函数创建表达式树,以便可以在聚合中使用多态函数——如果没有表达式树,这些函数将不知道它们应该使用的数据类型。 + agg_input_types、agg_state_type、agg_result_type 标识聚合的输入、转换和结果类型。 + 这些应被解析为实际类型(即没有一个应该是 ANYELEMENT 等),agg_input_collation 是聚合函数的输入排序规则。 + transfn_oid 和 finalfn_oid 标识要调用的函数,其中 finaln_oid 可能是 InvalidOid。 + 指向构造树的指针返回到 transfnexpr 和 finalfnexpr。 如果没有 finalfn,则后者设置为 NULL。 + + Oid* agg_input_types //输入参数类型的oid + int agg_num_inputs //输入的参数个数 + Oid agg_state_type //参数状态类型 + Oid agg_result_type //参数结果类型 + Oid agg_input_collation //整理后输入的参数 + Oid transfn_oid //转换函数的oid + Oid finalfn_oid //final函数的oid + Expr** transfnexpr //转换函数表达式 + Expr** finalnexpr //最终函数表达式 + +11.expand_grouping_sets + 该函数将 groupingSets 扩展为平面列表,并将返回的列表按长度排序,最短的集在前,也可以用来做部分一致性检查。 + + List* groupingSets //groupingSets子句 + int limit //限制数 + + 首先判断链表是否为空,若为空则返回NIL。如果不为空则遍历groupingSets中的节点,扩展gs节点并将扩展后的节点链表加入链表。然后在expanded_groups的子链表之间做笛卡尔积,并从单个分组集中删除任何重复的元素。最后将结果加入result并释放buf指针,函数调用结束。 + +12.transformGroupingFunc + 该函数转换 GROUPING 表达式,GROUPING 的行为非常像一个聚合,并且其级别和嵌套的处理与聚合相同,为这些表达式设置了 p_hasAggs。 + + ParseState* pstate //当前解析状态 + GroupingFunc* p //分组函数 + + 首先创建分组函数链表,如果参数链表大于31则抛出异常。接着转换grouping表达式类型,将结果加入result_list链表中,后续检查表达式的正确性。最后返回转换后的grouping表达式,函数调用结束。 + +13.check_windowagg_can_shuffle + 该函数检查 windowagg 是否可以打乱顺序。如果划分条件为空链表,则可以打乱顺序如果不是空链表,遍历划分条件链表,在grpcl中寻找targetList,如果未找到则继续遍历,如果找到则返回 false 表示不可打乱。 + + List* partitionClause //partition子句 + List* targeList //目标链表 + +14.get_aggregate_argtypes + 给定一个Aggref,该函数可以提取输入参数的实际数据类型。 + 对于给定集合的参数,Aggref包含直接参数和聚合参数,并且直接参数在聚合参数之前保存。 + 参数的数据类型加载保存在 inputTypes[],inputTypes[] 必须引用一个长度为 FUNC_MAX_ARGS 的数组。 + + Aggref* aggref //参数引用列表 + Oid* inputTypes //输入类型oid + int func_max_args //函数最大参数 + +15.resolve_aggregate_transtype + 该函数用于确定聚合调用的过渡状态值的数据类型。 + 当 agg 接受 ANY 或多态类型时,确定聚合调用的过渡状态值的数据类型。 + 此函数解析多态聚合的状态数据类型。aggtranstype 以及 get_aggregate_argtypes 提取的实际参数类型通过搜索 pg_aggregate 目录传递。 + +三、参赛感想 + 在本次软件开源大赛——openGauss代码评注赛中,我感觉到受益匪浅。在评注过程中我不仅学习到了许多优秀的代码,也接触到了新的编程思想。同时在GitLink平台的论坛交流中,通过与专业人员和同学们的交流学习,也得到了很大提升。最后,要特别感谢在此次比赛给予我们帮助的北京工业大学杨惠荣老师以及大赛组委会的各位老师们!