前情提要
从11月中旬开始,花了大概一个多月时间把CS61B的所有lab和hw写完了,然后开始搜索下一门准备刷的课,发现MIT6.830评价不错,而且是基于Java语言,正好无缝衔接CS61B,于是决定就是它了。时间来到2021年12月底,西安封城,周围一切都按下了暂停键,我也因此可以专心写lab了...(笑
0.环境搭建
课程官网提供slides和notes,以及丰富的课外阅读材料,B站有生肉版录制视频,不过录制质量不佳。个人建议可以去听CMU15-445,然后回来完成本课程的lab。
git clone一下:
git clone git@github.com:MIT-DB-Class/simple-db-hw-2021.git
值得一提的是,实验文档写的非常详尽,README里有一个简单的git教学,可谓从零开始手把手教学,生怕你学不会。SimpleDB使用Ant编译代码和运行单元测试,我们需要自行下载Ant并添加环境变量。然后执行命令ant eclipse构建项目(切记,一定要先构建项目,不然test时会因为找不到junit文件报错),最后在IDEA里open project,环境就搭建完成,然后可以开始愉快写代码了(划掉
1.实验开始
1.1
TupleDesc是对存储数据(tuple)的描述,包括数据类型和字段名称,数据类型由枚举类Type定义,SimpleDB仅支持int和固定长度的string两种数据类型,框架代码里给我们提供了Filed接口,并提供了IntFiled和StringField两种实现,为我们封装好了具体的数据类型(type)和存储值(value)的信息。Tuple存储具体的信息,可以理解为Field对象的集合,其实就是存储数据的一行,包括TupleDesc中的描述信息和Field中具体的值,是底层存储数据的具体实现。these are straightforward
1.2
Catalog其实就是一个目录,记录所有当前存在数据库中的tables,并且要求我们实现add和get方法。根据addTable注释和函数签名,可以抽象出table由file,name, pkeyFiled三个属性构成,因此我们可以自定义一个table类。其实这是一个简单的映射,所以考虑用map来实现, key为tableid, value为table。另外Catalog是一个单例对象,在使用的时候应该通过Database.getCatalog()获取。
1.3
BufferPool缓存最近被读取到内存中的pages,包括固定数量的pages,所有对磁盘中不同file里的page的读写操作都是通过缓冲池。BufferPool管理的对象是Page,所以同样使用map实现,key为PageId,值为Page。同样缓冲池在数据库里也是单例对象,通过静态方法Database.getBufferPool()拿到。
-
获取由pageid指定的page,首先在缓冲池里查找,如果page不在缓冲池里,我们从数据库中通过
Catalog拿到file,然后将相应的page写入缓冲池;最后直接返回。这里值得一提的是,数据库中数据以块(block)为单位,在SimpleDB中,其实就是HeapFile;内存里,数据以页(Page)为单位。
public Page getPage(TransactionId tid, PageId pid, Permissions perm) throws TransactionAbortedException, DbException { // some code goes here if (!pageBuffer.containsKey(pid.hashCode())) { DbFile dbFile = Database.getCatalog().getDatabaseFile(pid.getTableId()); Page page = dbFile.readPage(pid); pageBuffer.put(pid.hashCode(), page); } return pageBuffer.get(pid.hashCode()); }
1.4
Access methods提供了根据某一种方式读写磁盘里数据的实现,常见的例如heap files和B-trees。HeapFile对象由一系列pages构成,每一个page都由固定大小字节组成。在SimpleDB里HeapFile和table对应。*Pages存储在缓冲池里但通过HeapFile读写。 *HeapPage被划分为若干个槽(slot),每一个槽存储一个元组(tuple),table里每一个tuple大小都一样。另外,HeapPage还包括一个头部(header),用来标记某一个槽是否被使用,其实就是一个由byte组成的数组,或者叫位图(bitmap),用1表示该槽被使用,0表示被删除或者没有初始化。这里需要注意的是,实验要求使用最低位记录最早存在于file里的槽的使用情况,也就是第一个槽的情况。举个🌰,假设头部状态如下:
header = {[0001 1011], [1100 0001]}
则表示第0,1,3,4,8,14,15个槽被使用。所以判断某个槽是否被使用,其实就是基本位运算的运用:
public boolean isSlotUsed(int i) {
// some code goes here
int byteIndex = i / 8;
int bitIndex = i % 8;
return (header[byteIndex] >> bitIndex & 1) == 1;
}
每一个tuple需要的大小为tuple size * 8 bits + 1 bit,其中header占用一位,因此每一页可以存储的tuple数目为:
floor((_page size_ * 8) / (_tuple size_ * 8 + 1)) 。
需要多少bytes存储header可以很容易计算出来:
headerBytes = ceiling(tupsPerPage/8)。
-
HeapPageId用来标识page,是PageID接口的实现。通过tableId+pageNumber确定page所属哪个file,以及在其中的序号。RecordId用来标识tuple,通过pageID+tupleNumber确定tuple所在的页以及在其中的序号。
- 实现迭代器方法的时候,我们需要注意要跳过没有被使用的槽,可以通过上述
isSlotUsed方法进行判断。
1.5
HeapFile负责从磁盘上读写文件,是DbFile的实现,无序存储tuple。HeapFile可以认为是HeapPage的集合。readPage方法根据PageId从磁盘上读取page,这里的难点在于,我们需要先确定偏移量,然后移动指针,再开始读取。然后构造相应的页。另一个比较难的地方在于实现HeapFile.iterator(),这个迭代器需要遍历HeapFile里每一页的所有tuple,迭代器需要通过BufferPool.getPage()方法将page加载到内存中。这里我们需要自定义一个迭代器,内部通过HeapPageId去获取每一页,然后在获取每一页的迭代器。同时重写hasNext()和next()方法。
1.6
SeqScan简单对上述HeapFile的迭代器进行封装即可。
总结
Lab 1主要实现SimpleDB的基础架构,总的来说应该算比较容易。但是,自己在写代码的过程中因为不太理解某一模块具体实现什么功能,导致写的迷迷糊糊的。因此很多东西,写完之后才明白究竟有什么用。所以,认真整理和输出一份覆盖代码逻辑以及设计细节的实验报告是很有必要的,可以帮助自己理清思路。另外其实很早就写完lab1,但因为拖延症晚期,一直到现在才写完实验报告,以后还是要逼着自己多去总结复盘写作输出🤣最后,因为疫情被关在学校快一个月,现在孩子终于要回家了,给大家拜个早年!希望这个寒假可以通关所有实验~
2022.1.23