RDBMS关键技术 | 青训营

66 阅读5分钟

RDBMS关键技术

引言: 一条SQL语言想要实现其功能,往往需要以下几个步骤:

  1. 由一个请求产生的SQL经路由器发送到RDBMS中;
  2. RDBMS收到该SQL语句后,需要利用解析模块Parser由该SQL语言得到对应的语法树AST;
  3. 优化器Optimizer将生成的AST转换成最终的执行语句Plan发送给执行器Executor;
  4. 执行器Executor根据拿到的Plan语句对数据文件进行读写操作,同时也会写入日志;
  5. Executer执行结束后会得到Result返回给用户。

其中,Parser,Optimizer和Executor都是SQL引擎,数据文件和日志文件则是存储引擎。事务引擎也是其中起到重要作用的组件,虽然在上述过程中没有显式表现。事务引擎负责负责管理和执行数据库操作的事务,以保证数据库的一致性和完整性。也可以说,各组件的功能都是为了完成各事务而服务的。关于事务的具体介绍见文章《RDBMS简介》。

本文将主要介绍SQL引擎中的解析器Parser和优化器Optimizer。

解析器Parser

Parser是一种用于解析SQL语句的工具。它可以将输入的SQL查询文本转换为一个数据结构,通常是抽象语法树(AST),以便数据库管理系统能够理解和执行这些查询。简单来说,就是将SQL语句转换为执行器能看懂的语言。一般说,Parser可以分为词法分析(Lexical analysis)、语法分析(Syntax analysis)、语义分析(Semantic analysis)等步骤。

其中,词法分析可以将SQL语句分解成关键字(如UPDATE/SET/WHERE)、表列名、常量、运算符(如"="/"-")、结束符(";")。语法分析则可以将词法分析分解得到的各部分组合成一个结构体(如AST),包含Table、Values、Fields等字段。语义分析进行合法性校验,比如检查AST中的表名和列名是否存在,各种常量是否合法等。

以下是来自于github.com/marianogapp… 的源码,可以将一个string类型的SQL请求转换成定义的query.Query切片用来表示SQL查询语句结构,以简单展示Parser的实现过程及功能。

// Parse takes a string representing a SQL query and parses it into a query.Query struct. It may fail.
func Parse(sqls string) (query.Query, error) {
	qs, err := ParseMany([]string{sqls})
	if len(qs) == 0 {
		return query.Query{}, err
	}
	return qs[0], err
}

// ParseMany takes a string slice representing many SQL queries and parses them into a query.Query struct slice.
// It may fail. If it fails, it will stop at the first failure.
func ParseMany(sqls []string) ([]query.Query, error) {
	qs := []query.Query{}
	for _, sql := range sqls {
		q, err := parse(sql)
		if err != nil {
			return qs, err
		}
		qs = append(qs, q)
	}
	return qs, nil
}

func parse(sql string) (query.Query, error) {
	return (&parser{0, strings.TrimSpace(sql), stepType, query.Query{}, nil, ""}).parse()
}

// Query represents a parsed query
type Query struct {
	Type       Type
	TableName  string
	Conditions []Condition
	Updates    map[string]string
	Inserts    [][]string
	Fields     []string // Used for SELECT (i.e. SELECTed field names) and INSERT (INSERTEDed field names)
	Aliases    map[string]string
}

优化器Optimizer

Optimizer的作用是通过选择最优的执行计划来提高查询性能和效率。通过解析器Parser,我们已经可以知道用户想要执行的操作具体是什么了,类比于一个导航,我们已经知道目的地在哪里了。现在我们需要知道的是如何根据不同的需求找到最优的路线。

比如以下SQL语句是一个联接查询(Join Query),用于从三个不同的表 A、B 和 C 中检索数据,根据一些条件进行关联。

SELECT * FROM A, B, C WHERE A.a1 = B.b1 and A.a1 = C.b1;

联接查询通常会将多个表中的数据连接在一起,这时候就需要考虑多个表联接方案的优劣了。比如可以先联接A,B表,再联接C表,也可以A,C表先联接,在联接B表,等等。

联接的方式也可以不同,比如

  1. 排序联接(Sort Merge Join): 这个算法要求对连接的列进行排序,然后扫描排好序的表,逐行匹配相同的值,从而生成结果。这个算法适用于两个表都已经排序的情况,它不需要额外的内存空间。但是如果表没有排序,就需要先对其进行排序,增加了开销。
  2. 哈希联接(Hash Join): 这个算法将连接的列值映射到哈希表中,然后对另一个表进行哈希操作,将匹配的值从哈希表中找出,生成结果。这个算法适用于连接的列有大量重复值的情况,但需要额外的内存用于构建哈希表。

显然,不同的联接方式也有各自的优缺点。优化器可以对各种方式进行排列组合选择最优的方案。

这里再简单介绍一下基础的优化器。

首先是基于规则的优化器RBO(Rule Base Optimizer)。RBO可以通过一系列的预定义规则来优化查询语句,从而生成更高效的执行计划。这些规则是基于数据库的结构、索引信息以及查询语句的语法和语义特点等因素定义的。比如可以对表的联接进行规则定义,小的表会被优先联接;再比如可以对各种条件语句基于规则进行简化,冗余的多个条件可以优化为单个条件,等等。

还有基于代价的优化器CBO(Cost Base Optimizer)。CBO使用统计信息和代价模型来评估不同的执行计划,从而选择最优的执行计划来执行查询语句,以获得更高的性能和效率。比如之前提到的导航的例子,不同的路线有不同的路况,有不同数量的红绿灯,也有不同的过路费灯,那么CBO就可以综合考虑统计信息计算代价,根据需求(比如最低过路费,高速优先,最快抵达灯)选取最优路线。当然,对于数据库查询来说,最优先考虑的代价一般就是时间,请求的处理时间很大程度上决定了用户的体验。除了时间以外,内存、网络、IO等因素也会是代价所需要考虑的部分,因为数据库很可能要处理多个用户的请求。