数据库内核杂谈-InfoQ
简单数据库实现
- 一个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树索引得到的结果对于索引列是有序的
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.
执行模式
用户输入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+树的索引结果
聚合
JOIN
Table A: M个block m个row
Table B: N个block n个row
Join基于equally join
Join算子实现
优化器
优化运行时间
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
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调用,调用的成本相对更高。