openGauss数据库是华为深度融合在数据库领域多年经验,结合企业级场景要求推出的新一代企业级开源数据库。此前,Gauss松鼠会已经发布了openGauss数据库核心技术系列文章,介绍了openGauss的技术原理。从本期开始,Gauss松鼠会将陆续推出openGauss数据库源码解析系列文章,带你解析openGauss各功能模块的源代码逻辑和实现原理。该系列文章主要面向openGauss内核开发者及研究人员。
接下来会先向大家概要介绍openGauss。本篇将从openGauss概述、应用场景、系统结构、代码结构四个方面对openGauss进行介绍。
一、openGauss概述
openGauss是关系型数据库,采用客户端/服务器,单进程多线程架构;支持单机和一主多备部署方式,同时支持备机可读、双机高可用等特性。
openGauss有如下基本功能:
1、支持标准SQL
openGauss数据库支持标准的SQL(Structured Query Language,结构化查询语言)。SQL标准是一个国际性的标准,定期会进行更新和演进。SQL标准的定义分成核心特性以及可选特性,绝大部分的数据库都没有100%支撑SQL标准。openGauss数据库支持SQL92/SQL99/SQL2003等,同时支持SQL2011大部分的核心特性,另外还支持部分的可选特性。
2、支持标准开发接口
openGauss数据库提供业界标准的ODBC(Open Database Connectivity,开放式数据库连接)及JDBC(Java Database Connectivity,java数据库连接)接口,保证用户能将业务快速迁移至openGauss。目前支持标准的ODBC3.5及JDBC4.0接口,其中ODBC能够支持CentOS、openEuler、SUSE、Win32、Win64等平台,JDBC无平台差异。
3、混合存储引擎支持
openGauss数据库支持行存储引擎、列存储引擎和内存存储引擎等。行存分为“inplace update” 和 “append update”两种模式,前者通过单独的回滚段(undo log)来保留元组的前像以解决读写冲突,可以更自然的支持数据更新;后者将更新记录混杂在数据记录中,通过新旧版本的形式来支持数据更新,对于旧版本需要定期做vacuum操作来支持磁盘空间的回收。列存支持数据快速分析,更适合OLAP(Online Analytical Processing,联机分析处理)业务。内存引擎支持实时数据处理,对有极致性能要求的业务提供支撑。
4、事务支持
事务支持指的就是系统提供事务的能力,openGauss支持事务的原子性、一致性、隔离性和持久性。事务支持及数据一致性保证是绝大多数数据库的基本功能,只有支持了事务,才能满足事务化的应用需求。
- A(Atomicity):原子性。整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。
- C(Consistency):一致性。事务需要保证从一个执行性状态转移到另一个一致性状态,不能违反数据库的一致性约束。
- I(Isolation):隔离性。隔离事务的执行状态,使它们好像是系统在给定时间内执行的唯一操作。例如有两个事务并发执行,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。
- D(Durability):持久性。在事务提交以后,该事务对数据库所作的更改便持久的保存在数据库之中,不会因掉电,进程异常故障而丢失。
openGauss数据库支持事务的隔离级别有读已提交和可重复读,默认隔离级别是读已提交,保证不会读到脏数据。
事务分为隐式事务和显式事务,显式事务的相关基础接口如下:
- Start transaction:事务开启
- Commit:事务提交
- Rollback:事务回滚
另有用户还可以通过“Set transaction”命令设置事务的隔离级别、读写模式或可推迟模式。
5、软硬结合
openGauss数据库支持软硬件的结合,包括多核的并发访问控制、基于SSD(Solid-State Drive,固态硬盘)的IO(输入/输出Input/Output,)优化、智能的Buffer Pool(缓冲池)数据管理。
6、智能的优化器
openGauss数据库提供了智能的代价模型、智能计划选择,可以显著提升数据库性能。openGauss的执行器包含了向量化执行和LLVM(Low Level Virtual Machine,底层虚拟机,一种构架编译器的框架系统)编译执行,可以显著提升数据库性能。
7、AI的支持
传统数据库生态依赖于DBA(Database Administrator,数据库管理员)进行数据的管理、维护、监控、优化。但是在大量的数据库实例中,DBA难以支持海量实例,而AI(Artificial Intelligence ,人工智能)则可以自动优化数据库,openGauss数据库的AI功能包括AI自调优、AI索引推荐、AI慢SQL诊断等。
8、安全的支持
openGauss数据库具有非常好的安全特性,包括透明加密(即在磁盘的存储文件是加密的)、全密态(数据传输、存储、计算都是加密的)、防篡改(用户不可篡改)、敏感数据智能发现等。
9、函数及存储过程支持
函数和存储过程是数据库中的一种重要对象,主要功能将用户特定功能的SQL语句集进行封装,并方便调用。存储过程是SQL、PL/SQL(Procedural Language SQL,过程语言SQL)的组合。存储过程可以使执行商业规则的代码从应用程序中移动到数据库。从而代码存储一次能够被多个程序使用。
- 允许客户模块化程序设计,对SQL语句集进行封装,调用方便。
- 存储过程会进行编译缓存,可以提升用户执行SQL语句集的速度。
- 系统管理员通过对执行某一存储过程的权限进行限制,能够实现对相应数据访问权限的限制,避免了非授权用户对数据的访问,保证了数据的安全。
- 为了处理SQL语句,存储过程分配一段内存区域来保存上下文。游标是指向上下文区域的句柄或指针。借助游标,存储过程可以控制上下文区域的变化。
- 支持6种异常信息级别方便客户对存储过程进行调试。支持存储过程调试,存储过程调试是一种调试手段,可以在存储过程开发中,一步一步跟踪存储过程执行的流程,根据变量的值,找到错误的原因或者程序的bug,提高问题定位效率。支持设置断点和单步调试。
openGauss支持SQL标准中的函数及存储过程,增强了存储过程的易用性。
10、PostgreSQL接口兼容
兼容PSQL客户端,兼容PostgreSQL标准接口。
11、支持SQL hint
支持SQL hint(hints是SQL语句的注释,可以指导优化器选择人为指定的执行计划。)影响执行计划生成、提升SQL查询性能。Plan Hint为用户提供了直接影响执行计划生成的手段,用户可以通过指定Join顺序\Join、Scan方法\指定结果行数\等多个手段来进行执行计划的调优,以提升查询的性能。
12、Copy接口支持容错机制
openGauss数据库提供用户封装好的函数来创建Copy错误表,并允许用户在使用“Copy From”指令时指定容错选项,使得“Copy From”语句在执行过程中部分解析、数据格式、字符集等相关的报错不会报错中断事务、而是被记录至错误表中,使得在“Copy From”的目标文件即使有少量数据错误也可以完成入库操作。用户随后可以在错误表中对相关的错误进行定位以及进一步排查。
二、 应用场景
openGauss数据库主要的应用场景分为:
1、交易型应用
大并发、大数据量、以联机事务处理为主的交易型应用,如电商、金融、O2O、电信CRM/计费等,应用可按需选择不同的主备部署模式。
2、物联网数据
在工业监控和远程控制、智慧城市的延展、智能家居、车联网等物联网场景下,传感监控设备多、采样率高、数据存储为追加模型,操作和分析并重的场景。
三、系统架构
openGauss主要包含了openGauss服务器、客户端驱动、OM等模块,它的架构如图1所示,模块说明如表1所示。
|图1 openGauss软件架构|
表1 openGauss模块说明
名称 | 描述 |
---|---|
OM | 运维管理模块(Operation Manager)。提供openGauss日常运维、配置管理的管理接口、工具 |
客户端驱动 | 客户端驱动(Client Driver)。负责接收来自应用的访问请求,并向应用返回执行结果;负责与openGauss实例的通信,下发SQL在openGauss实例上执行,并接收命令执行结果 |
openGauss主(备) | openGauss主(备)。负责存储业务数据(支持行存、列存、内存表存储)、执行数据查询任务以及向客户端驱动返回执行结果 |
Storage | 服务器的本地存储资源,持久化存储数据 |
四、 代码结构
(一)通信管理
openGauss查询响应是使用简单的“单一用户对应一个服务器线程”的客户端/服务器模型实现的。由于我们无法提前知道需要建立多少个连接,因此必须使用主进程(GaussMaster)主进程在指定的TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/互联网协议)端口上侦听传入的连接,只要检测到连接请求,主进程就会生成一个新的服务器线程。服务器线程之间使用信号量和共享内存相互通信,以确保整个并发数据访问期间的数据完整性。
客户端进程可以被理解为满足openGauss协议的任何程序。许多客户端都基于C语言库libpq进行通信,但是该协议有几种独立的实现,例如Java JDBC驱动程序。
建立连接后,客户端进程可以将查询发送到后端服务器。查询是使用纯文本传输的,即在前端(客户端)中没有进行解析。服务器解析查询语句、创建执行计划、执行并通过在已建立的连接上传输检索到的结果集,将其返回给客户端。
openGauss数据库中处理客户端连接请求的模块叫做postmaster。前端程序发送启动信息给postmaster,postmaster根据信息内容建立后端响应线程。postmaster也管理系统级的操作比如调用启动和关闭程序。postmaster在启动时创建共享内存和信号量池,但它自身不管理内存、信号量和锁操作。
当客户端发来一个请求信息,postmaster立刻启动一个新会话,新会话对请求进行验证,验证成功后为它匹配后端工作线程。这种模式架构上处理简单,但是高并发下由于线程过多,切换和轻量级锁区域的冲突过大导致性能急剧下降。因此openGauss通过线程资源池化复用的技术来解决该问题。线程池技术的整体设计思想是线程资源池化,并且在不同连接直接复用。
1. postmaster源码组织
postmaster源码目录为:/src/gausskernel/process/postmaster。postmaster源码文件如表2所示。
表2 postmaster源码文件
postmaster.cpp | 用户响应主程序 |
---|---|
aiocompleter.cpp | 完成预取(Prefetch)和后端写(BackWrite)I/O操作 |
alarmchecker.cpp | 闹钟检查线程 |
lwlockmonitor.cpp | 轻量锁的死锁检测 |
pagewriter.cpp | 写页面 |
pgarch.cpp | 日志存档 |
pgaudit.cpp | 审计线程 |
pgstat.cpp | 统计信息收集 |
startup.cpp | 服务初始化和恢复 |
syslogger.cpp | 捕捉并写所有错误日志 |
autovacuum.cpp | 垃圾清理线程 |
bgworker.cpp | 后台工作线程(服务共享内存) |
bgwriter.cpp | 后台写线程(写共享缓存) |
cbmwriter.cpp remoteservice.cpp | postmaster信号处理 |
checkpointer.cpp | 检查点处理 |
fencedudf.cpp | 保护模式下运行用户定义函数 |
gaussdb_version.cpp | 版本特性控制 |
twophasecleaner.cpp | 清理两阶段事务线程 |
walwriter.cpp | 预写式日志写入 |
2. postmaster主流程
postmaster主流程代码如下。
/* postmaster.cpp */
...
int PostmasterMain(int argc, char* argv[])
{
InitializePostmasterGUC(); /* 初始化postmaster配置参数*/
...
pgaudit_agent_init(); /* 初始化审计模块*/
...
for (i = 0; i < MAXLISTEN; i++) /* 建立输入socket监听*/
t_thrd.postmaster_cxt.ListenSocket[i] = PGINVALID_SOCKET;
...
/* 建立共享内存和信号量池
*/reset_shared(g_instance.attr.attr_network.PostPortNumber);
...
/* 初始化postmaster信号管理
*/gs_signal_slots_init(GLOBAL_ALL_PROCS + EXTERN_SLOTS_NUM);
...
InitPostmasterDeathWatchHandle(); /* 初始化宕机监听*
...
pgstat_init(); /* 初始化统计数据收集子系统*/
InitializeWorkloadManager(); /* 初始化工作负载管理器*/
...
InitUniqueSQL(); /* 初始化unique sql资源*/
...
autovac_init(); /* 初始化垃圾清理线程子系统*/
...
status = ServerLoop(); /* 启动postmaster主业务循环*/
...
}
(二) SQL引擎
数据库的SQL引擎是数据库重要的子系统之一,它对上负责承接应用程序发送过来的SQL语句,对下则负责指挥执行器运行执行计划。其中优化器作为SQL引擎中最重要、最复杂的模块,被称为数据库的“大脑”,优化器产生的执行计划的优劣直接决定数据库的性能。
本篇从SQL语句发送到数据库服务器开始,对SQL引擎的各个模块进行全面的介绍与源码解析,以实现对SQL语句执行的逻辑与源码更深入的理解。其响应流程如图1所示。
|图2 openGauss数据库SQL查询响应流程|
1. 查询解析——parser
SQL解析对输入的SQL语句进行词法分析、语法分析、语义分析,获得查询解析树或者逻辑计划。SQL查询语句解析的解析器(parser)阶段包括如下:
- 词法分析:从查询语句中识别出系统支持的关键字、标识符、操作符、终结符等,每个词确定自己固有的词性。
- 语法分析:根据SQL语言的标准定义语法规则,使用词法分析中产生的词去匹配语法规则,如果一个SQL语句能够匹配一个语法规则,则生成对应的语法树(Abstract Synatax Tree,AST)。
- 语义分析:对语法树(AST)进行检查与分析,检查AST中对应的表、列、函数、表达式是否有对应的元数据(指数据库中定义有关数据特征的数据,用来检索数据库信息)描述,基于分析结果对语法树进行扩充,输出查询树。主要检查的内容包括:
①检查关系的使用:FROM子句中出现的关系必须是该查询对应模式中的关系或视图。
②检查与解析属性的使用:在SELECT句中或者WHERE子句中出现的各个属性必须是FROM子句中某个关系或视图的属性。
③检查数据类型:所有属性的数据类型必须是匹配的。
词法和语法分析代码基于gram.y和scan.l中定义的规则,使用Unix工具bison和flex构建产生。其中,词法分析器在文件scan.l中定义,它负责识别标识符、SQL关键字等。对于找到的每个关键字或标识符,都会生成一个标记并将其传递给解析器。语法解析器在文件gram.y中定义,由一组语法规则和每当触发规则时执行的动作组成,基于这些动作代码构架并输出语法树。在解析过程中,如果语法正确,则进入语义分析阶段并建立查询树返回,否则将返回错误,终止解析过程。
解析器在词法和语法分析阶段仅使用有关SQL语法结构的固定规则来创建语法树。它不会在系统目录中进行任何查找,因此无法理解所请求操作的详细语义。
语法解析完成后,语义分析过程将解析器返回的语法树作为输入,并进行语义分析以了解查询所引用的表、函数和运算符。用来表示此信息的数据结构称为查询树。解析器解析过程分为原始解析与语义分析,分开的原因是,系统目录查找只能在事务内完成,并且我们不希望在收到查询字符串后立即启动事务。原始解析阶段足以识别事务控制命令(BEGIN,ROLLBACK等),然后可以正确执行这些命令而无需任何进一步分析。一旦知道我们正在处理实际查询(例如SELECT或UPDATE),就可以开始事务,这时才调用语义分析过程。
1) parser源码组织
parser源码目录为:/src/common/backend/parser。parser源码文件如表3所示。
表3 parser源码文件
parser.cpp | 解析主程序 |
---|---|
scan.l | 词法分析,分解查询成token |
scansup.cpp | 处理查询语句转义符 |
kwlookup.cpp | 将关键词转化为具体的token |
keywords.cpp | 标准关键词列表 |
analyze.cpp | 语义分析 |
gram.y | 语法分析,解析查询tokens并产生原始解析树 |
parse_agg.cpp | 处理聚集操作,比如SUM(col1),AVG(col2) |
parse_clause.cpp | 处理子句,比如WHERE,ORDER BY |
parse_compatibility.cpp | 处理数据库兼容语法和特性支持 |
parse_coerce.cpp | 处理表达式数据类型强制转换 |
parse_collate.cpp | 对完成表达式添加校对信息 |
parse_cte.cpp | 处理公共表格表达式(WITH 子句) |
parse_expr.cpp | 处理表达式,比如col, col+3, x = 3 |
parse_func.cpp | 处理函数,table.column和列标识符 |
parse_node.cpp | 对各种结构创建解析节点 |
parse_oper.cpp | 处理表达式中的操作符 |
parse_param.cpp | 处理参数 |
parse_relation.cpp | 支持表和列的关系处理程序 |
parse_target.cpp | 处理查询解析的结果列表 |
parse_type.cpp | 处理数据类型 |
parse_utilcmd.cpp | 处理实用命令的解析分析 |
2) parser主流程
parser主流程代码如下。
/* parser.cpp */
...
/* 原始解析器,输入查询字符串,做词法和语法分析,返回原始语法解析树列表*/
List* raw_parser(const char* str, List** query_string_locationlist)
{
...
/* 初始化 flex scanner */ yyscanner = scanner_init(str, &yyextra.core_yy_extra, ScanKeywords, NumScanKeywords);
...
/* 初始化 bison parser */
parser_init(&yyextra);
/* 解析! */
yyresult = base_yyparse(yyscanner);
/* 清理释放内存*/
scanner_finish(yyscanner);
...
return yyextra.parsetree;
}
/* analyze.cpp */
...
/* 分析原始语法解析树,做语义分析并输出查询树 */
Query* parse_analyze(
Node* parseTree, const char* sourceText, Oid* paramTypes, int numParams, bool isFirstNode, bool isCreateView)
{
/* 初始化解析状态和查询树 */
ParseState* pstate = make_parsestate(NULL);
Query* query = NULL;
...
/* 将解析树转化为查询树 */
query = transformTopLevelStmt(pstate, parseTree, isFirstNode, isCreateView);
...
/* 释放解析状态 */
free_parsestate(pstate);
...
return query;
}
2. SQL查询分流——traffic cop
traffic cop模块负责查询的分流,它负责区分简单和复杂的查询指令。事务控制命令(例如BEGIN和ROLLBACK)非常简单,因此不需要其它处理,而其它命令(例如SELECT和JOIN)则传递给重写器(参考第6章)。这种区分通过对简单命令执行最少的优化,并将更多的时间投入到复杂的命令上,从而减少了处理时间。简单和复杂查询指令也对应如下2类解析:
- 软解析(简单,旧查询):当openGauss共享缓冲区中存在已提交SQL语句的已解析表示形式时,可以重复利用缓存内容执行语法和语义检查,避免查询优化的相对昂贵的操作。
- 硬解析(复杂,新查询):如果无缓存语句可重用,或者第一次将SQL语句加载到openGauss共享缓冲区中,则会导致硬解析。同样,当一条语句在共享缓冲区中老化时,再次重新加载该语句时,还会导致另一次硬解析。因此,共享Buffer的大小也会影响解析调用的数量。
我们可以查询gs_prepared_statements来查看缓存了什么,以区分软/硬解析(它仅对当前会话可见)。此外,gs_buffercache模块提供了一种实时检查共享缓冲区高速缓存内容的方法,它甚至可以分辨出有多少数据块来自磁盘,有多少数据来自共享缓冲区。
1) traffic cop(tcop)源码组织
traffic cop(tcop)源码目录为:/src/common/backend/tcop。traffic cop(tcop)源码文件如表4所示。
表4 traffic cop(tcop)源码文件
auditfuncs.cpp | 记录数据库操作审计信息 |
---|---|
autonomous.cpp | 创建可被用来执行SQL查询的自动会话 |
dest.cpp | 与查询结果被发往的终点通信 |
utility.cpp | 数据库通用指令控制函数 |
fastpath.cpp | 在事务期间缓存操作函数和类型等信息 |
postgres.cpp | 后端服务器主程序 |
pquery.cpp | 查询处理指令 |
stmt_retry.cpp | SQL语句错误错误码并重试 |
2) traffic cop主流程
traffic cop主流程代码如下。
/*postgres.cpp*/
...
/*原始解析器,输入查询字符串,做词法和语法分析,返回原始解析树列表*/
int PostgresMain(int argc, char* argv[], const char* dbname, const char* username)
{
...
/* 整体初始化*/
t_thrd.proc_cxt.PostInit->SetDatabaseAndUser(dbname, InvalidOid, username);
...
/* 自动事务的错误处理 */
if (sigsetjmp(local_sigjmp_buf, 1) != 0) { ... }
...
/* 错误语句的重新尝试阶段 */
if (IsStmtRetryEnabled() && StmtRetryController->IsQueryRetrying()){ ... }
/* 无错误查询指令循环处理*/
for (;;) {
...
/* 按命令类型执行处理流程*/switch(firstchar){
...
case: 'Q': ... /* 简单查询 */
case: 'P': ... /* 解析 */
case: 'E': ... /* 执行 */
}
...
}
...
}
3. 查询重写——rewriter
查询重写利用已有语句特征和关系代数运算来生成更高效的等价语句,在数据库优化器中扮演关键角色;尤其在复杂查询中,能够在性能上带来数量级的提升,可谓是“立竿见影”的“黑科技”。SQL语言是丰富多样的,非常的灵活,不同的开发人员依据经验的不同,手写的SQL语句也是各式各样,另外还可以通过工具自动生成。同时SQL语言是一种描述性语言,数据库的使用者只是描述了想要的结果,而不关心数据的具体获取方式,输入数据库的SQL语言很难做到是以最优形式表示的,往往隐含了一些冗余信息,这些信息可以被挖掘用来生成更加高效的SQL语句。查询重写就是把用户输入的SQL语句转换为更高效的等价SQL,查询重写遵循2个基本原则:
- 等价性:原语句和重写后的语句,输出结果相同。
- 高效性:重写后的语句,比原语句在执行时间和资源使用上更高效。
介绍openGauss如下几个关键的查询重写技术:
- 常量表达式化简:常量表达式即用户输入SQL语句中包含运算结果为常量的表达式,分为算数表达式、逻辑运算表达式、函数表达式。查询重写可以对常量表达式预先计算以提升效率。例如“SELECT * FROM table WHERE a=1+1; ”语句被重写为“SELECT * FROM table WHERE a=2”语句。
- 子查询提升:由于子查询表示的结构更清晰,符合人的阅读理解习惯,用户输入的SQL语句往往包含了大量的子查询,但是相关子查询往往需要使用嵌套循环的方法来实现,执行效率较低,因此将子查询优化为“Semi Join”的形式可以在优化规划时选择其它的执行方法,或能提高执行效率。例如“SELECT * FROM t1 WHERE t1.a in (SELECT t2.a FROM t2); ”语句可重写为“SELECT * FROM t1 LEFT SEMI JOIN t2 ON t1.a=t2.a”语句。
- 谓词下推:谓词(Predicate),通常为SQL语句中的条件,例如“SELECT * FROM t1 WHERE t1.a=1; ”语句中的“t1.a=1”即为谓词。等价类(Equivalent-Class)是指等价的属性、实体等对象的集合,例如“WHERE t1.a=t2.a”语句中,t1.a和t2.a互相等价,组成一个等价类{t1.a,t2.a}。利用等价类推理(又称作传递闭包),我们可以生成新的谓词条件,从而达到减小数据量和最大化利用索引的目的。如图2所示,我们举一个形象的例子来说明谓词下推的威力。假设有两个表t1、t2;它们分别包含[1,2,3,..100]共100行数据,那么查询语句“SELECT * FROM t1 JOIN t2 ON t1.a=t2.a WHERE t1.a=1”的逻辑计划在经过查询重写前后的对比。
|图3 查询重写前后对比|
查询重写的主要工作在优化器中实现,除此之外,openGauss还提供了基于规则的rewrite接口,用户可以通过创建替换规则的方法对逻辑执行计划进行改写。例如视图展开功能即通过rewrite模块中的规则进行替换,而视图展开的规则是在创建视图的过程中默认创建的。
1) rewriter源码组织
rewriter源码目录为:/src/gausskernel/optimizer/rewrite。rewriter源码文件如表5所示。
表5 rewriter源码文件 | rewriteDefine.cpp | 定义重写规则| |--|--| | rewriteHandler.cpp |重写主模块 | |rewriteManip.cpp | 重写操作函数 | |rewriteRemove.cpp | 重写规则移除函数 | | rewriteRlsPolicy.cpp |重写行粒度安全策略 | | rewriteSupport.cpp | 重写辅助函数|
2) rewriter主流程
rewriter主流程代码如下。
/* rewrite.cpp /.../ 查询重写主函数 /List QueryRewrite(Query* parsetree){... /* 应用所有non-SELECT规则获取改写查询列表 / querylist = RewriteQuery(parsetree, NIL); / 对每个改写查询应用RIR规则 / results = NIL; foreach (l, querylist) { Query query = (Query*)lfirst(l); query = fireRIRrules(query, NIL, false); query->queryId = input_query_id; results = lappend(results, query); } /* 从重写列表确定一个重写结果 */ origCmdType = parsetree->commandType; foundOriginalQuery = false; lastInstead = NULL; foreach (l, results) {...} ... return results;}
4. 查询优化——optimizer
优化器(optimizer)的任务是创建最佳执行计划。一个给定的SQL查询(以及一个查询树)实际上可以以多种不同的方式执行,每种方式都会产生相同的结果集。如果在计算上可行,则查询优化器将检查这些可能的执行计划中的每一个,最终选择预期运行速度最快的执行计划。
在某些情况下,检查执行查询的每种可能方式都会占用大量时间和内存空间,特别是在执行涉及大量连接操作(Join)的查询时。为了在合理的时间内确定合理的(不一定是最佳的)查询计划,当查询连接数超过阈值时,openGauss使用遗传查询优化器(genetic query optimizer),通过遗传算法来做执行计划的枚举。
优化器的查询计划(plan)搜索过程实际上与称为路径(path)的数据结构一起使用,该路径只是计划的简化表示,其中仅包含确定计划所需的关键信息。确定代价最低的路径后,将构建完整的计划树以传递给执行器。这足够详细地表示了所需的执行计划,供执行者运行。在下文中,我们将忽略路径和计划之间的区别。
5. 查询执行——executor
执行器(executor)采用优化器创建的计划,并对其进行递归处理以提取所需的行的集合。这本质上是一种需求驱动的流水线执行机制。即每次调用一个计划节点时,它都必须再传送一行,或者报告已完成传送所有行。
|图5 执行计划树示例|
如图4所示的执行计划树示例,顶部节点是Merge Join节点。在进行任何合并操作之前,必须获取2个元组(MergeJoin节点的2个子计划各返回1个元组)。因此,执行器以递归方式调用自身以处理其子计划(如从左子树的子计划开始)。Merge Join由于要做归并操作,因此它要子计划按序返回元组,从图4可以看出,它的子计划是一个Sort节点。Sort的子节点可能是Seq Scan节点,代表对表的实际读取。执行SeqScan节点会使执行程序从表中获取一行并将其返回到调用节点。Sort节点将反复调用其子节点以获得所有要排序的行。当输入完毕时(如子节点返回NULL而不是新行),Sort算子对获取的元组进行排序,它每次返回1个元组,即已排序的第1行。然后不断排序并向父节点传递剩余的排好序的元组。
Merge Join节点类似地需要获得其右侧子计划中的第1个元组,看是否可以合并。如果是,它将向其调用方返回1个连接行。在下1次调用时,或者如果它不能连接当前输入对,则立即前进到1个表或另1个表的下1行(取决于比较的结果),然后再次检查是否匹配。最终,1个或另1个子计划用尽,并且Merge Join节点返回NULL,以指示无法再形成更多的连接行。
复杂的查询可能涉及多个级别的计划节点,但是一般方法是相同的:每个节点都会在每次调用时计算并返回其下1个输出行。每个节点还负责执行优化器分配给它的任何选择或投影表达式。
执行器机制用于执行所有4种基本SQL查询类型:SELECT、INSERT、UPDATE和DELETE。
- 对于SELECT,顶级执行程序代码仅需要将查询计划树返回的每一行发送给客户端。
- 对于INSERT,每个返回的行都插入到为INSERT指定的目标表中。这是在称为ModifyTable的特殊顶层计划节点中完成的。(1个简单的“INSERT ... VALUES”命令创建了1个简单的计划树,该树由单个Result节点组成,该节点仅计算一个结果行,并传递给ModifyTable树节点实现插入)。
- 对于UPDATE,优化器对每个计算的更新行附着所更新的列值,以及原始目标行的TID(元组ID或行ID);此数据被馈送到ModifyTable节点,并使用该信息来创建新的更新行并标记旧行已删除。
- 对于DELETE,计划实际返回的唯一列是TID,而ModifyTable节点仅使用TID访问每个目标行并将其标记为已删除。
执行器的主要处理控制流程如下:
- 创建查询描述。
- 查询初始化:创建执行器状态(查询执行上下文)、执行节点初始化(创建表达式与每个元组上下文、执行表达式初始化)。
- 查询执行:执行处理节点(递归调用查询上下文、执行表达式,然后释放内存,重复操作)。
- 查询完成;执行未完成的表格修改节点。
- 查询结束:递归释放资源、释放查询及其子节点上下文。
- 释放查询描述。
1) executor源码组织
executor源码目录为:/src/gausskernel/runtime/executor。executor源码文件如表7所示。
表7 executor源码文件 | execAmi.cpp | 各种执行器访问方法 | |--|--| |execClusterResize.cpp | 集群大小调整 | | execCurrent.cpp |支持WHERE CURRENT OF | | execGrouping.cpp | 支持分组、哈希和聚集操作 | | execJunk.cpp | 伪列的支持 | | execMain.cpp |顶层执行器接口 | |execMerge.cpp | 处理MERGE指令 | |execParallel.cpp | 支持并行执行| | execProcnode.cpp | 分发函数按节点调用相关初始化等函数 | | execQual.cpp | 评估资质和目标列表表达式 | | execScan.cpp | 通用的关系扫描 | |execTuples.cpp | 元组相关的资源管理 | |execUtils.cpp | 多种执行相关工具函数 | | functions.cpp | 执行SQL语言函数 | | instrument.cpp | 计划执行工具 | | lightProxy.cpp | 轻量级执行代理 | | node*.cpp | 处理*相关节点操作的函数 | | opfusion.cpp、opfusion_util.cpp、opfusion_scan.cpp | 旁路执行器:处理简单查询 | | spi.cpp | 服务器编程接口 | |tqueue.cpp | 并行后端之间的元组信息传输 | |tstoreReceiver.cpp | 存储结果元组 |
2)executor主流程
executor主流程代码为。
/* execMain.cpp */
...
/* 执行器启动 */
void ExecutorStart(QueryDesc *queryDesc, int eflags)
{
gstrace_entry(GS_TRC_ID_ExecutorStart);
if (ExecutorStart_hook) {
(*ExecutorStart_hook)(queryDesc, eflags);
}
else {
standard_ExecutorStart(queryDesc, eflags);
}
gstrace_exit(GS_TRC_ID_ExecutorStart);
}
/* 执行器运行 */
void ExecutorRun(QueryDesc *queryDesc, ScanDirection direction, long count)
{
...
/* SQL 自调优: 查询执行完毕时,基于运行时信息分析查询计划问题 */
if (u_sess->exec_cxt.need_track_resource && queryDesc != NULL && has_track_operator &&(IS_PGXC_COORDINATOR || IS_SINGLE_NODE))
{
List *issue_results = PlanAnalyzerOperator(queryDesc, queryDesc->planstate);
/* 如果查询问题找到,存在系统视图 gs_wlm_session_history */
if (issue_results != NIL) {
RecordQueryPlanIssues(issue_results);
}
}
/* 查询动态特征, 操作符历史统计信息 */
if (can_operator_history_statistics)
{
u_sess->instr_cxt.can_record_to_table = true;
ExplainNodeFinish(queryDesc->planstate, queryDesc->plannedstmt, GetCurrentTimestamp(), false);
...
}
}
/* 执行器完成 */
void ExecutorFinish(QueryDesc *queryDesc)
{
if (ExecutorFinish_hook) {
(*ExecutorFinish_hook)(queryDesc);
}
else {
standard_ExecutorFinish(queryDesc);
}
}
/* 执行器结束 */
void ExecutorEnd(QueryDesc *queryDesc){
if (ExecutorEnd_hook) {
(*ExecutorEnd_hook)(queryDesc);
}
else
{
standard_ExecutorEnd(queryDesc);
}
}
本篇中,openGauss的代码结构从数据库系统通信管理、SQL引擎两方面展开了介绍,由于内容较多,其第三方面的内容——存储引擎将放在下一篇图文中具体介绍,敬请期待!
Gauss松鼠会是汇集数据库爱好者和关注者的大本营, 大家共同学习、探索、分享数据库前沿知识和技术, 互助解决问题,共建数据库技术交流圈。