From c2ef23848bee34191ec272183d3d90f6429b2d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=A0=BC?= Date: Wed, 23 Nov 2022 06:13:49 +0000 Subject: [PATCH 01/16] add TestTasks. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 罗格 --- TestTasks/geyouguang | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 TestTasks/geyouguang diff --git a/TestTasks/geyouguang b/TestTasks/geyouguang new file mode 100644 index 0000000..e69de29 -- Gitee From e5c55b5dcfa6fe1d3585b1e334895486d5cf6ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=A0=BC?= Date: Wed, 23 Nov 2022 14:13:25 +0000 Subject: [PATCH 02/16] rename TestTasks/geyouguang to TestTasks/geyouguang/. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 罗格 --- ...\347\232\204\346\234\210\345\205\211\351\230\237-openGauss.md" | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename TestTasks/geyouguang => "TestTasks/geyouguang/\345\215\232\345\256\242\344\273\243\347\240\201\346\240\207\346\263\250\346\200\273\347\273\223-\345\245\224\350\265\260\347\232\204\346\234\210\345\205\211\351\230\237-openGauss.md" (100%) diff --git a/TestTasks/geyouguang "b/TestTasks/geyouguang/\345\215\232\345\256\242\344\273\243\347\240\201\346\240\207\346\263\250\346\200\273\347\273\223-\345\245\224\350\265\260\347\232\204\346\234\210\345\205\211\351\230\237-openGauss.md" similarity index 100% rename from TestTasks/geyouguang rename to "TestTasks/geyouguang/\345\215\232\345\256\242\344\273\243\347\240\201\346\240\207\346\263\250\346\200\273\347\273\223-\345\245\224\350\265\260\347\232\204\346\234\210\345\205\211\351\230\237-openGauss.md" -- Gitee From baa15b684278edb3c15d9919b644bce103cbe625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=A0=BC?= Date: Wed, 23 Nov 2022 14:14:02 +0000 Subject: [PATCH 03/16] =?UTF-8?q?update=20TestTasks/geyouguang/=E5=8D=9A?= =?UTF-8?q?=E5=AE=A2=E4=BB=A3=E7=A0=81=E6=A0=87=E6=B3=A8=E6=80=BB=E7=BB=93?= =?UTF-8?q?-=E5=A5=94=E8=B5=B0=E7=9A=84=E6=9C=88=E5=85=89=E9=98=9F-openGau?= =?UTF-8?q?ss.md.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 罗格 --- ...\210\345\205\211\351\230\237-openGauss.md" | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git "a/TestTasks/geyouguang/\345\215\232\345\256\242\344\273\243\347\240\201\346\240\207\346\263\250\346\200\273\347\273\223-\345\245\224\350\265\260\347\232\204\346\234\210\345\205\211\351\230\237-openGauss.md" "b/TestTasks/geyouguang/\345\215\232\345\256\242\344\273\243\347\240\201\346\240\207\346\263\250\346\200\273\347\273\223-\345\245\224\350\265\260\347\232\204\346\234\210\345\205\211\351\230\237-openGauss.md" index e69de29..58db842 100644 --- "a/TestTasks/geyouguang/\345\215\232\345\256\242\344\273\243\347\240\201\346\240\207\346\263\250\346\200\273\347\273\223-\345\245\224\350\265\260\347\232\204\346\234\210\345\205\211\351\230\237-openGauss.md" +++ "b/TestTasks/geyouguang/\345\215\232\345\256\242\344\273\243\347\240\201\346\240\207\346\263\250\346\200\273\347\273\223-\345\245\224\350\265\260\347\232\204\346\234\210\345\205\211\351\230\237-openGauss.md" @@ -0,0 +1,60 @@ +#博客/代码标注总结-奔走的月光队-openGauss + +##队伍名称 + +奔走的月光 + +##队伍成员及其论坛主页 +- [罗格](https://forum.gitlink.org.cn/accounts/Eao3piq4e/memos "罗格") +- [周忠泉](https://forum.gitlink.org.cn/accounts/Eukanj827/memos "周忠泉") +- [李欣然](https://forum.gitlink.org.cn/accounts/xinran/memos "李欣然") + +##主题介绍 + +  我们队伍力求将一条 SQL 语句在 openGauss 内部的处理流程给展现出来,从模块层面入手,在确定了各自负责的模块后,接着就从代码层面入手,从实际的理论出发,争取把流程讲明白,把内部机制说清楚。博客共计35篇,对函数的注释共计101个。 + +##工作介绍 + +  我们三人的工作围绕“一条 SQL 语句的处理流程”展开,那么,当我们将眼光放到 openGauss 是如何处理一条 SQL 语句时,我们就会发现,openGauss 内部的结构是有序的,是以模块为基本单位从而层层处理 SQL 语句的,详见下图: + +![](https://img-blog.csdnimg.cn/1727c3ba7ba749b398da7a3b8e88bba0.png) + +当一条 SQL 语句由终端输入,经 postmaster 模块即**通信管理模块**处理与客户端的连接请求后,被 parse 模块即**查询分析模块**接收,得到的查询树被 optimizer 模块即**查询重写和查询优化模块**接收后进一步重写优化,最后将得到的计划树交给 executor 模块**查询执行模块**,在查询执行模块执行计划树时会和存储引擎有数据交互,最后查询执行模块会将执行结果返回终端。 + +  我们三人的工作便是解析这几个重要模块,弄清楚它们的内部机理,接下来我将一一介绍。 + +####罗格:队长 + +  本人负责解析查询分析、查询重写、查询执行模块,其中,查询分析模块是最复杂的。对一条查询语句进行查询分析,有三步:词法分析、语法分析、语义分析。经过前两步我们构造出了词法分析器和语法分析器,语法分析器能够调用词法分析器并返回语法分析树。这棵语法分析树被传送回主调函数,然而这时还不能确定它就是有效的。有可能这条语句并不符合语法规则。自然地, 由这条语句构造出来的语法分析树就是无效的。所以,我们还要对这棵语法分析树进行语义分析,那么,将由主调函数把这棵语法分析树交给特定的函数进行语义分析。如果它确实是有效的,那就会将它转变成查询树,之后它会被交给查询重写模块。然而并不是所有 SQL 语句构造出来的查询树都会被重写,例如由功能性语句创建而来的查询树便不会被重写。 + +  之后便是查询执行模块,它的内部又分为好几个子模块,主要的有 Portal 模块、Executor 模块、Plan 模块、Node 模块以及 ProcessUtility 模块。分层次地讲,Portal模块是决策模块,它决定要把计划树交给 Executor 模块还是 ProcessUtility 模块,而 Plan 模块和 Node 模块是 Executor 模块的下属模块,用来处理更加精细的工作。需要说一点,查询执行模块内部被划分成了几个模块,这并不是说它们仅限于模块和模块之间的联系,事实上,它们各自的接口函数是有一种层级的调用关系的,更详细的解释本人在文末的总结博客里有叙述。 + +####周忠泉:队员 + +  该队员负责解析 Postmasters 模块,即通讯管理模块。前面已经提到,通信管理模块是 openGuass 在处理简单 SQL 语句时的调用的第一个模块,也是其最基础的代码模块。openGauss 查询响应使用的是“单个用户对应一个服务器线程”的简单客户端/服务器模型实现的。由于我们无法预先知道需要建立多少连接,所以必须使用主进程(GaussMaster)来监听指定 TCP/IP (传输控制协议/网际协议)端口上的传入连接,只要连接请求被检测到,主进程将生成一个新的服务器线程。服务器线程使用信号量和共享内存相互通信,以确保整个并发数据访问期间的数据完整性。 + +  另外,openGuass 的一大亮点就是将具有创新性的 AI 技术引入数据库,该同学在AI调优技术总结中介绍了AI引入数据库的两个研究方向,即 DB4AI 和 AI4DB,并对这两个方向中涉及的主要技术以图表的方式进行了总结,然后结合 openGuass 中使用到的 AI 技术进行了解析,包括数据库快照、数据库查询优化、X-Tuner 调优策略、索引管理技术等方面。通过比较发现,openGuass 相较于其他数据库中的 AI 技术,具有学习门槛低、数据库管理简洁、性能更优的特点。 + +####李欣然:队员 + +  该队员负责查询优化模块的解析工作,已知查询重写和查询优化模块同属于优化器的,当查询重写模块将查询树重写后,这棵重写后的查询树就被传送到了查询优化模块。另外,我们所说的单棵查询树可以扩展到查询树链表,因为我们不一定是单次只写一条 SQL 语句,当事务中由多条 SQL 语句时,我们便会建立起对每条 SQL 语句的语法分析树,形成一条链表,最后过渡到查询树链表,这一点是很重要的。 + +  查询优化的第一步是查询重写,该队员从原理上进行了描述,发现 openGauss 采用的是 CBO(Cost Based Optimization) 技术,即基于代价的查询优化,并且在 ABO(AI Based Optimization) 技术即基于机器学习的查询优化上也有所探索。第二步则是路径搜索,优化器的核心问题是获取特定 SQL 语句的最优解,这个过程通常需要枚举 SQL 语句对应的解空间,即枚举不同的候选执行路径。这些候选执行路径是等价的,但是执行效率不同,需要计算执行成本,最终得到最优执行路径。第三步则是代价估算,查询执行成本分为 I/O 成本和 CPU 成本。这两个成本与查询期间处理的元组数量正相关。因此,选择性地评估查询计划的总成本会使得最终的执行计划更为高效。 + +##代表性博客 + +- 1、[查询分析模块总结](https://forum.gitlink.org.cn/forums/7501/detail "查询分析模块总结") + +- 2、[查询执行模块总结](https://forum.gitlink.org.cn/forums/7796/detail "查询执行模块总结") + +- 3、[查询重写模块总结](https://forum.gitlink.org.cn/forums/8209/detail "查询重写模块总结") + +- 4、[通信管理模块总结](https://forum.gitlink.org.cn/forums/8210/detail "通信管理模块总结") + +- 5、[查询优化模块总结](https://forum.gitlink.org.cn/forums/8133/detail "查询优化模块总结") + +##队伍总结 + +  在解析 openGauss 内部的模块前,我们还解析了一部分代码,为一部分代码添加了注释,可以在华为的 openGauss仓库下看到我们的合并请求:[GitLink | 确实开源](https://www.gitlink.org.cn/huawei/openGauss-server/pulls "GitLink | 确实开源"),这段经历也让我们明白了 openGauss 内部代码的复杂程度。在查阅 openGauss 的源代码时,我们也会将所查阅的部分和 PostgreSQL 的对应代码作比较,看一看 openGauss 代码在哪里做了改进,查找比对代码的过程中我们自己也收获了很多的经验。同时,不断地翻看找来的资料,我们也进一步理解了 openGauss 的结构和机理。 + +  金秋十月,此次比赛接近尾声。我们团队不遗余力,奋发向上,终于将提交我们共同努力的最好的答卷。在此,感谢CCF开源发展委员会组织的这次比赛,为我们提供了展现能力的平台,感谢团队里的两位队员,以及我们的指导老师。 \ No newline at end of file -- Gitee From a474fe551ec875e9900cadfa05554255d9e5c549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=A0=BC?= Date: Wed, 23 Nov 2022 14:15:37 +0000 Subject: [PATCH 04/16] =?UTF-8?q?update=20TestTasks/geyouguang/=E5=8D=9A?= =?UTF-8?q?=E5=AE=A2=E4=BB=A3=E7=A0=81=E6=A0=87=E6=B3=A8=E6=80=BB=E7=BB=93?= =?UTF-8?q?-=E5=A5=94=E8=B5=B0=E7=9A=84=E6=9C=88=E5=85=89=E9=98=9F-openGau?= =?UTF-8?q?ss.md.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 罗格 --- ...\210\345\205\211\351\230\237-openGauss.md" | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git "a/TestTasks/geyouguang/\345\215\232\345\256\242\344\273\243\347\240\201\346\240\207\346\263\250\346\200\273\347\273\223-\345\245\224\350\265\260\347\232\204\346\234\210\345\205\211\351\230\237-openGauss.md" "b/TestTasks/geyouguang/\345\215\232\345\256\242\344\273\243\347\240\201\346\240\207\346\263\250\346\200\273\347\273\223-\345\245\224\350\265\260\347\232\204\346\234\210\345\205\211\351\230\237-openGauss.md" index 58db842..e021a8f 100644 --- "a/TestTasks/geyouguang/\345\215\232\345\256\242\344\273\243\347\240\201\346\240\207\346\263\250\346\200\273\347\273\223-\345\245\224\350\265\260\347\232\204\346\234\210\345\205\211\351\230\237-openGauss.md" +++ "b/TestTasks/geyouguang/\345\215\232\345\256\242\344\273\243\347\240\201\346\240\207\346\263\250\346\200\273\347\273\223-\345\245\224\350\265\260\347\232\204\346\234\210\345\205\211\351\230\237-openGauss.md" @@ -1,19 +1,19 @@ -#博客/代码标注总结-奔走的月光队-openGauss +# 博客/代码标注总结-奔走的月光队-openGauss -##队伍名称 +## 队伍名称 奔走的月光 -##队伍成员及其论坛主页 +## 队伍成员及其论坛主页 - [罗格](https://forum.gitlink.org.cn/accounts/Eao3piq4e/memos "罗格") - [周忠泉](https://forum.gitlink.org.cn/accounts/Eukanj827/memos "周忠泉") - [李欣然](https://forum.gitlink.org.cn/accounts/xinran/memos "李欣然") -##主题介绍 +## 主题介绍   我们队伍力求将一条 SQL 语句在 openGauss 内部的处理流程给展现出来,从模块层面入手,在确定了各自负责的模块后,接着就从代码层面入手,从实际的理论出发,争取把流程讲明白,把内部机制说清楚。博客共计35篇,对函数的注释共计101个。 -##工作介绍 +## 工作介绍   我们三人的工作围绕“一条 SQL 语句的处理流程”展开,那么,当我们将眼光放到 openGauss 是如何处理一条 SQL 语句时,我们就会发现,openGauss 内部的结构是有序的,是以模块为基本单位从而层层处理 SQL 语句的,详见下图: @@ -23,25 +23,25 @@   我们三人的工作便是解析这几个重要模块,弄清楚它们的内部机理,接下来我将一一介绍。 -####罗格:队长 +#### 罗格:队长   本人负责解析查询分析、查询重写、查询执行模块,其中,查询分析模块是最复杂的。对一条查询语句进行查询分析,有三步:词法分析、语法分析、语义分析。经过前两步我们构造出了词法分析器和语法分析器,语法分析器能够调用词法分析器并返回语法分析树。这棵语法分析树被传送回主调函数,然而这时还不能确定它就是有效的。有可能这条语句并不符合语法规则。自然地, 由这条语句构造出来的语法分析树就是无效的。所以,我们还要对这棵语法分析树进行语义分析,那么,将由主调函数把这棵语法分析树交给特定的函数进行语义分析。如果它确实是有效的,那就会将它转变成查询树,之后它会被交给查询重写模块。然而并不是所有 SQL 语句构造出来的查询树都会被重写,例如由功能性语句创建而来的查询树便不会被重写。   之后便是查询执行模块,它的内部又分为好几个子模块,主要的有 Portal 模块、Executor 模块、Plan 模块、Node 模块以及 ProcessUtility 模块。分层次地讲,Portal模块是决策模块,它决定要把计划树交给 Executor 模块还是 ProcessUtility 模块,而 Plan 模块和 Node 模块是 Executor 模块的下属模块,用来处理更加精细的工作。需要说一点,查询执行模块内部被划分成了几个模块,这并不是说它们仅限于模块和模块之间的联系,事实上,它们各自的接口函数是有一种层级的调用关系的,更详细的解释本人在文末的总结博客里有叙述。 -####周忠泉:队员 +#### 周忠泉:队员   该队员负责解析 Postmasters 模块,即通讯管理模块。前面已经提到,通信管理模块是 openGuass 在处理简单 SQL 语句时的调用的第一个模块,也是其最基础的代码模块。openGauss 查询响应使用的是“单个用户对应一个服务器线程”的简单客户端/服务器模型实现的。由于我们无法预先知道需要建立多少连接,所以必须使用主进程(GaussMaster)来监听指定 TCP/IP (传输控制协议/网际协议)端口上的传入连接,只要连接请求被检测到,主进程将生成一个新的服务器线程。服务器线程使用信号量和共享内存相互通信,以确保整个并发数据访问期间的数据完整性。   另外,openGuass 的一大亮点就是将具有创新性的 AI 技术引入数据库,该同学在AI调优技术总结中介绍了AI引入数据库的两个研究方向,即 DB4AI 和 AI4DB,并对这两个方向中涉及的主要技术以图表的方式进行了总结,然后结合 openGuass 中使用到的 AI 技术进行了解析,包括数据库快照、数据库查询优化、X-Tuner 调优策略、索引管理技术等方面。通过比较发现,openGuass 相较于其他数据库中的 AI 技术,具有学习门槛低、数据库管理简洁、性能更优的特点。 -####李欣然:队员 +#### 李欣然:队员   该队员负责查询优化模块的解析工作,已知查询重写和查询优化模块同属于优化器的,当查询重写模块将查询树重写后,这棵重写后的查询树就被传送到了查询优化模块。另外,我们所说的单棵查询树可以扩展到查询树链表,因为我们不一定是单次只写一条 SQL 语句,当事务中由多条 SQL 语句时,我们便会建立起对每条 SQL 语句的语法分析树,形成一条链表,最后过渡到查询树链表,这一点是很重要的。   查询优化的第一步是查询重写,该队员从原理上进行了描述,发现 openGauss 采用的是 CBO(Cost Based Optimization) 技术,即基于代价的查询优化,并且在 ABO(AI Based Optimization) 技术即基于机器学习的查询优化上也有所探索。第二步则是路径搜索,优化器的核心问题是获取特定 SQL 语句的最优解,这个过程通常需要枚举 SQL 语句对应的解空间,即枚举不同的候选执行路径。这些候选执行路径是等价的,但是执行效率不同,需要计算执行成本,最终得到最优执行路径。第三步则是代价估算,查询执行成本分为 I/O 成本和 CPU 成本。这两个成本与查询期间处理的元组数量正相关。因此,选择性地评估查询计划的总成本会使得最终的执行计划更为高效。 -##代表性博客 +## 代表性博客 - 1、[查询分析模块总结](https://forum.gitlink.org.cn/forums/7501/detail "查询分析模块总结") @@ -53,7 +53,7 @@ - 5、[查询优化模块总结](https://forum.gitlink.org.cn/forums/8133/detail "查询优化模块总结") -##队伍总结 +## 队伍总结   在解析 openGauss 内部的模块前,我们还解析了一部分代码,为一部分代码添加了注释,可以在华为的 openGauss仓库下看到我们的合并请求:[GitLink | 确实开源](https://www.gitlink.org.cn/huawei/openGauss-server/pulls "GitLink | 确实开源"),这段经历也让我们明白了 openGauss 内部代码的复杂程度。在查阅 openGauss 的源代码时,我们也会将所查阅的部分和 PostgreSQL 的对应代码作比较,看一看 openGauss 代码在哪里做了改进,查找比对代码的过程中我们自己也收获了很多的经验。同时,不断地翻看找来的资料,我们也进一步理解了 openGauss 的结构和机理。 -- Gitee From 7020f91d638e9877ee503fbe6bd84b93b505475a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=A0=BC?= Date: Wed, 23 Nov 2022 14:18:06 +0000 Subject: [PATCH 05/16] =?UTF-8?q?add=20TestTasks/geyouguang/=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E5=88=86=E6=9E=90=E6=A8=A1=E5=9D=97=E6=80=BB=E7=BB=93?= =?UTF-8?q?.md.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 罗格 --- ...41\345\235\227\346\200\273\347\273\223.md" | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 "TestTasks/geyouguang/\346\237\245\350\257\242\345\210\206\346\236\220\346\250\241\345\235\227\346\200\273\347\273\223.md" diff --git "a/TestTasks/geyouguang/\346\237\245\350\257\242\345\210\206\346\236\220\346\250\241\345\235\227\346\200\273\347\273\223.md" "b/TestTasks/geyouguang/\346\237\245\350\257\242\345\210\206\346\236\220\346\250\241\345\235\227\346\200\273\347\273\223.md" new file mode 100644 index 0000000..55e8a73 --- /dev/null +++ "b/TestTasks/geyouguang/\346\237\245\350\257\242\345\210\206\346\236\220\346\250\241\345\235\227\346\200\273\347\273\223.md" @@ -0,0 +1,167 @@ +# 查询分析模块总结 + +  在这半个月学习查询解析模块的过程中,本人不断地翻看源码,不断地上网查找相应的资料,终于对这个模块有了进一步的认识。 + +  原本本人认为查询分析模块的作用就是对所有的 SQL 语句进行分析处理最终生成查询树,而实际上只是对于查询处理命令( SELECT/INSERT/DELETE/UPDATE )才为其构建查询树,若是功能性命令( CREATE TABLE/CREATE USER 等 )则将其分配到功能性命令处理模块即 ProcessUtility 模块,后期本人会解析一下这个模块。本人认为这是本人在学习了相关的知识后,对这个模块作用上的认识的一个很大的改观。除此之外,本人也对查询分析过程中的三个步骤有了更多的理解。 + +## 词法分析 + +  词法分析的定义是: + +- 从查询语句中识别出系统支持的关键字、标识符、运算符、终结符等,并确定每个词固有的词性。 + +  应当想到,既然关键字、标识符、运算符、终结符等在系统看来是不同的东西,那么就一定预先定义好了词的类别,同时,还要有与之配套的操作,而这些都在词法文件 scan.l 中,所以本人对该文件做了解析: + +- [ 对scan.l的解析(一)](https://forum.gitlink.org.cn/forums/7430/detail " 对scan.l的解析(一)") +- [ 对scan.l的解析(二)](https://forum.gitlink.org.cn/forums/7433/detail " 对scan.l的解析(二)") + +不过,虽然运算符、终结符等这些词可以在 scan.l 中定义,但是关键字这一类并不是在这里定义的,它们是在 src/include/parser 目录下的 kwlist.h 中被定义的。而如何查找 SQL 语句中的关键字呢?这时就需要用到 ScanKeywordLookup() 函数,而这个函数在 kwlookup.cpp 中: + +- [ 对kwlookup.cpp的解析](https://forum.gitlink.org.cn/forums/7429/detail " 对kwlookup.cpp的解析") + +对于标识符,也需要能够对其进行预处理的函数,其中一些常用的就在 scansup.cpp 中: + +- [ 对scansup.cpp的解析(一)](https://forum.gitlink.org.cn/forums/7426/detail " 对scansup.cpp的解析(一)") +- [ 对scansup.cpp的解析(二)](https://forum.gitlink.org.cn/forums/7427/detail " 对scansup.cpp的解析(二)") + +## 语法分析 + +  同词法分析需要预先定义词法结构一样,也需要在语法分析前预先定义语法结构。由于词的组合形式会比词的种类多得多,所以用来定义语法结构的文件 gram.y 的体积会比 scan.l 的大了很多,我对 gram.y 做了一定的解析: + +- [ 对gram.y的解析(一)](https://forum.gitlink.org.cn/forums/7450/detail " 对gram.y的解析(一)") +- [ 对gram.y的解析(二)](https://forum.gitlink.org.cn/forums/7465/detail " 对gram.y的解析(二)") + +  而仅仅分析定义了语法结构的文件还不行,还需要理解语法分析的过程才能加深理解,所以本人对语法分析的入口函数 raw_parser() 所在的文件也进行了解析: + +- [对parser.cpp的解析](https://forum.gitlink.org.cn/forums/7470/detail "对parser.cpp的解析") + +  到这时,本人才对词法分析和语法分析的流程有了更深的理解,详见下图: + +![](/api/attachments/394947) + +首先,词法文件 scan.l 经 Flex 编译后就生成了 scan.cpp 文件,在这个文件中有我们所需要的词法分析器 core_yylex() ,实际上 core_yylex() 只能算是这个分析器的入口,因为在这个函数内部还调用了其它函数,并且还有着更复杂的调用关系。然后,语法文件 gram.y 经 Bison 编译后就生成了 gram.cpp 文件和 gram.h 头文件,其中,在 gram.cpp 文件中有着我们需要的语法分析器 base_yyparse() ,它还将一同生成的 gram.h 作为头文件。当这个语法分析器工作时,它需要多次调用词法分析器,而每次调用都能让它获得一个返回的 token ,就是这个 token 标示了该 SQL 语句中各个词的词性(关键字、标识符、常量等)。语法分析器利用这个 token ,可以进行一系列的操作,从无到有地构建起一棵原始的语法分析树,也被称为抽象语法树( AST )。之后,**词法分析和语法分析的入口函数 raw_parse() ,就会获取储存了这个语法分析树的链表**。 + +## 语义分析 + +  虽然通过词法分析和语法分析,确实构建起了一棵语法分析树,但是又如何保证这棵树代表的查询语句是有效的呢?以下面这条语句为例: + +- SELECT fir_col FROM sourcetable WHERE id>1; + +能不能确定 fir_col 是 FROM 子句中 sourcetable 的属性,能不能确定 sourcetable 是存在的,能不能确定 id 是 sourcetable 的属性并且它的值可与数字常量比较?凡此种种,都直接决定了这条 SQL 语句的有效性,决定了所构建出来的语法分析树的有效性,这才有了语义分析这一步。 + +  语义分析阶段会检查命令中是否有不符合语义规则的成分,主要作用是为了检查命令是否可以正确的执行。而语义分析的入口一般就是 parse_analyze() ,它所在的文件是 analyze.cpp ,连同该文件中定义的其他一些重要函数本人做了一些解析: + +- [ 对analyze.cpp的解析(一)](https://forum.gitlink.org.cn/forums/7471/detail " 对analyze.cpp的解析(一)") +- [ 对analyze.cpp的解析(二)](https://forum.gitlink.org.cn/forums/7486/detail " 对analyze.cpp的解析(二)") +- [对analyze.cpp的解析(三)](https://forum.gitlink.org.cn/forums/7493/detail "对analyze.cpp的解析(三)") + +  这时,在本人查找资料和翻看源码的过程中,本人又对整个语法分析树和查询树的转换流程有了更深的理解,详见下图: + +![](/api/attachments/394948) + +这张图片中,和第8、9步操作有关的 pg_rewrite_query() 函数属于查询重写这个模块,查询重写就是把用户输入的SQL语句转换为更高效的等价SQL,是基于规则的逻辑优化。由于现阶段我还没有涉及这方面的知识,所以这个日后再说。另外,这个函数所在文件为 postgres.cpp ,该文件又在 src/gausskernel/process/tcop 目录下,值得一看。pg_parse_query()、pg_analyze_and_rewrite() 和 exec_simple_query() 函数均在 postgres.cpp 文件中,我们先来看一下 pg_parse_query() 的部分源码: + +```cpp +//代码清单1 +//src/gausskernel/process/tcop/postgres.cpp +List* pg_parse_query(const char* query_string, List** query_string_locationlist) +{ + List* raw_parsetree_list = NULL; + PGSTAT_INIT_TIME_RECORD(); + TRACE_POSTGRESQL_QUERY_PARSE_START(query_string); + if (u_sess->attr.attr_common.log_parser_stats) + ResetUsage(); + PGSTAT_START_TIME_RECORD(); + List* (*parser_hook)(const char*, List**) = raw_parser; +······ + raw_parsetree_list = parser_hook(query_string, query_string_locationlist); + PGSTAT_END_TIME_RECORD(PARSE_TIME); + if (u_sess->attr.attr_common.log_parser_stats) + ShowUsage("PARSER STATISTICS"); +······ + TRACE_POSTGRESQL_QUERY_PARSE_DONE(query_string); + return raw_parsetree_list; +} +``` + +  可以看到,在代码清单1中 pg_parse_query() 的返回值是 List\* 类型的指针 raw_parsetree_list ,也可以认为是原始语法分析树链表,而得到 raw_parsetree_list 用到了 parser_hook() 函数,再往前看就发现 parser_hook() 其实就是 raw_parser() 函数,这就说明上图步骤2、3是合理的。还有一点,为什么返回的是原始语法分析树链表而不是原始语法分析树呢应为当用户在一个命令字符串中执行多个 SQL 命令,也就是说客户端提交给服务进程的字符串包含多个 SQL 命令时,比如: + +```cpp +//代码清单2 +CREATE TABLE newtable(id INTERGER,name VARCHAR(10)); +INSERT INTO newtable VALUES(7,'Luoge'); +SELECT id FROM newtable; +``` + +那么在接收到该字符串之后进行词法和语法分析的结果就是三个分析树:CreateStmt、InsertStmt和 SelectStmt ,在返回的 raw_parsetree_list 中就有三个 ListCell 结构体分别包含上述的这三个分析树。 + +  再来看一下 pg_analyze_and_rewrite() 的部分源码: + +```cpp +//代码清单3 +//src/gausskernel/process/tcop/postgres.cpp +List* pg_analyze_and_rewrite(Node* parsetree, const char* query_string, Oid* paramTypes, int numParams) +{ + Query* query = NULL; + List* querytree_list = NULL; + TRACE_POSTGRESQL_QUERY_REWRITE_START(query_string); + /* + * (1) Perform parse analysis. + */ + if (u_sess->attr.attr_common.log_parser_stats) + ResetUsage(); +······ + query = parse_analyze(parsetree, query_string, paramTypes, numParams); + + if (u_sess->attr.attr_common.log_parser_stats) + ShowUsage("PARSE ANALYSIS STATISTICS"); + /* + * (2) Rewrite the queries, as necessary + */ + querytree_list = pg_rewrite_query(query); +······ + TRACE_POSTGRESQL_QUERY_REWRITE_DONE(query_string); + return querytree_list; +} +``` + +可以很清晰地看到函数里的代码分为两部分,一部分用来做语义分析,另一部分用来做查询重写。关键的代码就是第14、21行,第14行是调用了 parse_analyze() 函数,用 Query\* 类型的指针接收返回的指向经语义分析得到的查询树的指针,第21行是利用 pg_rewrite_query() 得到重写后的查询树链表,用 List\* 类型的 querytree_list 接收,最后该函数返回该值并结束。 + +  最后看一下 exec_simple_query() 函数的部分源码: + +```cpp +//代码清单4 +//src/gausskernel/process/tcop/postgres.cpp +static void exec_simple_query(const char* query_string, MessageType messageType, StringInfo msg = NULL) +{ +······ + if (HYBRID_MESSAGE == messageType) { + parsetree_list = pg_parse_query(sql_query_string); + } else { + if (copy_need_to_be_reparse != NULL && g_instance.status == NoShutdown) { + bool reparse_query = false; + gs_stl::gs_string reparsed_query; + do { + parsetree_list = pg_parse_query(reparsed_query.empty() ? + query_string : reparsed_query.c_str(), &query_string_locationlist); + reparse_query = copy_need_to_be_reparse(parsetree_list, query_string, reparsed_query); + } while (reparse_query); + } else { + parsetree_list = pg_parse_query(query_string, &query_string_locationlist); + } + } +······ + /* + * @hdfs + * If we received a hybridmessage, we use sql_query_string to analyze and rewrite. + */ + if (HYBRID_MESSAGE != messageType) + querytree_list = pg_analyze_and_rewrite(parsetree, query_string, NULL, 0); + else + querytree_list = pg_analyze_and_rewrite(parsetree, sql_query_string, NULL, 0); +······ +} +``` + +## 总结 +  本人关于查询分析模块的解析博客到这就算是完结了,但是只学习一个模块是孤立的,是不成体系的,后续本人会继续去学习其它的模块。而利用解析查询分析模块的经验,本人相信这对于解析其它的模块会有很大的帮助。 \ No newline at end of file -- Gitee From b405e7d04e97b6d70212da289bd862fc2a49735b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=A0=BC?= Date: Wed, 23 Nov 2022 14:19:00 +0000 Subject: [PATCH 06/16] =?UTF-8?q?add=20TestTasks/geyouguang/=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E6=89=A7=E8=A1=8C=E6=A8=A1=E5=9D=97=E6=80=BB=E7=BB=93?= =?UTF-8?q?.md.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 罗格 --- ...41\345\235\227\346\200\273\347\273\223.md" | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 "TestTasks/geyouguang/\346\237\245\350\257\242\346\211\247\350\241\214\346\250\241\345\235\227\346\200\273\347\273\223.md" diff --git "a/TestTasks/geyouguang/\346\237\245\350\257\242\346\211\247\350\241\214\346\250\241\345\235\227\346\200\273\347\273\223.md" "b/TestTasks/geyouguang/\346\237\245\350\257\242\346\211\247\350\241\214\346\250\241\345\235\227\346\200\273\347\273\223.md" new file mode 100644 index 0000000..182edef --- /dev/null +++ "b/TestTasks/geyouguang/\346\237\245\350\257\242\346\211\247\350\241\214\346\250\241\345\235\227\346\200\273\347\273\223.md" @@ -0,0 +1,63 @@ +# 查询执行模块总结 + +## 概要 + +  离总结本人对查询分析模块的认识,已经过去了半个月多一点,在这段时间里,本人也利用了解查询分析模块的方法对查询执行模块有了更多的了解。 + +  对查询执行模块印象最深的,还是它对于执行策略的选择,以及将计划树计划转变为状态树的过程,这两步都是很有意义的步骤。前一步确定了将计划树转交给 Executor 模块还是 ProcessUtility 模块,后一步在 Plan 模块中起着重要作用,这一步既是在这个模块中实现的,也推动了物理执行计划的执行。 + +  查询执行模块内部的结构主体是这样: + +![](https://img-blog.csdnimg.cn/b1759608d17a4eb2848cb9670faee3fe.png) + +如你所见,这些个模块的主调函数还是 exec_simple_query(),而查询分析模块的主调函数同样也是这个。 + +## Portal模块 + +  查询执行模块的第一个子模块便是 Portal 模块,它起到了决策的作用。当查询执行的前一个模块即查询编译模块输出执行计划时,Portal 模块会决定将这个执行计划传递给 ProcessUtility 模块还是 Executor 模块,因此,可以认为 Portal 模块就是一个选择器。当要执行的语句属于可优化语句,查询编译模块会为其生成一个或多个计划树,Portal 模块将会选择 Executor 模块来执行这些计划树操作;若属于数据定义语句时,就交给 ProcessUtility 模块。针对这些流程,本人写了以下几篇博客: + +- [ 对pquery.cpp的解析(一)](https://forum.gitlink.org.cn/forums/7752/detail " 对pquery.cpp的解析(一)") + +- [ 对pquery.cpp的解析(二)](https://forum.gitlink.org.cn/forums/7766/detail " 对pquery.cpp的解析(二)") + +- [对portalmem.cpp的解析](https://forum.gitlink.org.cn/forums/7729/detail "对portalmem.cpp的解析") + +  在这几篇博客中,本人分析了 Portal 模块的主要函数即 PortalStart()、PortalRun() 以及 PortalDrop() ,最值得留意的是 PortalRun() ,由于函数之间的调用关系虽有层次但也复杂,所以本人在博客中梳理了一下: + +![](https://img-blog.csdnimg.cn/2702afc2f84749bb99c06785f2450b0e.png) + +总而言之,本人还是建议新手去看一下本人写的这篇博客。那么,妥妥地,我接着总结。 + +## Executor模块和Plan模块 + +  在此之后,本人便开始解析 Executor 模块,这个模块多执行增删改查语句,说到底是对元组的操作,比如说 INSERT 语句要将一个或多个元组存入特定表,DELETE 语句要从特定表中删除一个或多个元组,UPDATE 语句要更改符合 WHERE 子句条件的元组的一个或多个数据项,而 SELECT 语句则是要挑选符合 WHERE 子句条件的元组出来,这么说,大家可能就理解了。针对 Executor 模块,本人写了以下几篇博客谈了自己的理解: + +- [ 对execMain.cpp的解析(一)](https://forum.gitlink.org.cn/forums/7730/detail " 对execMain.cpp的解析(一)") + +- [ 对execMain.cpp的解析(二)](https://forum.gitlink.org.cn/forums/7751/detail " 对execMain.cpp的解析(二)") + +这两篇博客涉及到了该模块下主要的函数 ExecutorStart()、ExecutorRun() 以及 ExecutorEnd(),还有其它一些发挥了重要作用的记录执行状态的结构体比如 EState 结构体。然后继 Executor 模块之后,就自然而然地过渡到了 Plan 模块,这个模块用来对执行计划作进一步的细化。对此,本人写了一篇博客分析: + +- [对execMain.cpp的解析(三)](https://forum.gitlink.org.cn/forums/7779/detail "对execMain.cpp的解析(三)") + +另外,本人还需要说明一点,Executor 模块和 Plan 模块并不是相互独立的模块,事实上,Executor 模块中的 standard_ExecutorStart()、standard_ExecutorRun()、standard_ExecutorEnd() 分别调用了 Plan 模块的 InitPlan()、ExecutePlan()、ExecEndPlan(),简而言之,就是包含关系,前者包含了后者。上图中的各个模块的前后关系其实是包含关系,这样画只是为了方便叙述。 + +## Node模块 + +  现在总结一下本人对 Node 模块的理解,就像前面所说的,Node 模块也是融合在 Plan 模块中的。先用 ExecInitNode() 来将前一个模块传送过来的计划树转化为计划状态树,是从根节点开始按先遍历顺序一个个转化的,此后得到的计划状态树中是有一个指向存储了目标元组的特定内存区域的指针的,即 TupleDesc 结构体指针类型的 scandesc 变量。然后便是 ExecProcNode() 以中序遍历的顺序去执行状态节点,并根据节点的类型去调用不同的执行函数。还需要留意到的是,ExecProcNode() 每次只能从计划状态树获取一个元组,然而目标元组往往不止一条,该怎么办?大家还是到本人的这篇博客以及前面关于 Plan 模块的博客里去找答案吧: + +- [对execProcnode.cpp的解析](https://forum.gitlink.org.cn/forums/7780/detail "对execProcnode.cpp的解析") + +  还是在这里说了吧,首先,正常情况下 Plan 模块的 ExecutePlan() 会在自身的大循环中不断地调用 ExecProcNode(),直到得到的并处理后的元组的个数达到了上限,也就是处理完了所有的数组,这时候就可以结束循环了。 + +## ProcessUtility模块 + +  这个模块就是用来处理数据定义语句的了,最终要调用的是 standard_ProcessUtility() 函数,本人用一篇博客简要地分析了一下: + +- [对utility.cpp的解析](https://forum.gitlink.org.cn/forums/7767/detail "对utility.cpp的解析") + +这个模块处理的还是语法分析树,而不是计划树,换句话说,该模块处理的语句本身是不可优化的了,比如建表语句 CREATE TABLE 等。 + +## 总结 + +  总地来说,查询执行模块内部各个子模块间是有机联系在一起的,对于整体而言,它前接查询编译模块,得到计划树或是语法分析树,后接存储模块,可能和它会有动态的数据交换。另外,计划树和计划状态树由四类节点组成:Control node、Scan node、Join node、Materialization node,本人虽然对此没有进行分析,但是就整体而言,它们代表着更加细化的操作。 \ No newline at end of file -- Gitee From b85299a24f5c5cfa9fdcc6cc0f07006fc5d30d5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=A0=BC?= Date: Wed, 23 Nov 2022 14:19:51 +0000 Subject: [PATCH 07/16] =?UTF-8?q?add=20TestTasks/geyouguang/=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E9=87=8D=E5=86=99=E6=A8=A1=E5=9D=97=E6=80=BB=E7=BB=93?= =?UTF-8?q?.md.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 罗格 --- ...41\345\235\227\346\200\273\347\273\223.md" | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 "TestTasks/geyouguang/\346\237\245\350\257\242\351\207\215\345\206\231\346\250\241\345\235\227\346\200\273\347\273\223.md" diff --git "a/TestTasks/geyouguang/\346\237\245\350\257\242\351\207\215\345\206\231\346\250\241\345\235\227\346\200\273\347\273\223.md" "b/TestTasks/geyouguang/\346\237\245\350\257\242\351\207\215\345\206\231\346\250\241\345\235\227\346\200\273\347\273\223.md" new file mode 100644 index 0000000..2c8535d --- /dev/null +++ "b/TestTasks/geyouguang/\346\237\245\350\257\242\351\207\215\345\206\231\346\250\241\345\235\227\346\200\273\347\273\223.md" @@ -0,0 +1,19 @@ +# 查询重写模块总结 + +  查询重写模块前接查询分析模块,后接查询优化模块。对于从查询分析模块传递过来的原始查询树链表,查询重写模块会一个个地分析。对于单个查询树,它先判断这个查询树是由功能性语句还是由查询语句构造而来,在确定了语句类型后,又会有细分的操作分支。 + +  pg_rewrite_query() 函数体现了这个功能,这个函数也是查询重写模块的入口函数,对于这个函数,本人写了一篇博客进行介绍: + +- [对postgres.cpp的解析](https://blog.csdn.net/m0_60340015/article/details/127318103 "对postgres.cpp的解析") + +在这个函数中,被用来实现重写功能的函数主要是 QueryRewrite(),而当查询树不被重写时, pg_rewrite_query() 函数就会调用 list_make1() 来对查询树作进一步的处理,对于这两个函数,我也做了解析: + +- [对rewriteHandler.cpp的解析(一)](https://blog.csdn.net/m0_60340015/article/details/127329641?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22127329641%22%2C%22source%22%3A%22m0_60340015%22%7D "对rewriteHandler.cpp的解析(一)") + +说到 QueryRewrite() 函数,它用三步来处理一个可重写的查询树,首先它用 RewriteQuery() 只对由 INSERT/DELETE/UPDATE 语句构造而来的查询树进行重写,之后使用 fireRIRrules() 对由 SELECT 语句构造而来的查询树进行重写,最后主要是将查询重写后的查询树链表返回。 + +  之后,本人又对 RewriteQuery() 和 fireRIRrules() 进行了解析: + +- [对rewriteHandler.cpp的解析(二)](https://blog.csdn.net/m0_60340015/article/details/127334120 "对rewriteHandler.cpp的解析(二)") + +RewriteQuery() 中,首先是对 CTE 的重写,然后利用范围表对结果进一步处理。fireRIRrules() 中,先用 while 循环来遍历该查询树的所有范围表,每通过 rt_fetch() 获得一个范围表的地址,就判断它的类型,根据它的类型进行相应操作。之后,打开范围表获取关系,再根据关系获取作用于这些关系的规则,应用这些规则去重写查询树,如果存在 WITH 子句,调用 fireRIRrules() 对 WITH 子句进行重写操作,如果查询树有子链接,那么对它们进行遍历,并使用 fireRIRonSubLink() 函数进行重写处理,最终也是调用 fireRIRrules() 函数来完成重写工作。 \ No newline at end of file -- Gitee From f6df4edf06480904060e7bafcfc25a4f674b49c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=A0=BC?= Date: Wed, 23 Nov 2022 14:21:48 +0000 Subject: [PATCH 08/16] =?UTF-8?q?add=20TestTasks/geyouguang/=E5=AF=B9gram.?= =?UTF-8?q?y=E7=9A=84=E8=A7=A3=E6=9E=90(=E4=B8=80).md.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 罗格 --- ...\350\247\243\346\236\220(\344\270\200).md" | 339 ++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 "TestTasks/geyouguang/\345\257\271gram.y\347\232\204\350\247\243\346\236\220(\344\270\200).md" diff --git "a/TestTasks/geyouguang/\345\257\271gram.y\347\232\204\350\247\243\346\236\220(\344\270\200).md" "b/TestTasks/geyouguang/\345\257\271gram.y\347\232\204\350\247\243\346\236\220(\344\270\200).md" new file mode 100644 index 0000000..6385c1e --- /dev/null +++ "b/TestTasks/geyouguang/\345\257\271gram.y\347\232\204\350\247\243\346\236\220(\344\270\200).md" @@ -0,0 +1,339 @@ +# 对gram.y的解析(一) + +## 源码链接 + +https://www.gitlink.org.cn/Eao3piq4e/openGauss-server/tree/master/src%2Fcommon%2Fbackend%2Fparser%2Fgram.y + +## 概述 + +  该文件在 src/common/backend/parser 目录下,我们在这个文件中定义了语法结构。在计算机科学里面,Bison 是一个语法分析器产生程序( parser generator ),具体一点那就可以说是是 GNU 版的 Yacc 。而 gram.y 作为 Bison 的输入文件,最终产生了 gram.cpp 和 gram.h ,这篇博客我来分析一下 gram.y 文件的结构。 + +## 解析 +  gram.y 和 scan.l 的结构是相同的,它同样被顶行的 %% 分为3个部分: + +- 定义段 +- %% +- 规则段 +- %% +- C代码段 + +  接下来我分别讲一下3个部分的结构,可以和 scan.l 的结构参照起来一起看:https://forum.gitlink.org.cn/forums/7430/detail + +#### 定义段 + +  往下又分为代码部分和声明部分,其中代码部分用 %{...%} 或 %code{...} 括起来,这些代码都是C代码,经过 Bison 编译后会被复制 gram.cpp 中,我们可以在其中定义一些函数声明、结构体类型等,以及包含一些必要的头文件,该部分内容在 gram.y 中的范围为1~255行,例如: + +```cpp +//代码清单1 +%{ +#include "postgres.h" +#include "knl/knl_variable.h" +#include +#include +#include "catalog/index.h" +...... +/* Private struct for the result of privilege_target production */ +typedef struct PrivTarget +{ + GrantTargetType targtype; + GrantObjectType objtype; + List *objs; +} PrivTarget; +...... +/* ConstraintAttributeSpec yields an integer bitmask of these flags: */ +#define CAS_NOT_DEFERRABLE 0x01 +#define CAS_DEFERRABLE 0x02 +#define CAS_INITIALLY_IMMEDIATE 0x04 +#define CAS_INITIALLY_DEFERRED 0x08 +#define CAS_NOT_VALID 0x10 +#define CAS_NO_INHERIT 0x20 +...... +static void base_yyerror(YYLTYPE *yylloc, core_yyscan_t yyscanner, + const char *msg); +static Node *makeColumnRef(char *colname, List *indirection, + int location, core_yyscan_t yyscanner); +static Node *makeTypeCast(Node *arg, TypeName *typname, int location); +...... +%} +``` + +而声明部分的内容显得更加丰富,它先以这些内容开头,范围为257~263行: + +```cpp +//代码清单2 +%define api.pure /*指定希望解析器是可重入的*/ +%expect 0 /*希望冲突数量为0*/ +%name-prefix "base_yy" /*代表生成的函数和变量名的前缀从yy改成base_yy*/ +%locations /*生成处理位置的代码。语法使用特殊的'@n'标记后便会启用此模式,但是如果你的语法不使用它,则使用'%locations'可以获取更准确的语法错误消息*/ +%parse-param {core_yyscan_t yyscanner} /*声明一个或多个参数是其它yyparse参数。在声明函数或原型时使用参数声明,参数声明中的最后一个标识符必须是该参数名称*/ +%lex-param {core_yyscan_t yyscanner} /*指定参数声明是其他yylex参数声明,你可以传递一个或多个这样的声明,等效于重复%lex-param*/ +``` + +%union 用来声明标识符号值的所有可能C类型,它后面的花括号里定义的共用体,在265~313行: + +```cpp +//代码清单3 +%union +{ + core_YYSTYPE core_yystype; + /* these fields must match core_YYSTYPE: */ + int ival; + char *str; + const char *keyword; + ......//省略 +/* PGXC_BEGIN */ + DistributeBy *distby; + PGXCSubCluster *subclus; +/* PGXC_END */ + ForeignPartState *foreignpartby; + MergeWhenClause *mergewhen; + UpsertClause *upsert; + EncryptionType algtype; + LockClauseStrength lockstrength; +} +``` + +这里的共用体最终会被用来定义一个叫 yylval 的整型全局变量,可以用来在 Flex 和 Bison 之间传递变量,以实现它们之间的通信。而 这个 yylval 的定义最终会被放到 scan.h 中去,并将 scan.h 作为 Flex 的除 scan.l 外的另外一个输入文件。 + +%type 开始的行是说明非终结符语义值的类型,包括315~761行,每个以 %type 开始的行的标准格式是: + +- %type < tp > symbol[sym1 sym2 ... symn] + +定义每个 symbol 为 tp 类型,以消除歧义。如果此项存在,Bison 将执行类型检查,否则假定其后所有的符号为整数类型。截取一段源码: + +```cpp +//代码清单4 +%type opt_type +%type foreign_server_version opt_foreign_server_version +%type data_source_version opt_data_source_version data_source_type opt_data_source_type +%type auth_ident +%type opt_in_database opt_rename + +%type OptSchemaName +%type OptSchemaEltList +%type OptBlockchainWith OptAlterToBlockchain + +%type TriggerForSpec TriggerForType ForeignTblWritable +%type TriggerActionTime +%type TriggerEvents TriggerOneEvent +%type TriggerFuncArg +%type TriggerWhen +``` + +%token 用于定义文法中使用了的标识符,包括763~901行,格式一般为: + +- %token [] token[tok1 tok2 ...tokn] + +%token 定义文法中使用了的终结符,其中 tok1、tok2 等都是终结符,一般大写。在 Yacc 源程序语法规则部分出现的所有终结符(文字字符 literal 除外)必须在这部分定义,附一段源码: + +```cpp +//代码清单5 +%token TYPECAST ORA_JOINOP DOT_DOT COLON_EQUALS PARA_EQUALS + +%token ABORT_P ABSOLUTE_P ACCESS ACCOUNT ACTION ADD_P ADMIN AFTER + AGGREGATE ALGORITHM ALL ALSO ALTER ALWAYS ANALYSE ANALYZE AND ANY APP APPEND ARCHIVE ARRAY AS ASC + ASSERTION ASSIGNMENT ASYMMETRIC AT ATTRIBUTE AUDIT AUTHID AUTHORIZATION AUTOEXTEND AUTOMAPPED +······ +``` + +%left、%right、%nonassoc 也是定义文法中使用的终结符,定义形式与 %token 类似,范围为903~973行,但是他们定义的终结符具有某种优先级和结合性,%left 表示左结合声明,%right 表示右结合声明,%nonassoc 表示不可结合声明(即它定义的终结符不能连续出现:例如 '<' ,如果文法中不允许出现形如 a' CmpOp +%nonassoc LIKE ILIKE SIMILAR +%nonassoc ESCAPE +%nonassoc OVERLAPS +%nonassoc BETWEEN +%nonassoc IN_P +%left POSTFIXOP /* dummy for postfix Op rules */ +%nonassoc UNBOUNDED /* ideally should have same precedence as IDENT */ +%nonassoc IDENT GENERATED NULL_P PARTITION SUBPARTITION RANGE ROWS PRECEDING FOLLOWING CUBE ROLLUP +%left Op OPERATOR '@' /* multi-character ops and user-defined operators */ +%nonassoc NOTNULL +%nonassoc ISNULL +%nonassoc IS /* sets precedence for IS NULL, etc */ +%left '+' '-' +%left '*' '/' '%' +%left '^' +%left AT /* sets precedence for AT TIME ZONE */ +%left COLLATE +%right UMINUS BY NAME_P PASSING ROW TYPE_P VALUE_P +%left '[' ']' +%left '(' ')' +%left TYPECAST +%left '.' +%left JOIN CROSS LEFT FULL RIGHT INNER_P NATURAL ENCRYPTED +%right PRESERVE STRIP_P +``` + +那么从 PARTIAL_EMPTY_PREC 运算符开始,越往后的运算符优先级越高,当一个表达式中出现位置更靠后的运算符时,那么先对含有后面运算符的表达式进行计算。假设有一条 SQL 语句: + +```cpp +//代码清单7 +SELECT * FROM tab1 CROSS JOIN tab2 WHERE tab1.kind = 'Apple' AND tab2.native = 'JiangXi'; +``` + +其中 tab1 为: + +| kind | price | +| :----: | :---: | +| Apple | 5 | +| Banana | 3 | + +tab2 为: + +| discount | native | +| :------: | :-----: | +| 0.2 | JiangXi | +| 0.1 | FuJian | + +已知代码清单7中有两个表达式,分别是 tab1 CROSS JOIN tab2 和 tab1.kind = 'Apple' AND tab2.native = 'JiangXi' 。在第一个表达式中,CROSS 和 JOIN 是同等优先级的左结合性终结符,同时这个表达式中再没有其它的终结符,所以它们共同执行为表 tab1 和表 tab2 作笛卡儿积的功能。不妨将由该表达式得到的表称为表 tab3: + +| tab1.kind | tab1.price | tab2.discount | tab2.native | +| :-------: | :--------: | :-----------: | :---------: | +| Apple | 5 | 0.2 | JiangXi | +| Apple | 5 | 0.1 | FuJian | +| Banana | 3 | 0.2 | JiangXi | +| Banana | 3 | 0.1 | FuJian | + +在第二个表达式中,我们可以看到的终结符有 '.' 、'=' 、AND,其中 '.' 终结符的优先级最高,我们由此最先得到 tab1.kind 和 tab2.native 的值,即分别得到了表 tab1 中 kind 列和 表 tab2 中 native 列的值。而 '=' 的优先级次之,所以会分别从 kind 列中匹配名为 'Apple' 的行、从 native 列中匹配名为 'JianXi' 的行。而 AND 是这三个中优先级最低的,具有左结合性,所以又会先从满足tab1.kind = 'Apple' 的行中去挑满足 tab2.native = 'JiangXi' 的,最终这条语句执行的结果就是这样的: + +| tab1.kind | tab1.price | tab2.discount | tab2.native | +| :-------: | :--------: | :-----------: | :---------: | +| Apple | 5 | 0.2 | JiangXi | + +#### 规则段 + +  该部分内容包括976~24440行。gram.y 的规则段要比 scan.l 的复杂得多,主要体现在 scan.l 只是定义了词法结构,而在 gram.y 中定义了语法结构,可以预想,正是不同性质的词在特定规则排序下才形成了语法,那么语法的种类将比词法的种类多数倍。首先我们需要先了解一下显示指定优先级和隐式指定优先级的区别,例如,我们在规则段可以怎样指定 '+'、'-'、'*'、'/' 的优先级: + +```cpp +//代码清单8 +expression: + expression '+' mulexp { + $$ = $1 + $3; + } + |expression '-' mulexp { + $$ = $1 - $3; + } + |mulexp { + $$ = $1; + } + +mulexp: + NUMBER '*' NUMBER { + $$ = $1 * $3; + } + |NUMBER '/' NUMBER { + $$ = $1 / $3; + } + |NUMBER { + $$ = $1; + } +``` + +  其中,代码清单8中使用的是隐式指定优先级的方法,下面这个采用的是隐式指定优先级的方法,我们再对这两种方式做进一步的解读。 + +```cpp +//代码清单9 +%left '-' '+' +%left '*' '/' +...... +%% +expression: + expression '*' expression { + $$ = $1 * $3; + } + |expression '/' expression { + $$ = $1 / $3; + } + |expression '+' expression { + $$ = $1 + $3; + } + |expression '-' expression { + $$ = $1 - $3; + } + |NUMBER { + $$ = $1; + } +``` + +可以看到,不论是代码清单8还是代码清单9,花括号里都出现了 $$ 、$1、$2 的符号,这涉及到产生式的概念,例如,我们可以从代码清单8中取出所有的子产生式分析一下: + +- 1. expression: expression '+' mulexp {$$ = $1 + $3;}; +- 2. expression: expression '-' mulexp {$$ = $1 - $3;}; + +- 4. expression: mulexp {$$ = $1;}; + +- 6. mulexp: NUMBER '*' NUMBER {$$ = $1 * $3;}; +- 7. mulexp: NUMBER '/' NUMBER {$$ = $1 / $3;}; +- 8. mulexp: NUMBER {$$ = $1;}; + +在这6个产生式中,NUMBER 是最小的语素,且每个子产生式具有很相似的结构,以第一个产生式为例,冒号 ':' 是产生符号,它左边的 expression 是一个非终结符,称为这个产生式的左部,其属性值为 $$,它右边则是多个文法符号,expression 的属性值是 $1,'+' 的为 $2,mulexp 的为$3,如果后面还有,那就以此类推。子产生式最后花括号里的内容正是对这些属性值的操作,以第一个子产生式为例,归约 expression '+' mulexp 为 expression 时,将会做如下操作:从属性值栈中取出产生式右部 expression 的属性值 $1 和 mulexp 的属性值 $3 ,**将两者的和存入属性值栈中对应产生式左部的 expression 的属性值的位置 $$** 。 + +  现在我们可以来了解一下隐式指定优先级和显示指定优先级的区别了,首先,显示指定优先级是需要在定义段中用 %left、%right、%nonassoc 来声明运算符的结合性和优先级的,之后才能在规则段中定义这个运算符的运算语法规则,但隐式指定优先级不需要,不过它需要定义更小的语法规则以表明优先级,例如在daimaqingdan8中除 expression 外还定义了更小的规则 mulexp ,编译器会先规约这个更小的规则,换句话说,优先级就显现出来了。 + +  在此,附上一段源码: + +```cpp +//代码清单10 +stmt : + AlterAppWorkloadGroupMappingStmt + | AlterCoordinatorStmt + | AlterDatabaseStmt + | AlterDatabaseSetStmt + | AlterDataSourceStmt + | AlterDefaultPrivilegesStmt + | AlterDomainStmt +······ + | SnapshotStmt + | TransactionStmt + | TruncateStmt + | UnlistenStmt + | UpdateStmt + | VacuumStmt + | VariableResetStmt + | VariableSetStmt + | VariableShowStmt + | VerifyStmt + | ViewStmt + | /*EMPTY*/ + { $$ = NULL; } + ; +``` + +这个规则代表的是语句的种类以及各自对应的执行操作,而更加细致的东西还是建议直接看源码,实在是太多了,就不细说了。 + +## C代码段 + +  该部分内容包括24444~26445行。该部分的函数还是十分多的,但没有 main() 函数,经查资料我发现是使用了 Unix/Linux 的 Yacc库中提供的 main() 。库里的 main() 如下: + +```cpp +//代码清单11 +main() { +return(yyparse()); +} +``` + +该部分的函数大多数是为了语法解析,或者说是在解析发生一个错误时能够抛出异常的原因。 + +## 总结 +  gram.y 的结构和 scan.l 的结构还是很相似的,但两者的文件体积相差十分大,前者是700KB左右,后者却只有50KB,而 gram.y 文件中占据部分最大的还是内部的规则段。在这一部分规约了很多的产生式,当语法解析器碰到相同的语法结构时,就会触发这些产生式对应的语法动作,这样就完成了语法解析的功能,最终生成了一棵语法树,虽然思路简单但是这个过程还是很复杂的。 \ No newline at end of file -- Gitee From b12f331e84413fdb7fec144b041b17869064afc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=A0=BC?= Date: Wed, 23 Nov 2022 14:22:58 +0000 Subject: [PATCH 09/16] =?UTF-8?q?add=20TestTasks/geyouguang/=E5=AF=B9gram.?= =?UTF-8?q?y=E7=9A=84=E8=A7=A3=E6=9E=90(=E4=BA=8C).md.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 罗格 --- ...\350\247\243\346\236\220(\344\272\214).md" | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 "TestTasks/geyouguang/\345\257\271gram.y\347\232\204\350\247\243\346\236\220(\344\272\214).md" diff --git "a/TestTasks/geyouguang/\345\257\271gram.y\347\232\204\350\247\243\346\236\220(\344\272\214).md" "b/TestTasks/geyouguang/\345\257\271gram.y\347\232\204\350\247\243\346\236\220(\344\272\214).md" new file mode 100644 index 0000000..915a49e --- /dev/null +++ "b/TestTasks/geyouguang/\345\257\271gram.y\347\232\204\350\247\243\346\236\220(\344\272\214).md" @@ -0,0 +1,111 @@ +## 源码链接 + +https://www.gitlink.org.cn/Eao3piq4e/openGauss-server/tree/master/src%2Fcommon%2Fbackend%2Fparser%2Fgram.y + +## 概述 + +  接上一篇博客[对gram.y的解析(一)](https://forum.gitlink.org.cn/forums/7450/detail "对gram.y的解析(一)"),这篇博客我来介绍一下C代码段中一些比较重要的函数。 + +## 函数 + +#### base_yyerror() + +函数原型为: + +```cpp +//代码清单1 +//src/common/backend/parser/gram.y +static void base_yyerror(YYLTYPE *yylloc, core_yyscan_t yyscanner, const char *msg) +``` + +  Bison 需要此函数的反馈,同时我们忽略传递的 yylloc ,而是使用扫描仪上可用的最后一个令牌位置。 + +#### makeColumnRef() + +```cpp +//代码清单2 +//src/common/backend/parser/gram.y +static Node *makeColumnRef(char *colname, List *indirection,int location, core_yyscan_t yyscanner) +``` + +  该函数用来生成 ColumnRef 节点,如果指定的间接列表中有任何订阅,则添加 A_Indirection 节点。同时,必须将间接列表开头的任何字段选择转换为 ColumnRef 节点的字段部分。 + +#### makeBoolAConst() + +```cpp +//代码清单3 +//src/common/backend/parser/gram.y +Node *makeBoolAConst(bool state, int location) + +``` + +  该函数用来创建一个 A_Const 字符串节点,并将其放入布尔转换中。 + +#### check_qualified_name() + +```cpp +//代码清单4 +//src/common/backend/parser/gram.y +static void check_qualified_name(List *names, core_yyscan_t yyscanner) +``` + +  该函数用来检查 qualified_name 生成的结果,最简单的方法是让 qualified_name 的语法中允许下标和 '\*',然后我们在这里禁止这样做。 + +#### check_func_name() + +```cpp +//代码清单5 +//src/common/backend/parser/gram.y +static List *check_func_name(List *names, core_yyscan_t yyscanner) +``` + +  该函数用来检查 func_name 生成的结果,最简单的方法是让 func_name 的语法中允许下标和 '\*',在这里禁止这样使用。 + +#### insertSelectOptions() + +```cpp +//代码清单6 +//src/common/backend/parser/gram.y +static void insertSelectOptions(SelectStmt *stmt, + List *sortClause, List *lockingClause, + Node *limitOffset, Node *limitCount, + WithClause *withClause, + core_yyscan_t yyscanner) +``` + +  该函数用来将 ORDER BY 等插入已构建的 SelectStmt,此函数只是为了避免在已构建的 SelectStmt 中重复代码。 + +#### doNegate() + +```cpp +//代码清单7 +//src/common/backend/parser/gram.y +static Node *doNegate(Node *n, int location) +``` + +  该函数用来处理数值常量的求反,之前在这里这样做是因为优化器无法处理看起来像 "var=-4" 的索引,它需要 "var=const" ,并且应用于常量的一元减运算不符合条件。从 Postgres7.0( openGauss 由 Postgres 发展而来) 开始,这个问题不再存在,因为优化器中有一个常量子表达式简化器。然而,在这里仍然这样做有一个很好的理由,那就是我们可以推迟对简单负常量的特定内部表示的提交。我们最好将像 "-123.456" 这样的负常量保留为字符串形式,直到我们知道所需的类型。 + +#### makeRangeVarFromAnyName() + +```cpp +//代码清单8 +//src/common/backend/parser/gram.y +static RangeVar *makeRangeVarFromAnyName(List *names, int position, core_yyscan_t yyscanner) +``` + +  将(带虚线的)名称列表转换为 RangeVar (类似 makeRangeVarFromNameList ,但可以定位)。"AnyName" 指语法中的 any_name 产物。 + +#### processCASbits() + +```cpp +//代码清单9 +//src/common/backend/parser/gram.y +static void processCASbits(int cas_bits, int location, const char *constrType, + bool *deferrable, bool *initdeferred, bool *not_valid, + bool *no_inherit, core_yyscan_t yyscanner) +``` + +  该函数用来处理 ConstraintAttributeSpec 的结果,并在输出命令节点中设置适当的 bool 标志,为不支持特定命令的任何标志传递 NULL 。 + +## 总结 +  这些函数在生成语法树的过程中发挥了很大的作用。 \ No newline at end of file -- Gitee From d9f91107d08a64b6ce9b4d66df50ed7ea63f177d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=A0=BC?= Date: Wed, 23 Nov 2022 14:23:49 +0000 Subject: [PATCH 10/16] =?UTF-8?q?add=20TestTasks/geyouguang/=E5=AF=B9kwloo?= =?UTF-8?q?kup.cpp=E7=9A=84=E8=A7=A3=E6=9E=90.md.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 罗格 --- ...pp\347\232\204\350\247\243\346\236\220.md" | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 "TestTasks/geyouguang/\345\257\271kwlookup.cpp\347\232\204\350\247\243\346\236\220.md" diff --git "a/TestTasks/geyouguang/\345\257\271kwlookup.cpp\347\232\204\350\247\243\346\236\220.md" "b/TestTasks/geyouguang/\345\257\271kwlookup.cpp\347\232\204\350\247\243\346\236\220.md" new file mode 100644 index 0000000..77b7716 --- /dev/null +++ "b/TestTasks/geyouguang/\345\257\271kwlookup.cpp\347\232\204\350\247\243\346\236\220.md" @@ -0,0 +1,146 @@ +# 对kwlookup.cpp的解析 + +## 源码链接 + +https://www.gitlink.org.cn/Eao3piq4e/openGauss-server/tree/master/src%2Finclude%2Fparser%2Fkeywords.h + +## 概述 + +  该cpp文件中仅有 ScanKeywordLookup() 这一个函数,但却是在词法分析中使用最频繁的一个函数,在将一个SQL语句划分为多个 token 的过程中起着至关重要的作用。 + +## 源码 + +```cpp +//代码清单1 +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); + /* We assume all keywords are shorter than NAMEDATALEN. */ + if (len >= NAMEDATALEN) { + return NULL; + } + + /* + * Apply an ASCII-only downcasing. We must not use tolower() since it may + * produce the wrong translation in some locales (eg, Turkish). + */ + for (i = 0; i < len; i++) { + char ch = text[i]; + + if (ch >= 'A' && ch <= 'Z') { + ch += 'a' - 'A'; + } + word[i] = ch; + } + word[len] = '\0'; + + /* + * Now do a binary search using plain strcmp() comparison. + */ + 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; + } else if (difference < 0) { + low = middle + 1; + } else { + high = middle - 1; + } + } + + return NULL; +} +``` + +## 解析 + +  可以看到,ScanKeywordLookup() 有三个参数,分别为 text、keywords、num_keywords。第一个参数 text 是一个常量指针,指向一个存储了关键字或标识符的字符串;第二个参数 keywords 是一个 ScanKeyword* 类型的常量指针,指向一个储存了包括所有系统支持的关键字信息在内的结构体数组;第三个参数 num_keywords 正是这个结构体数组所含有的元素个数。 + +  在代码清单1第5行,定义了一个字符数组,当把 text[] 数组中的英文字母字符均变为其小写形式后,用它来存储这个新的字符串,第23~31行的代码执行的作用正是这个: + +```cpp +//代码清单2 +for (i = 0; i < len; i++) { + char ch = text[i]; + + if (ch >= 'A' && ch <= 'Z') { + ch += 'a' - 'A'; + } + word[i] = ch; + } + word[len] = '\0'; +``` + +  而正如我们在这篇博客中提到的: + +https://blog.csdn.net/m0_60340015/article/details/126395890 + +由于某些地区的语言体系不同,所以不能直接用 tolower() 函数来将单个字符转化为其对应小写形式。 + +  代码清单1第36\~51行代码,执行了在给定的有序的关键字数组中二分匹配关键字的功能。在第36和37行,分别对应数组的第一个和最后一个元素。第42行不断地缩小一般的范围,然后根据 strcmp() 的返回值确定返回值。 + +  ScanKeyword 结构体如下,所在文件的路径为: +src/include/parser/keywords.h + +```cpp +//代码清单3 +typedef struct ScanKeyword { + const char* name; /* in lower case */ + int16 value; /* grammar's token code */ + int16 category; /* see codes above */ +} ScanKeyword; +``` + +在这个文件中我们也可以看到,关键字是分类别的,它们应当能够与 gram.y 中的列表匹配: + +```cpp +//代码清单4 +#define UNRESERVED_KEYWORD 0 +#define COL_NAME_KEYWORD 1 +#define TYPE_FUNC_NAME_KEYWORD 2 +#define RESERVED_KEYWORD 3 +``` + +  而所有的关键字又按照字典序被排列,所在文件的路径为:src/include/parser/kwlist.h,当匹配成功后,该函数就会返回当前标识符指向单词表中对应单词的指针。 + +```cpp +//代码清单5 +/* name, token-value, category */ +PG_KEYWORD("abort", ABORT_P, UNRESERVED_KEYWORD) +PG_KEYWORD("absolute", ABSOLUTE_P, UNRESERVED_KEYWORD) +PG_KEYWORD("access", ACCESS, UNRESERVED_KEYWORD) +PG_KEYWORD("account", ACCOUNT, UNRESERVED_KEYWORD) +PG_KEYWORD("action", ACTION, UNRESERVED_KEYWORD) +PG_KEYWORD("add", ADD_P, UNRESERVED_KEYWORD) +PG_KEYWORD("admin", ADMIN, UNRESERVED_KEYWORD) +PG_KEYWORD("after", AFTER, UNRESERVED_KEYWORD) +PG_KEYWORD("aggregate", AGGREGATE, UNRESERVED_KEYWORD) +PG_KEYWORD("algorithm", ALGORITHM, UNRESERVED_KEYWORD) +PG_KEYWORD("all", ALL, RESERVED_KEYWORD) +PG_KEYWORD("also", ALSO, UNRESERVED_KEYWORD) +PG_KEYWORD("alter", ALTER, UNRESERVED_KEYWORD) +PG_KEYWORD("always", ALWAYS, UNRESERVED_KEYWORD) +PG_KEYWORD("analyse", ANALYSE, RESERVED_KEYWORD) /* British spelling */ +PG_KEYWORD("analyze", ANALYZE, RESERVED_KEYWORD) +PG_KEYWORD("and", AND, RESERVED_KEYWORD) +PG_KEYWORD("any", ANY, RESERVED_KEYWORD) +······ +``` + +## 总结 +  该函数之所以是使用最频繁的一个函数,在于我们需要将 SQL 语句的每一个用空格分隔的字符串都用该函数去匹配一下是不是关键字,而当用该函数确认所有 token 后,接下来就可以作进一步的语法分析了。 \ No newline at end of file -- Gitee From 686ba9758aaf1351b03b7b5e45e8f7ad8bc29dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=A0=BC?= Date: Wed, 23 Nov 2022 14:24:50 +0000 Subject: [PATCH 11/16] =?UTF-8?q?add=20TestTasks/geyouguang/=E5=AF=B9scan.?= =?UTF-8?q?l=E7=9A=84=E8=A7=A3=E6=9E=90(=E4=B8=80).md.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 罗格 --- ...\350\247\243\346\236\220(\344\270\200).md" | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 "TestTasks/geyouguang/\345\257\271scan.l\347\232\204\350\247\243\346\236\220(\344\270\200).md" diff --git "a/TestTasks/geyouguang/\345\257\271scan.l\347\232\204\350\247\243\346\236\220(\344\270\200).md" "b/TestTasks/geyouguang/\345\257\271scan.l\347\232\204\350\247\243\346\236\220(\344\270\200).md" new file mode 100644 index 0000000..71bb4d3 --- /dev/null +++ "b/TestTasks/geyouguang/\345\257\271scan.l\347\232\204\350\247\243\346\236\220(\344\270\200).md" @@ -0,0 +1,190 @@ +# 对scan.l的解析(一) + +## 源码链接 + +https://www.gitlink.org.cn/Eao3piq4e/openGauss-server/tree/master/src%2Fcommon%2Fbackend%2Fparser%2Fscan.l + +## 概述 +  该文件的路径为src/common/backend/parser/scan.l,在这个文件中定义了词法结构。在计算机科学里面,Flex 是一个*词法分析器产生程序*\( lexical analyzer generator),Flex 即Fast Lex,它常常与 Bison *语法分析器产生程序*( parser generator )一起使用,Bison 即 GNU 版的 Yacc 。而 Flex 作为词法分析中一个十分有用的工具,在本篇博客我来介绍一下与之联系甚密的 scan.l 文件的用处及其基本构造。 + +## 解析 +  在解析之前,我们先来了解一下 token 的意义,这对我们理解为何要用 Flex 生成一个词法分析器有着莫大意义。当 Flex 读进一个代表词法分析器规则的输入字符串流,然后输出以C语言编写的词法分析器源代码。利用这个词法分析器可以将一个文本中的单词提取出来,而这其实就是提取编程语言占用的各种保留字、操作符等等语言的元素,我们也将这些元素称为令牌( token )。词法分析器一般以函数( yylex() 函数)的形式存在,供[语法分析器](https://baike.baidu.com/item/%E8%AF%AD%E6%B3%95%E5%88%86%E6%9E%90%E5%99%A8/10598664 "语法分析器")调用。 + +  令牌可以是关键字、标识符、常量、字符串值,或者是一个符号。例如,下面的 SQL 语句包括五个令牌: + +```sql +//代码清单1 +SELECE name,id FROM student; +``` + +这五个令牌分别是: + +| | SELECT | name | id | FROM | student | ; | +| :-------: | :----: | :----: | :----: | :----: | :-----: | :------: | +| token类型 | 关键字 | 标识符 | 标识符 | 关键字 | 标识符 | 终结符 | +| 描述 | 命令 | 列名 | 列名 | 条款 | 表名 | 结束语句 | + +  对于一种编程语言来说,token 的类型是有限的。Flex 工具会帮我们生成一个 yylex() 函数,Bison 通过调用这个函数来得知拿到的 token 是什么类型的,而这,便是 scan.l 和 Flex 的关联之处。Flex 的输入文件一般会被命名成 .l 文件,通过 scan.l 我们得到输出的文件是 scan.cpp,yylex() 函数就在这个文件中。这个过程的结构图大概是这样的,yylex() 函数就是我们所说的词法分析器的主体了: + +![](/api/attachments/394604) + +  接下来,我们再了解一下 scan.l 文件的基本结构。总地来说,scan.l 文件的结构为: + +- 定义段 +- %% +- 规则段 +- %% +- C代码段 + +可以看到,scan.l 文件分为3个部分,每个部分间用顶行的 %% 分隔开。 + +#### 定义段 +  这一部分又包含了代码部分、选项部分、状态定义部分、模式定义部分。在代码部分中,代码是用 %top{...} 或者是 %{...%} 括起来的。 + +不管是 %top{...} 还是 %{...%} 括起来的内容都是C代码,经 Flex 处理后会将代码照搬到生成的 scan.cpp 中。但是它们二者括起来的内容会有一点小差别: + +1. %top{...}是将其中的代码拷贝到 scan.cpp 中的顶部,这里我们可以加入一些注释或者包含一些头文件。 +2. %{...%}中可以定义一些函数声明、结构体、类型等,还可以重定义一些 Flex 的宏,从而改变程序的规则。 + +对应在 scan.l 文件中的这一部分: + +```cpp +//代码清单2 +%{ +#include "postgres.h" +#include "knl/knl_variable.h" +#include +#include +······ + +#undef fprintf +#define fprintf(file, fmt, msg) ereport(ERROR, (errmsg_internal("%s", msg))) +#define YYSTYPE core_YYSTYPE +······ + +static void addlit(char *ytext, int yleng, core_yyscan_t yyscanner); +static void addlitchar(unsigned char ychar, core_yyscan_t yyscanner); +static char *litbufdup(core_yyscan_t yyscanner); +static char *litbuf_udeescape(unsigned char escape, core_yyscan_t yyscanner); +······ +%} +``` + +这里我做了部分省略,但还是可以看出在这一部分出现了头文件、宏定义、函数声明等。 + +  对于选项部分,它使得我们不必在控制台使用 Flex 命令来挑选选项,而是在 Flex 的输入文件中通过 %option 语句就可以实现这一功能,对应在 scan.l 文件中的这一部分: + +```cpp +//代码清单3 +%option reentrant +%option bison-bridge +%option bison-locations +%option 8bit +%option never-interactive +%option nodefault +%option noinput +%option nounput +%option noyywrap +%option noyyalloc +%option noyyrealloc +%option noyyfree +%option warn +%option prefix="core_yy" +``` + +这些选项具体的作用可以参照这篇文章:[Flex介绍](https://zhuanlan.zhihu.com/p/89473441 "Flex介绍")。 + +  在状态定义部分,对应的源码为: + +```cpp +//代码清单4 +%x xb +%x xc +%x xd +%x xh +%x xe +%x xq +%x xdolq +%x xui +%x xus +%x xeu +``` + +  模式定义部分由正则表达式构成,对应的源码为: + +```cpp +//代码清单5 +space [ \t\n\r\f] +horiz_space [ \t\f] +newline [\n\r] +non_newline [^\n\r] +comment ("--"{non_newline}*) +whitespace ({space}+|{comment}) +whitespace_only ({space}+) +special_whitespace ({space}+|{comment}{newline}) +horiz_whitespace ({horiz_space}|{comment}) +whitespace_with_newline ({horiz_whitespace}*{newline}{special_whitespace}*) +quote ' +quotestop {quote}{whitespace}* +quotecontinue {quote}{whitespace_with_newline}{quote} +quotefail {quote}{whitespace}*"-" +xbstart [bB]{quote} +xbinside [^']* +······ +``` + +#### 规则段 + +  在这一部分,每一个规则分为*模式/行为行*和*C执行代码行*,例如其中的一个规则: + +```cpp +//代码清单6 +{comment} { + if (yyextra->include_ora_comment) + { + SET_YYLLOC(); + addlit(yytext, yyleng, yyscanner); + yylval->str = litbufdup(yyscanner); + return COMMENTSTRING; + } + } +``` + +其中 {comment} 即为*模式/行为行*,而后面用花括号括起来的即*C执行代码行*,并且在前面的定义部分中,我们可以找到 comment 的正则表达式,意为以 "\-\-" 开头的语句是注释: + +```cpp +//代码清单7 +non_newline [^\n\r] +comment ("--"{non_newline}*) +``` + +scan.l 文件在这里放置的规则就是每个正则表达式要对应的动作,一般是返回一个 token,例如 comment 对应的动作是将注释文本添加到指定的缓冲区并返回 COMMENTSTRING,该部分的C执行代码被 Flex 拷贝到 scan.cpp 中。 + +#### C代码段 +  该部分的C代码也被 Flex 拷贝到 scan.cpp 中,具体的函数行使特定的功能。附加一些源码: + +```cpp +//代码清单8 +#undef yyextra +#define yyextra (((struct yyguts_t *) yyscanner)->yyextra_r) + +/* Likewise for a couple of other things we need. */ +#undef yylloc +#define yylloc (((struct yyguts_t *) yyscanner)->yylloc_r) +#undef yyleng +#define yyleng (((struct yyguts_t *) yyscanner)->yyleng_r) + +int scanner_errposition(int location, core_yyscan_t yyscanner) +{ + ······ +} + +void scanner_yyerror(const char *message, core_yyscan_t yyscanner) +{ + ······ +} +······ +``` + +## 总结 +  我在这篇博客介绍了 scan.l 的作用、基本结构,以及它和 Flex 工具的联系,这给我们理解 gram.y 的作用以及它和 Bison 的联系起到了可供借鉴的作用。 \ No newline at end of file -- Gitee From 4b21e017220c648f6eb7016fbd55df4e11ca1feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=A0=BC?= Date: Wed, 23 Nov 2022 14:25:53 +0000 Subject: [PATCH 12/16] =?UTF-8?q?add=20TestTasks/geyouguang/=E5=AF=B9scan.?= =?UTF-8?q?l=E7=9A=84=E8=A7=A3=E6=9E=90(=E4=BA=8C).md.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 罗格 --- ...\350\247\243\346\236\220(\344\272\214).md" | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 "TestTasks/geyouguang/\345\257\271scan.l\347\232\204\350\247\243\346\236\220(\344\272\214).md" diff --git "a/TestTasks/geyouguang/\345\257\271scan.l\347\232\204\350\247\243\346\236\220(\344\272\214).md" "b/TestTasks/geyouguang/\345\257\271scan.l\347\232\204\350\247\243\346\236\220(\344\272\214).md" new file mode 100644 index 0000000..3b70670 --- /dev/null +++ "b/TestTasks/geyouguang/\345\257\271scan.l\347\232\204\350\247\243\346\236\220(\344\272\214).md" @@ -0,0 +1,120 @@ +# 对scan.l的解析(二) + +## 源码链接 + +https://www.gitlink.org.cn/Eao3piq4e/openGauss-server/tree/master/src%2Fcommon%2Fbackend%2Fparser%2Fscan.l + +## 概述 +  接上一篇博客[对scan.l的解析(一)](https://forum.gitlink.org.cn/forums/7430/detail "对scan.l的解析(一)"),这篇博客我来介绍一下C代码段中一些比较重要的函数。 + +## 函数 + +#### scanner_yyerror() + +函数原型为: + +```cpp +//代码清单1 +//src/common/backend/parser/scan.l +void scanner_yyerror(const char *message, core_yyscan_t yyscanner) +``` + +  它的作用是报告词法或语法错误。该函数的第一个字符串储存了错误信息,而第二个参数在该函数中并未显式地出现,而是以作为宏函数的参数出现的,在本篇博客后面会有介绍。该函数内会使用一个全局指针变量 yylloc ,消息的游标位置是 yylloc 最后一次设置的位置,即如果在 yylex() 中调用,则为当前 token 的开始位置;如果在 base_yyparse() 中调用,那么为最新的词法 token 。对于来自 Bison 解析器的语法错误消息,这是正常的,因为 Bison 解析器在到达第一个不可解析的令牌时立即报告错误。 + +#### scanner_init()、scanner_finish() + +函数原型分别为: + +```cpp +//代码清单2 +//src/common/backend/parser/scan.l +core_yyscan_t scanner_init(const char *str, + core_yy_extra_type *yyext, + const ScanKeyword *keywords, + int num_keywords) +``` + +```cpp +//代码清单3 +//src/common/backend/parser/scan.l +void scanner_finish(core_yyscan_t yyscanner) +``` + +  在完成任何实际解析之前调用 scanner_init() ,换句话说 scanner_init() 用来初始化词法分析器;在解析完成后调用 scanner_finish() ,以在scanner_init() 之后进行清理,也就是释放内存。其中 scanner_init() 的第一个参数是指向存储了查询语句字符串的指针;第二个参数是一个core_yy_extra_type* 类型的结构体指针,它指向的内存区域储存了词法分析器允许我们传送的数据;第三个参数是一个 ScanKeyword* 类型的常量指针,指向一个存储了所有关键字的数组,第四个参数则是记录了这个数组中有效元素的个数。而 scanner_finish() 唯一的参数 yyscanner 的类型是 core_yyscan_t ,通过搜索源文件,我发现了这个类型名是在 src/include/parser/scanner.h 文件中被定义的: + +```cpp +//代码清单4 +//src/include/parser/scanner.h +typedef void* core_yyscan_t; +``` + +这说明 yyscanner 的类型在 scan.l 外部是不透明的。另外,之前不是说 yyscanner 并未显式出现嘛,其实它是有出现的,由于在调用 scanner_init()、scanner_finish() 时如果出错会调用 ereport() 函数,这时就将 yyscanner 作为参数参送给函数 fe_rethrow() 了。 + +```cpp +//代码清单5 +//src/include/parser/scanner.h +#define ereport(a, b) fe_rethrow(yyscanner) +``` + +#### addlit()、addlitchar() + +函数原型分别为: + +```cpp +//代码清单6 +//src/common/backend/parser/scan.l +static void addlit(char *ytext, int yleng, core_yyscan_t yyscanner) +``` + +```cpp +//代码清单7 +//src/common/backend/parser/scan.l +static void addlitchar(unsigned char ychar, core_yyscan_t yyscanner) +``` + +  这两个函数均用来向指定的缓冲区中添加文本,不同的是,可以用 addlit() 添加字符串,而用 addlitchar() 只能添加一个字符。这两个函数中的操作大同小异,我们分析一下前一个函数的源码: + +```cpp +//代码清单8 +static void addlit(char *ytext, int yleng, core_yyscan_t yyscanner) +{ + if ((yyextra->literallen + yleng) >= yyextra->literalalloc) + { + do + { + yyextra->literalalloc *= 2; + } while ((yyextra->literallen + yleng) >= yyextra->literalalloc); + yyextra->literalbuf = (char *) repalloc_huge(yyextra->literalbuf, + yyextra->literalalloc); + } + memcpy(yyextra->literalbuf + yyextra->literallen, ytext, yleng); + yyextra->literallen += yleng; +} +``` + +可以看到,addlit() 函数的第一个参数是一个字符串指针,它指向要被添加的文本,第二个参数记录了该文本的长度,第三个参数是为了使用 yyextra 。第4~12行代码是为了预防添加文本后缓冲区出现溢出现象,所以当 yyextra 记录的文本长度 literallen 和将被添加的文本的长度超过预先设置的最大长度时,要将该预先设置的长度不断翻倍直到能够容纳 ytext 指向的文本。第13行则是将 ytext 指向的文本拷贝到 literalbuf 的原文本的末尾,第14行将文本长度 literallen 更新。 + +#### getDynaParamSeq() + +函数原型为: + +```cpp +//代码清单9 +long getDynaParamSeq(const char *string, bool initflag, bool placeholder, core_yyscan_t yyscanner) +``` + +  该函数用于获取动态 SQL 的参数序列,并返回参数的序列号。它的第一个参数是一个字符指针,指向存储了所求得参数的名称,第二个参数标记操作是否为初始操作,第三个参数标记绑定参数的标志是占位符或美元引号,第四个参数用于 yyextra 。 + +#### is_trans_stmt() + +函数原型为: + +```cpp +//代码清单10 +static bool is_trans_stmt(const char *haystack, int haystack_len) +``` + +  这个函数用于判断一段 SQL 代码是不是事务语句,如果是那么返回 true ,否则返回 false 。 这个函数的第一个参数是一个字符串指针,指向一个可能储存着多条 SQL 语句的字符串,各条语句间用 '\0' 分隔,第二个参数则是所有 SQL 语句的长度加上分隔符 '\0' 数量之和。 + +## 总结 +  可以看到,这些C代码段中的函数都和 scanner 有关,不论是在报错还是初始化,这些对于 scanner 都是一些很重要的操作。 \ No newline at end of file -- Gitee From 7bd43ab90ef738a22e192299a964cacc078d0292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=A0=BC?= Date: Wed, 23 Nov 2022 14:26:50 +0000 Subject: [PATCH 13/16] =?UTF-8?q?add=20TestTasks/geyouguang/=E5=AF=B9scans?= =?UTF-8?q?up.cpp=E7=9A=84=E8=A7=A3=E6=9E=90(=E4=B8=80).md.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 罗格 --- ...\350\247\243\346\236\220(\344\270\200).md" | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 "TestTasks/geyouguang/\345\257\271scansup.cpp\347\232\204\350\247\243\346\236\220(\344\270\200).md" diff --git "a/TestTasks/geyouguang/\345\257\271scansup.cpp\347\232\204\350\247\243\346\236\220(\344\270\200).md" "b/TestTasks/geyouguang/\345\257\271scansup.cpp\347\232\204\350\247\243\346\236\220(\344\270\200).md" new file mode 100644 index 0000000..e870ccb --- /dev/null +++ "b/TestTasks/geyouguang/\345\257\271scansup.cpp\347\232\204\350\247\243\346\236\220(\344\270\200).md" @@ -0,0 +1,200 @@ +# 对scansup.cpp的解析(一) + +## 源码链接 + +https://www.gitlink.org.cn/Eao3piq4e/openGauss-server/tree/master/src%2Fcommon%2Fbackend%2Fparser%2Fscansup.cpp + +## 概述 + +  这个cpp文件在src/common/backend/parser目录下,它提供了词法分析中的常用函数,这些函数被用来处理查询语句中的转义字符。在这篇博客里,我将介绍文件中的第一个函数scanstr()。 + +## 源码 + +```cpp +//代码清单1 +char* scanstr(const char* s) +{ + char* newStr = NULL; + int len, i, j; + + if (s == NULL || s[0] == '\0') + return pstrdup(""); + + len = strlen(s); + + newStr = (char*)palloc(len + 1); /* string cannot get longer */ + + for (i = 0, j = 0; i < len; i++) { + if (s[i] == '\'') { + /* + * Note: if scanner is working right, unescaped quotes can only + * appear in pairs, so there should be another character. + */ + 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'; + return newStr; +} +``` + +## 解析 + +  对于这个函数,如果传入的字符串 s 有转义字符,那么它会将转义字符映射到实际字符,这个是什么意思呢?举个例子,比如字符串 "a\b’’c" ,现在各个字符对应的十进制ASCII码值为: + +- a —— 97 +- \ —— 92 +- b —— 98 +- ’ —— 39 +- ’ —— 39 +- c —— 99 + +将转义字符映射到实际字符之后,就变成了: + +- a —— 97 +- \b —— 8 +- ’ —— 39 +- c —— 99 + +  相信大家对 '\b' 并不感到陌生,它是退格符,但有一个疑惑,为什么两个单引号凑一起经转义后就变成了一个单引号,这是因为一个单引号它是SQl查询语句中的转义字符,就像两个反斜杠凑一起由 '\\\\' 转义变成了一个反斜杠 '\\' 。 +  对于代码清单1中的代码,由于传入的字符串指针 s 是 const char\* 类型的,这也就是说这个字符串是不可更改的常量,所以我们在第4行定义了一个指针变量newStr,并在第12行为它分配了内存空间,这样我们就可以用它存储指针 s 指向的字符串的副本,通过对这个副本进行操作,从而不会影响源字符串的内容。第7、8行,指的是如果指针 s 为空或者说它指向的字符串为空,那么结束程序。 +  然后就是第14至66行核心的 for 循环结构,主体上又分为3个判断结构: + +- 1. 字符为单个单引号 +- 2. 字符为单个反斜杠 +- 3. 其他普通字符 + +#### 第一种情况 + +  那么相应地,就出现了三种应对方式,先讲第一种: + +```cpp +//代码清单2 +//字符为单引号 +if (s[i] == '\'') +{ + i++; + newStr[j] = s[i]; +} +``` + +代码清单2中,如果一个字符为单个单引号,那么这个单引号在还未转义的字符串中出现,应当是成对出现的,所以有 i++ 的操作,因为 s[i] 应当也是单个单引号。 + +#### 第二种情况 + +```cpp +//代码清单3 +//字符为单个反斜杠 +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; + } +} +``` + +  在代码清单3中,当 s[i] 为单个反斜杠字符时(注:这里单个反斜杠字符是用 '\\\\' 来表示的),跟在它后面的字符可能是字母也可能是数字,所以这里又有三种情况: + +- 1. 当它的后一个字符为字母时,我们只需要判断其为 'b'、'f'、'n'、'r'、't' ,即判断其空白字符的种类,'\b' 即退格符,'\f' 即换页符,'\n' 即换行符,'\r' 即回车,'\t' 即水平制表符; +- 2. 当它的后一个字符为数字时,最多能跟3个0~7的数,代表的是一个八进制的数; +- 3. 其他种类的转义字符。 + +  对于第1种情况,自然是将单个反斜杠字符和字母字符转义成特定的ASCII码即可,对应代码清单3中第8\~22行。而对于第2种情况,第35\~38行的for循环结构是十分重要的,它将反斜杠后连在一起的数由八进制转换为十进制。说到第3种情况,对于其它的转义字符比如 '\0' ,在我们将字符 '0' 前面的反斜杠符号存入数组后,还需要做的就是将字符 '0' 也存入数组,这就是第43行代码的用处。 + +#### 第三种情况 + +```cpp +//代码清单4 +//其他普通字符 +else +{ + newStr[j] = s[i]; +} +``` +  这个结构和第二种情况分类中的最后一个很相似,但不同的是,这个结构处理的是普通的数字字符或字母字符。 + +## 总结 + +  通过对 scanstr() 函数的解析,我们清楚地发现这个函数只是负责处理空白字符和八进制数字转义字符,其它字符不做修改。 \ No newline at end of file -- Gitee From 30f7b17b45a2404e645553c1cd835eeca50b9679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=A0=BC?= Date: Wed, 23 Nov 2022 14:28:02 +0000 Subject: [PATCH 14/16] =?UTF-8?q?add=20TestTasks/geyouguang/=E5=AF=B9scans?= =?UTF-8?q?up.cpp=E7=9A=84=E8=A7=A3=E6=9E=90(=E4=BA=8C).md.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 罗格 --- ...\350\247\243\346\236\220(\344\272\214).md" | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 "TestTasks/geyouguang/\345\257\271scansup.cpp\347\232\204\350\247\243\346\236\220(\344\272\214).md" diff --git "a/TestTasks/geyouguang/\345\257\271scansup.cpp\347\232\204\350\247\243\346\236\220(\344\272\214).md" "b/TestTasks/geyouguang/\345\257\271scansup.cpp\347\232\204\350\247\243\346\236\220(\344\272\214).md" new file mode 100644 index 0000000..139b0c4 --- /dev/null +++ "b/TestTasks/geyouguang/\345\257\271scansup.cpp\347\232\204\350\247\243\346\236\220(\344\272\214).md" @@ -0,0 +1,105 @@ +# 对scansup.cpp的解析(二) + +## 源码链接 + +https://www.gitlink.org.cn/Eao3piq4e/openGauss-server/tree/master/src%2Fcommon%2Fbackend%2Fparser%2Fscansup.cpp + +## 概述 + +  这个 cpp 文件在 src/common/backend/parser 目录下,它提供了词法分析中的常用函数,这些函数被用来处理查询语句中的转义字符。在这篇博客里,我将介绍文件中常常一起使用的两个函数 downcase_truncate_identifier() 和 truncate_identifier() 。 + +## 源码 + +```cpp +//代码清单1 +char* downcase_truncate_identifier(const char* ident, int len, bool warn) +{ + char* result = NULL; + int i; + bool enc_is_single_byte = false; + + result = (char*)palloc(len + 1); + enc_is_single_byte = pg_database_encoding_max_length() == 1; + 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; + } + result[i] = '\0'; + + if (i >= NAMEDATALEN) + truncate_identifier(result, i, warn); + + return result; +} +void truncate_identifier(char* ident, int len, bool warn) +{ + if (len >= NAMEDATALEN) { + len = pg_mbcliplen(ident, len, NAMEDATALEN - 1); + if (warn) { + char buf[NAMEDATALEN]; + errno_t rc; + rc = memcpy_s(buf, NAMEDATALEN, ident, len); + securec_check(rc, "\0", "\0"); + buf[len] = '\0'; + ereport(NOTICE, + (errcode(ERRCODE_NAME_TOO_LONG), errmsg("identifier \"%s\" will be truncated to \"%s\"", ident, buf))); + } + ident[len] = '\0'; + } +} +``` + +## 解析 + +#### 函数downcase_truncate_identifier() +  我们先来讲一下第一个函数 downcase_truncate_identifier() 的作用,顾名思义,它是要将一个标识符进行降格和截断处理。那什么是降格呢?其实就是将标识符中的大写英文字母变为小写。那截断需要什么条件吗?当然,如果标识符的长度超过了规定的最大长度,那么才会截断,这个最大长度即宏常量 NAMEDATALEN ,即64(实际为63,最后一位为 ‘\0’ )。 + +  downcase_truncate_identifier() 有三个参数,第一个参数为一个指向包含了标识符常量字符串的指针,第二个参数为该字符串的长度,第三个参数用于标识是否在字符串长度大于规定长度时输出警告信息。 + +  在代码清单1第4行我们定义了一个字符串指针,并在第8行为它分配了足够的空间。在第6行我们定义了 boolean 变量用于标明是单字节还是多字节编码,默认是多字节编码,所以我们在第9行通过 pg_database_encoding_max_length() 函数获取字节编码的方式,如果它的返回值为1,自然就将这个 boolean 类型的变量 enc_is_single_byte 的值赋为 true 。 + +  第10~19行的 for 循环结构是这个函数的核心,大部分情况下,我们执行第二个块中的判断语句即可: + +```cpp +//代码清单2 +else if (enc_is_single_byte && IS_HIGHBIT_SET(ch) && isupper(ch)) { + ch = tolower(ch); +} +``` + +这时,我们需要了解一下 IS_HIGHBIT_SET() 函数: + +```cpp +//代码清单3 +#define IS_HIGHBIT_SET(ch) ((unsigned char)(ch) & HIGHBIT) +#define HIGHBIT (0x80) +``` + +在 ASCII 码中,某个英文字母 ch 与 HIGHBIT 的逻辑与的结果必为零,但是当编码方式不是 ASCII 码,而是其它某个地区特有的编码方式时,这段代码就起到作用了。 + +  可是,这并非是一定的,比如在土耳其语中 'i' 和 'I' 并非是配套的大小写字母,它们有各自的大写字母,分别为 'İ' 和 'L' ,所以对应的编码不再是ASCII码了,这时用的便是另外一个块中的语句将其转换为小写: + +```cpp +//代码清单4 +if (ch >= 'A' && ch <= 'Z') { + ch += 'a' - 'A'; +} +``` + +当然,代码清单4中的代码已经够用了。 + +  第22行的语句判断目标字符串的的长度(这里包括了 '\0' )是否超过规定的最大长度 NAMEDATALEN ,注意是超过而非等于 NAMEDATALEN ,这是因为数组下标是从0开始的,如果确实超过了,那么继续调用 truncate_identifier() ,对目标字符串作进一步的截断处理。 + +#### 函数truncate_identifier() + +  在代码清单1第30行,len 变量的值取 pg_mbcliplen(ident, len, NAMEDATALEN - 1) 的返回值即 NAMEDATALEN - 1 。另外,如果 warn 变量为 true ,那么将执行截断操作的信息打印出来,在第35行进行安全性检查,确保如果在分配内存时发生错误会抛出异常。 + +## 总结 + +  总的来说,downcase_truncate_identifier() 函数的作用就是将标识符中的大写字母转化为对应的小写字母,只有当标识符的长度过长时,它才会调用 truncate_identifier() 函数进行截断,这一点是需要明确的。 \ No newline at end of file -- Gitee From 32745b296f00f01f87f4d6df87dc632d9af717bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=A0=BC?= Date: Wed, 23 Nov 2022 14:28:42 +0000 Subject: [PATCH 15/16] =?UTF-8?q?add=20TestTasks/geyouguang/=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E4=BC=98=E5=8C=96=E6=A8=A1=E5=9D=97=E6=80=BB=E7=BB=93?= =?UTF-8?q?.md.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 罗格 --- ...41\345\235\227\346\200\273\347\273\223.md" | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 "TestTasks/geyouguang/\346\237\245\350\257\242\344\274\230\345\214\226\346\250\241\345\235\227\346\200\273\347\273\223.md" diff --git "a/TestTasks/geyouguang/\346\237\245\350\257\242\344\274\230\345\214\226\346\250\241\345\235\227\346\200\273\347\273\223.md" "b/TestTasks/geyouguang/\346\237\245\350\257\242\344\274\230\345\214\226\346\250\241\345\235\227\346\200\273\347\273\223.md" new file mode 100644 index 0000000..7c2af42 --- /dev/null +++ "b/TestTasks/geyouguang/\346\237\245\350\257\242\344\274\230\345\214\226\346\250\241\345\235\227\346\200\273\347\273\223.md" @@ -0,0 +1,39 @@ +# 查询优化模块总结 + +# 查询 + +  本人在团队负责的是查询优化这个板块,在阅读源码的过程中,虽然一开始有很多不理解的地方,但是通过查询相关资料或者询问老师,对其有了深刻的认知。以下是本人对opengauss的查询优化部分做的一个小总结。 + +## 查询重写 + +  查询重写利用现有的语句特性和关系代数运算生成更高效的等价语句,在数据库优化中起着关键作用,尤其是在复杂查询中,可以带来巨大的性能提升。 ![302500c73ef3de64e5b7a1398a192882](https://img-blog.csdnimg.cn/img_convert/302500c73ef3de64e5b7a1398a192882.png) + +- [对查询重写部分的解析](https://forum.gitlink.org.cn/forums/7594/detail) + +  首先,本人基于优化方法的不同,对优化器的优化技术进行了分类。其中,opengauss采用基于代价的查询优化(CBO)技术,另外在基于机器学习的查询优化(ABO)方面也在进行积极探索。其次,对源码的主流程代码的框架进行了分析。 + +  把用户输入的SQL语句转换为更高效的等价SQL,需要遵循等价性和高效性的基本原则。基于这两个原则,分支出常量表达式化简、子查询提升、谓词下推等的查询重写技术,提高查询的易用性。 + +## 路径搜索 + +  优化器的核心问题是获取特定 SQL 语句的最优解。这个过程通常需要枚举SQL语句对应的解空间,即枚举不同的候选执行路径。这些候选执行路径是等价的,但是执行效率不同,需要计算执行成本,最终得到最优执行路径。 + +- [对路径搜索部分的解析](https://forum.gitlink.org.cn/forums/7598/detail) + +  优化器的结构分为自底向上模式、自底向下模式、随机搜索模式,目前openGauss采用的是自底向上模式和随机搜索模式相结合的方式。搜索的过程是从逻辑执行计划转化为物理执行计划的过程,比如每张表可能有不同的扫描算子,逻辑连接算子也可以换成不同的物理连接算子,对于单表连接路径,本人对源码进行了具体分析。对于多表,其问题核心为Join Order,比较常用的算法是基于代价的动态规划算法,随着关联表个数的增多,会发生表搜索空间膨胀的问题,进而影响优化器路径选择的效率,可以采用基于代价的遗传算法等随机搜索算法来解决,针对多表,openGauss采用了三种剪枝策略,详见链接。 + +  openGauss优化引擎还可以生成高效的分布式路径。在分布式架构下,同一个表的数据会分布到不同的DN上,创建表的时候可以选择将数据在每个表上做Hash分布或者Random分布,根据分发算子所需要处理的数据量以及网络通信所带来的消耗,可以计算这些路径的代价,openGauss优化引擎会根据代价从中选出最优的路径。 + +  为了提升计算的性能,opengauss还借助一些数据结构或算法来对数据的分布做一些预处理,这些预处理方法利用了物理执行路径的物理属性,比如B+树、Hash表、排序、物化等。 + +## 代价估算 + +  查询执行成本分为 I/O 成本和 CPU 成本。这两个成本与查询期间处理的元组数量正相关。因此,选择性地评估查询计划的总成本更为准确。 + +- [对代价估算部分的解析](https://forum.gitlink.org.cn/forums/7599/detail) + +  对每条SQL语句,openGauss都会生成多个候选的计划,并且给每个计划计算一个执行代价,然后选择代价最小的计划。确定了约束条件过滤出的数据占总数据量的比例后,本人对总代价进行了细分,包括IO代价、CPU代价、通信代价,并进行了详细的解释。 + +## 总结 + +  本人对查询优化部分的解析就到这里,相信接下来的学习,会对其他板块也有一个更深层次的帮助和理解。 \ No newline at end of file -- Gitee From ee97681a17b4768c0167a0e5bd6afb3bfbae714f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=A0=BC?= Date: Wed, 23 Nov 2022 14:30:10 +0000 Subject: [PATCH 16/16] =?UTF-8?q?add=20TestTasks/geyouguang/=E9=80=9A?= =?UTF-8?q?=E4=BF=A1=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97.md.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 罗格 --- ...41\347\220\206\346\250\241\345\235\227.md" | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 "TestTasks/geyouguang/\351\200\232\344\277\241\347\256\241\347\220\206\346\250\241\345\235\227.md" diff --git "a/TestTasks/geyouguang/\351\200\232\344\277\241\347\256\241\347\220\206\346\250\241\345\235\227.md" "b/TestTasks/geyouguang/\351\200\232\344\277\241\347\256\241\347\220\206\346\250\241\345\235\227.md" new file mode 100644 index 0000000..f3d4c7f --- /dev/null +++ "b/TestTasks/geyouguang/\351\200\232\344\277\241\347\256\241\347\220\206\346\250\241\345\235\227.md" @@ -0,0 +1,58 @@ +# OpenGauss之postmaster通信模块概述 + +**一、OpenGauss数据库体系概述** + +​ openGauss是关系型数据库,采用客户端/服务器,单进程多线程架构;支持单机和一主多备部署方式,同时支持备机可读、双机高可用等特性。 openGauss相比于其他开源数据库主要有以下几个主要特点: + +1.高性能 + +- 提供了面向多核架构的并发控制技术结合鲲鹏硬件优化,在两路鲲鹏下TPCC Benchmark达成性能150万tpmc。 +- 针对当前硬件多核numa的架构趋势,在内核关键结构上采用了Numa-Aware的数据结构。 +- 提供Sql-bypass智能快速引擎、融合引擎技术。 + +2.高可用 + +- 支持主备同步、异步和级联备机多种部署模式。 +- 数据页CRC校验,损坏数据页通过备机自动修复。 +- 备机并行恢复,10秒内可升主提供服务。 + +3.高安全 + +- 支持全密态计算、访问控制、加密认证、数据库审计和动态数据脱敏等安全特性,提供全方位端到端的数据安全保护。 + +4.易运维 + +- 基于AI的智能参数调优和索引推荐,提供AI自动参数推荐。 +- 慢SQL诊断,多维性能自监控视图,实时掌控系统的性能表现。 +- 提供在线自学习的SQL时间预测。 + +5.全开放 + +- 采用木兰宽松许可证协议,允许对代码自由修改、使用和引用。 +- 数据库内核能力全开放。 +- 提供丰富的伙伴认证,培训体系和高校课程。 + +从代码结构体系结构的角度来说,oepnGauss的第一个组成部分是通信管理。 ![img](https://forum.gitlink.org.cn/api/attachments/394619) 图1 Opengauss代码结构 + +​ openGauss查询响应是使用“单个用户对应一个服务器线程”的简单客户端/服务器模型实现的。由于我们无法预先知道需要建立多少连接,所以必须使用主进程(GaussMaster)来监听指定TCP/IP(传输控制协议/网际协议)端口上的传入连接,只要连接请求 检测到,主进程将生成一个新的服务器线程。服务器线程使用信号量和共享内存相互通信,以确保整个并发数据访问期间的数据完整性。 + +​ 建立连接后,客户端进程可以将查询发送到后端服务器。查询使用纯文本传输,即在前端(客户端)中没有进行解析。服务器解析查询语句、创建执行计划、执行并通过在已建立连接上传输检索到的结果集,将其返回给客户端。 + +​ Postmasters是openGauss数据库中处理客户端连接请求的模块。前端程序发送启动信息给postmaster,postmaster根据信息内容建立后端响应线程。postmaster也管理系统级的操作,比如调用启动和关闭程序。postmaster在启动时创建共享内存和信号量池,但它自身不管理内存、信号量和锁操作。 + +​ 当客户端发来一个请求信息,postmaster立刻启动一个新会话,新会话对请求进行验证,验证成功后为它匹配后端工作线程。这种模式架构上处理简单,但是高并发下由于线程过多,切换和轻量级锁区域的冲突过大导致性能急剧下降。因此openGauss通过线程资源池化复用的技术来解决该问题。线程池技术的整体设计思想是线程资源池化,并且在不同连接直接复用。 + +​ 除开通信管理之外,Open Gauss的一大组成部分就是SQL引擎,承担着查询解析、查询分流、查询重写、查询优化和查询执行等任务,之后剩下的就是存储引擎了。 + +**二、postmaster源码解析** + +1.postmaster源码目录为:/src/gausskernel/process/postmaster postmaster源码文件如下表所示: ![img](https://forum.gitlink.org.cn/api/attachments/394726) ![img](https://forum.gitlink.org.cn/api/attachments/394727) 图2 源码目录表 2. postmaster主流程 相较于postgreSQL修改过的函数: + +- GaussdbThreadGate-定义 +- Serverloop-启动线程 +- Reaper-回收线程 +- GaussDBThreadMain-入口函数 + +​ 主线程postmaster负责内存、全局信息、信号、线程池等的初始化,用来创建其他子线程,OpenGauss是单进程多线程,在程序启动时,一个进程被操作系统创建,同时主线程立刻运行。Postgremaster进程在起始时会建立共享内存和信号库,Postgremaster及其子进程的通信就是通过共享内存和信号来实现的。 + +​ 主线程运行时可以用来接收前端的命令,对命令做出处理,还会对子线程进行监视,在子线程出现问题时会对子线程进行管理,保证数据库正常的运行。这种多进程设计使得整个系统的稳定性更好,Postgremaster只需要重置共享内存即可从单个后台进程的奔溃中恢复。 \ No newline at end of file -- Gitee