1. 背景介绍
StoneDB 采用基于知识网格技术和列式存储引擎。该存储引擎为海量数据背景下 OLAP 应用而设计,通过列式存储数据、知识网格过滤、高效数据压缩等特性,为应用系统提供低成本和高性能的数据查询支持。
本文旨在分析 StoneDB 引擎的查询模块,包括:查询模块结构图、查询流程、知识网格、知识网格优化、优化算法。
2. StoneDB架构图
3. 查询模块结构图
SQL模块(Query Layer)
1.SQL Parser
解析器的主要作用是解析SQL语句,最终生成语法树。解析器会对SQL语句进行语法和语义分析,如果有错误,则返回相应的错误信息。检查通过后,解析器会查询缓存,如果缓存中有对应的语句和查询结果,则直接返回结果,不进行接下来的优化和执行步骤。
2.Optimizer
优化器的主要作用是对SQL语句进行优化,包括选择合适的索引,数据的读取方式,生成代价最小的执行计划。
3.Execute
执行器的主要作用是判断操作的表是否有权限,如果有权限,会根据表的存储引擎定义,调用接口去读取数据,最后返回满足条件的查询结果。
4.Filter
粗糙集过滤,找到可能包含的包。
5.DPN evaluation
根据DPN迭代判断包里面每条是否满足。
6.decompress
分片并行解压粗糙过滤后命中的包。
知识网格模块(Knowledge Grid)
知识网格是由数据包节点和知识节点组成的。由于数据包都是压缩存放的,所以数据读取解压的代价比较高,在查询中如何做到读取尽量少的数据包是提升效率的关键。知识网格正是起到了这样的一个作用,它能够有效的过滤查询中不符合条件的数据,以最小的代价定位以数据包为最小单位的数据。知识网格的数据大小只占数据总量的1%以下,通常情况下可以加载到内存中,进一步提升查询效率。
对于大部分统计、聚合性查询,StoneDB 往往只需要使用知识网格就能返回查询结果,这是因为通过知识网格可以消除或大量减少需要解压的数据包,降低 IO 消耗,提高查询响应时间和网络利用率。例如:数据包节点记录了最大值、最小值、平均值、总和、总记录数、null 值的数量,如果想对某个列做聚合运算,那么知识网格就能根据这些元数据很快的得到结果,而无需再解压访问底层的数据包。
1.数据包节点(Data Pack Node)
数据包节点也称为元数据节点(Metadata Node),因为数据包节点记录了每个数据包中列的最大值、最小值、平均值、总和、总记录数、null 值的数量、压缩方式、占用的字节数。每一个数据包节点对应一个数据包。
2.知识节点(Knowledge Node)
数据包节点的上一层是知识节点,记录了数据包之间或者列之间关系的元数据集合,比如值数据包的最小值与最大值的范围、列之间的关联关系。大部分的知识节点数据是装载数据的时候产生的,另外一部分是查询的时候产生的。
4. 查询流程图
1.Client 连接
与数据库建立连接,此过程遵循 MySQL5.6、5.7 的连接协议。
2.SQL Parser
对 SQL 语句进行语法和语义分析。
解析入口:
parse_sql()
3.StoneDB Optimizer
对 SQL 语句进行优化,生成代价最小的执行计划。
优化函数:
optimize_select()
4.知识网格
StoneDB 在执行查询的时候会根据知识网络(Knowledge Grid)把 DP 分成三类:
- 相关的 DP(Relevant Packs),满足查询条件限制的 DP
- 不相关的 DP(Irrelevant Packs),不满足查询条件限制的 DP
- 可疑的 DP(Suspect Packs),DP 里面的数据部分满足查询条件的限制
入口函数:
RCAttr::ApproxAnswerSize
获取DN:
Pack *get_pack(size_t i)
5.命中之后,解压相关包。
主函数:
CprsErr Decompress
6.返回结果集。
主函数:
ResultSender
5. 知识网格
5.1 知识网络总览图
知识网格由数据元信息节点 (MD) 和知识节点 (KN) 组成, 通过知识网格,StoneDB 引擎无需通过传统数据索引、数据分区、预测、手动调优或特定架构等方式就能高速处理复杂的分析查询。
5.2 元信息节点(MD)
数据元信息节点 (MD) 与数据节点 (DN) 之间保持 1:1 关系,元信息节点中包含了其对应数据块中数据的相关信息:
- 数据聚合函数值 (MIN,MAX,SUM,AVG 等)。
- 记录数量 (count)。
- 数据中的 null 记录标记。
- 元信息节点在数据装载的时候就会构建,MD 为数据压缩, 聚合函数即席查询等技术提供了支持。
- 知识节点 (KN) 除了基础元数据外,还包括数据特征以及更深度的数据统信息。
- 知识网格存储在内存中,方便快速查询。
数据结构:
struct DPN{}
获取DPN:
DPN &get_dpn
5.3 知识节点(KN)
- Column Metadata 包含了该列的数据类型定义,约束条件等基础信息。
主类:
class impl
FieldRange 是一个标识数据节点 (DN) 中记录值范围段的标识 Map。针对数值类型的记录 (date/time, integer,decimal…),FieldRange 可以用来快速确认当前对应 DN 是否包含所需数据。FieldRange 的组成:
- 从 MD 中获取数据块的 MAX 与 MIN,并将 MAX-MIN 划分为固定段(例如1024)。
- 每个段中分别使用1个 bit 标识是否有记录存在与该范围内。
Char Map 是一个记录字符是否匹配的 Map。 针对字符类型的记录(char,varchar等),CharMap 可以快速确定当前 DP 是否包含所需数据。
- 统计当前 DN 内,1-64 长度中 ASCII 字符是否存在的标识值。
- 字符检索时,按照字符顺序,依次对比字符标示值即可知道该 DN 是否包含匹配数据。
- join nodes
- 在 Join 查询时自动创建,以关系位图的形式,存储 Join 操作中关联 DataNode 的信息。
- 存储在 Session 中,仅对当前 Client 生效。
- 显著提高Join查询性能。减少无效 DN 扫描,避免无用 IO 的产生。
distributions
- 统计当前 Column 中各记录的值分布信息。
- 基于统计信息,和粗糙集 (RoughSet) 计算,提供近似查询支持 (Approximate Query)。
6 知识网格优化器
对于来自客户端的请求,首先由查询优化器进行基于知识网格的优化,产生执行计划后再交给执行引擎去处理。
下图为执行流程:
基于知识网格中的信息进行粗燥集(Rough Set)构建, 并确定此次请求所需使用到的数据节点。
基于 KN 和 MD ,确定查询涉及到的 DN 节点集合,并将 DN 节点归类:
- 关联 DN 节点:满足查询条件限制的数据节点(直接读取并返回)
- 不确定性 DN 节点: 数据节点中部分数据满足查询条件(解压后进行处理再返回).
- 非关联 DN 节点: 与查询条件完全不相关(直接忽略).
执行计划构建时, 会完全规避非关联 DN, 仅读取并解压关联 DN, 按照特定情况决定是否读取不确定的 DN。
- 例子: 对于一个查询请求, 通过KG可以确定 3 个关联性 DN 和 1 个不确定性 DN。如果此请求包含聚合函数。此时只需要解压不确定性 DN 并计算聚合值,再结合3个关联性 DN 中 MD上的统计值即可得出结果。如果此请求需要返回具体数据, 那么无论关联性 DN 还是不确定性 DN,都需要读取数据块并解压缩,以获得结果集。
- 如果查询请求的结果可以直接从元信息节点 (MD) 中产生(例如 count, max, min 等操作),则直接返回元信息节点中的数据,无需访问物理数据文件.
查询案例1:SELECT count(*) FROM students where score < 550。
- 通过 KG 知识,查找包含 score < 550 的数据节点(DN),此处可以看到只有 A/B/C 三个节点涉及到该查询。
- 数据节点 A 与 B 属于关联 DN 节点, 只需直接从对应的 MD 节点中获取 count 值即可。
- 数据节点 C 属于不确定性 DN 节点,需要读取数据块并解压, 执行函数计算后才能返回结果集。
- 这里只有数据块C会被读取并解压,数据节点 A 与 B 并不消耗 IO 资源。
7. 优化算法
7.1 Comment Lookup
Comment Lookup 只能显式地使用在 char 或者 varchar 上面。Comment Lookup可以减少存储空间,提高压缩率,对 char 和 varchar 字段采用 comment lookup 可以提高查询效率。
Comment Lookup 实现机制很像位图索引,实现上利用简短的数值类型替代char字段已取得更好的查询性能和压缩比率。Comment Lookup 的使用除了对数据类型有要求,对数据也有一定的要求。一般要求数据类别的总数小于10000并且当前列的单元数量/类别数量大于10。Comment Lookup 比较适合年龄,性别,省份这一类型的字段。
Comment Lookup 使用很简单,在创建数据库表的时候如下定义即可:
act char(15) comment 'lookup',
part char(4) comment 'lookup',
7.2 join算法
StoneDB 支持 NESTED LOOP JOIN,SORT MERGE JOIN,HASH JOIN和MAP JOIN 算法。
StoneDB 通过 EvaluateConditionJoinWeight 评估 join 权重,根据 UpdateJoinCondition 改变 join 条件,ChooseJoinAlgorithm 选择join算法。
7.2.1 NESTED LOOP JOIN
对于被连接的数据子集较小的情况,嵌套循环连接是个较好的选择。在嵌套循环中,内表被外表驱动,外表返回的每一行都要在内表中检索找到与它匹配的行,因此整个查询返回的结果集不能太大(大于1 万不适合),要把返回子集较小表的作为外表,CBO 默认外表是驱动表 (CBO:基于成本优化),而且在内表的连接字段上一定要有索引。通过 JoinerGeneral::ExecuteOuterJoinLoop 函数进入 NESTED LOOP JOIN 计算。
7.2.2 SORT MERGE JOIN
通常情况下散列连接的效果都比排序合并连接要好,然而如果行源已经被排过序,在执行排序合并连接时不需要再排序了,这时排序合并连接的性能会优于散列连接。
Sort Merge join 用在没有索引,并且数据已经排序的情况。函数 JoinerSort::ExecuteJoinConditions 进入算法,Join关联过程遵循小表扫描 MarkUsedDims(dims1),大表匹配 MarkUsedDims(dims2)。
7.2 HASH JOIN
散列连接是 CBO 做大数据集连接时的常用方式,优化器使用两个表中较小的表(或数据源)利用连接键在内存中建立散列表,然后扫描较大的表并探测散列表,找出与散列表匹配的行。
这种方式适用于较小的表完全可以放于内存中的情况,这样总成本就是访问两个表的成本之和。但是在表很大的情况下并不能完全放入内存,这时优化器会将它分割成若干不同的分区,不能放入内存的部分就把该分区写入磁盘的临时段,此时要有较大的临时段从而尽量提高 I/O 的性能。通过函数 JoinerHash::ExecuteJoinConditions 进入 JoinerHash::ExecuteJoin 执行 HASH JOIN 算法,如果 ini_join_parallel>0,调用ProxyHashJoiner/ParallelHashJoiner 并行执行。
7.2.4 MAP JOIN
Map join 适用于一个大表和一个或多个小表执行 join 操作的场景。整个join过程包含 map、shuffle 和reduce 三个阶段。通常情况下,join 操作在 reduce 阶段执行表连接。map join 操作是在 map 阶段执行的,大量缩短了数据传输的时间,提升了系统资源的利用率,从而起到了优化作业的作用,并且 map join 会将指定的小表全部加载到执行 join 操作的程序的内存中,从而加快 join 的速度。
通过 JoinerMapped::ExecuteJoinConditions 进入MAP JOIN算法,根据 JoinerMapped::GenerateFunction 生产map函数,调用 map_function 的 Init 执行 map join。