Database|6.830 lab1
为了对数据库系统有更深层次的理解,尝试在暑期间做了6.830的Lab。
项目展图
本项目主要实现了数据库系统,包括了存储系统Storage,查询Query框架,查询的优化器Optimizer,索引的查询B+-tree实现等。整个项目的框架大致会随着项目的逐渐深入给出。
环境配置
ant环境配置下载官网上的在ant build,按照README中的教程执行即可。
Tuple/Tuple Desc
Tuple
和TupleDesc
类较为简单,注意好已经提供了Field
的字段封装。TupleDesc
需要type
和fieldname
两个属性,一个用来标明数据的属性,一个用来记录数据的,将其这两属性包裹为TdItem
静态类,使用ArrayList
保存即可。Tuple
中包含TupleDesc
,RecordId
以及ArrayList<Field>
,qizh其中RecordId
在前几个Lab并没有作用,是在后期
1 | private TupleDesc tupleDesc; |
Buffer Pool
buffer pool是一个缓冲存储区,就是为了能够更快访问table所设立的区域
以Page
作为基本单位。使用CocurrentHashMap
来进行维护。
Page
在书写的过程中可以发现,SimpleDb
中实现的相应的Page
和大二下写过的Lsm-tree
的写法非常类似,如果做类比的话BufferPool
缓冲区就类似sst_buf
,而Page
就类似于SsTable
的写法。也就是说比起当时LSM-tree的实现,SimpleDb在页的层级进行分割,对效率有了更大的提高,减少了IO操作。
HeapFile HeapPage
在设计db的过程中,最为重要的还是Dbfile和page都是一个抽象接口,可以Dbfile是一个table在硬盘的存储接口,而一个Dbfile。而在读取的时候以Page为单位来读取,pages来存储相应的Tuples。
每个Page的结构如下,
1 | final HeapPageId pid; |
page.getId().pageno()
从对应Dbfile
读取的起始偏移量Dbfile
和Table
的关系为一个Table
对应的是一个Dbfile
。Dbfile
和Page
有不同的实现方式,这也是数据io的主要优化点,lab1中实现的是最为基础的HeapFile
和HeapPage
RecordId
RecordId是Tuple中包含的一个元素,他的目的是为了在添加以及删除Tuple
确定Tuple所在的页位置以及页中的位置,因此包含两个元素PageId
以及TupleNumber
,通过这两个成员来实现Database对于Tuple的删除和添加操作。
Database|6.830 lab2
Project
关系代数投影
算法是编写的新的TupleDesc
来做投影,子操作元素OpIterator child,Project
利用新的TupleDesc
和子操作的fetchNext
来形成新的Tuple
Filter
Filter中使用Predicate
进行Tuple判断是否符合条件,不符合条件则在fetchNext
中跳过,和Project
的逻辑类似。
Join
笔者在此实现的Join方式简单的simple nest join,也就是循环嵌套联结,通过二层嵌套来进行实现表和表的连接。事实上还有不同的实现方式,有
Agg
实现了IntegerAggregator
以及StringAggregator
。
具体思路是通过给定的gbfield
将Tuple的gbfield位相同的合在一起,调用mergeTupleIntoGroup
函数添加入组,通过op来相应对每个group进行聚合结果进行计算。其中IntegerAggregator都是对Intfield字段符的聚合,而StringAggregator是对StringField字符段的聚合,聚合操作只有COUNT
。
笔者算法上采用HashMap
存储相同的gbField
和聚合结果的对应关系,IntAggregator
中由于需要实现AVG
聚合操作,因此既要相应的结果val
,又需要相应的计数cnt
,因此建立两个HashMap
1 | if(value){ |
Add and Delete
Add
加入Tuple
的思路是先在HeapPage
中寻找empty slot
,如果有则在HeapPage
的list中加入Tuple
,同时修改该Tuple
的RecordId
;如果HeapFile
内所有HeapPage
都没有empty slot
,则调用emptyPage
创建新的HeapPage
。
需要注意的是,所有的HeapFile
中所有的getPage
都需要调用BufferPool
的getPage()
方法,来获取相应的Page
,原因一是通过BufferPool
获取Page
是可能可以减少磁盘IO操作的,二是最关键的,也是我做到Lab4才发现的,只有在这里的insertTuple
和DeleteTuple
中调用的是BufferPool
中的getPage()
方法,读和写的事务才能统一被管理,都在事务ACID协议的管辖范围内
Delete
Page Eviction
Flush Page
使用CocurrentHashMap
中value的迭代器,将所有脏页写入磁盘
1 | public synchronized void flushAllPages() throws IOException { |
LRU缓存,实现方式类似Leetcode.146
将原先维护的CocurrentHashMap
改为链表+HashMap的LRUCache
类来进行维护。双链表是为了将调用的页插入队首,并且将队尾的页去除。
PS:笔者出于练手的目的造了LRUCache
的轮子,事实上Java有自己的类似实现LinkedHashMap
Template Summary
完成Lab1以及Lab2后,Database基本的存储框架和增删查询的框架已经搭建完成,对于数据库的大抵架构也有所熟悉,我画了一张草图,来表示Database的大致结构。
Database|6.830 Lab3
Lab3主要是查询的优化,对查询语句
Lab3的Optimizer架构如下图所示,可以看到
而Optimizer中干的是两件事情:
- 第一阶段:收集表的统计信息,有了统计信息才可以进行,也就是Ex1,2做的是
- 第二阶段:根据统计信息进行估计,找出最优的执行方案。
通过创建直方图来解决filter selectivity estimation
估计相应的
TableStat.Java
TableStat
主要是用来统计表中的信息,使用实现的IntHisgram
对进行相应的实现。TableStat
通过ConcurrentMap
存储了表的名称和表的对应的关系。
算法思路如下:调用LAB2实现的SeqScan
遍历,通过第一次遍历构建Field
相应的Histogram
,如果是IntField
类则计算他的最大值和最小值,如果是StringField
类则创建StringHistogram
。
随后SeqScan.rewind()
,进行第二次遍历,TableStat
中实现以下3个Estimate:
estimateSelectivity
estimateScanCost
estimateTableCardinality
计算这些代码都是基于1
2joincost(t1 join t2) = scancost(t1) + ntups(t1) x scancost(t2) //IO cost
+ ntups(t1) x ntups(t2) //CPU costSeqScan
来进行相对应的书写基数估计
Cardinality是指表中唯一值的行数,即是选择出来的行中该字段符应当不重复,对于有唯一约束的列,“基数”等于表的总行数。而选择性指的(Selectivity)的意思就是字段可取值相对于总行数的比值,公式如下:选择性 = 基数 / 总行数 * 100%
Join优化
根据相应的知识查询抗压发现,Join优化有两种,也就是先Filter再Join,会相应减少Cpu的操作
并且JoinOptimizer
会相应的computeCostAndCardOfSubplan
返回了CostCard
Database|6.830 Lab4
Transacation遵守ACID原则,ACID实现的前提是严格两阶段封锁协议(Strict two-phase locking)
两阶段封锁协议将事务分为两个阶段:
- 增长阶段:事务可以获得锁,但不能释放锁,锁的数量单调不减。
- 缩减阶段:事务可以释放锁,但不能获得新锁。锁的数量单调不增。
共享锁(Shared lock)以及排他锁(Exclusive lock)的区别和联系。
注意的是,一个事务t可能会多次访问同一个Page
,而他说是锁实现使用类的等待获取来做到的,类似于读写锁。
下面有一张思维导图,很好地阐述了PageLock
的工作逻辑
基于这张思维导图开始写PageLock
页级别的锁和LockManager
类来对BufferPool
中的锁进行统一管理。
PageLock实现
首先使用枚举类型指定PageLock
的两个类型EXCLUSIVE
和SHARED
,并且按照上面的思维图对应PageId
的Page
Database|6.830 Lab5
未完待续……