文章目录
简单数据库实现
- 一个sql查询语句,可以拆分成原子operator的叠加,把operator组建成operator tree,自底向上执行最终得到结果
- Mysql和Postgres中执行
EXPLAIN SQL_STMT
可以打印生成的operator tree
存储
- 分离元数据和数据的存储
- 修改文件造成较大的读写负担,可以使用slot_table记录信息,并且使用append only的形式(对于数据文件的修改也用append only的形式),在读取数据时先读取slot_table,然后逆序逆序读取所有行的标注信息。随着数据文件增加,使用vacuum操作(compact),读取数据文件和slot_table根据标注把有效的数据行写回新的文件。
- 不适用明文而使用raw byte来存储数据配合高效的编码和解码算法
- 类似雪花模型,需要存多且大的实体表,但是语句只需要读取相关的几个属性。可以使用列村而不是行存。在
索引优化
-
哈希索引:建立哈希表
-
B树索引:建立B/B+树
优化:对于经常查询的列可以把列的数据直接存储在索引中,省去读取文件的时间
缺点:高并发环境下需要加锁,可以用skip list代替
两种索引相比更建议使用B树索引,且B树索引得到的结果对于索引列是有序的
Because of the limited utility of hash indexes, a B-tree index should generally be preferred over a hash index. We do not have sufficient evidence that hash indexes are actually faster than B-trees even for = comparisons. Moreover, hash indexes require coarser locks.
-
位图索引:适合基数小的列来做索引
例:
sex: bitmap male: 10001111 female: 01110000
查询时只需要和位图做与操作,选出结果为1的即可
局限:只适合列基数小的,且不适合高并发环境
执行模式
用户输入SQL语句
Parsing
通过编译器把语句编译(parsing)成抽象的语法树
Binding
负责将语法树和数据库的metadata结合,附上语义。自底向上对整颗语法树的节点依次检查,同时在节点绑定元数据,最终生成含有语义的语法树。
binding完成之后,SQL语句就算编译通过。
Optimizing
给定语法树,首先生成一个逻辑执行树,执行树上的每一个节点称为逻辑操作符。对应每个逻辑操作符,扩展出所有的物理操作符,逻辑操作符表示某个功能,物理操作符则说明用什么方法来实现这个功能, 可能有多个物理执行树。下一步在多个物理执行树中选出最优的一个,这步操作比较复杂。
Executing
从执行树底层开始依次执行。
执行器的执行模式:
- materialization模式,代码运行时自底向上,每个节点process只需要运行一次,一次性处理全部输入
- iterator model/volcano model,第一种模式可能面临OOM(out of memory)的问题,使用类似流系统的运行模式(其实是流系统借鉴了数据库的运行模式),每个操作符既是producer,又是consumer。但对于一些操作,如sort数据会阻塞等待所有数据,需要spill to disk操作。
- vectorization model/batch model:一批一批数据处理,
排序和聚合
排序
需要排序的sql语句:Order By, Distinct
实现:
- 外部归并排序(external merge sort) 用读写数据的次数衡量算法
- 读取B+树的索引结果
聚合
-
单项聚合:聚合之后的结果是一个单项值
只需要保存聚合中间值,根据新输入值不断更新即可,且内存消耗不大
-
组队聚合:结合group by的聚合
使用排序或者哈希表来实现
遇到内存不够大的问题,使用外部哈希表,借助文件系统暂存数据
JOIN
Table A: M个block m个row
Table B: N个block n个row
Join基于equally join
Join算子实现
-
NestedLoopJoin
-
SortMergeJoin
类似Merge的过程,每个表维护一个指针,根据Join的键做排序
时间复杂度 M+N+2M*log2M + 2N *log2N
-
HashJoin
时间是线性的,在绝大多数情况下,性能是最佳的
优化器
优化运行时间
Query Rewrite
在这一步对原来的语法树进行等价的语义重写,优化掉无效或者无意义的操作
常见规则
- Projections push down,把需要哪些column直接下推到叶节点的table scan,可以减少数据的大小并提高scan的速度
- Predicates push down,把filter predicates往下推送
- Impossible/Unnecessary Predicates: 计算Predicates的值,如果恒为false( where 1 = 0 )直接返回空集,如果恒为true,直接去掉predicates
- Merge Predicates:优化器把多个predicates合并
- Join elimination
Heuristic approach
优化器如何决定join的顺序来找到最优解?
n个表join的搜索空间是4n,首先要减小搜索空间。
在最早的Valcano Optimizer中,提出了对于Join Order,只考虑left-deep-tree
![](https://static001.infoq.cn/resource/image/7c/95/7cc616f9240851b6f00bf782c1ac0095.png)
Cardinality Estimation
用于预测哪两个表join之后的cardinality比较小,来决定join ordering。帮助决定logical operator
Selection cardinality:
SC(P,R)表示当predicate是P的时候,对于表R,最后大约会有多少条row输出
需要知道表的一些基本的元信息: 如总行数,表中的每个column有多少个distinct value
数据库会不定期的自动更新每个表以及每个column的统计信息
Join cardinality:
SEL * FROM R1, R2 WHERE R1.a = R2.a;
公式: JC(R1, R2, R1.a = R2.a) = N_R * N_S / max(V(a, R), V(a, S))
V(a,R)表示R中的column a,一共有多少个distinct value
同时还需要知道数据的分布情况,常见的统计数据分布的方法是使用Histogram
Cost Model
不同的算子有不同的实现方法,使用Cost Model决定
每一个operator,有一个cost formula
同一个算子的不同实现方法,可以根据实际语句计算cost formula的值,选择cost比较小的
对于整个logical operator tree, 每个logical operator的选择不是局部最优的问题,使用DP解决
事务、隔离、并发
这部分笔者比较熟悉,不再详细记录
单机数据库扩展到分布式
数据库内核杂谈(十三):如何把一个单机数据库扩展成分布式数据库-InfoQ
使用一个leader节点,多个worker节点的架构,leader节点处理客户端的请求,并且对语句进行编译、绑定和优化。仅leader节点需要有优化器的实例,所有节点都会有执行器的实例。
和普通算子相比,分布式算子是节点之间的相互通信,需要RPC调用,调用的成本相对更高。