SimpleDB-Lab2
0.实验目标
实验二需要为SimpleDB编写一组操作符,用于实现表的修改(例如,插入和删除记录),选择操作,连接操作和聚合操作。这些操作将在Lab 1中所编写的基础上构建。此外,需要设计页面淘汰策略,完善缓冲池。仍不需要实现事务和锁。
可以使用提供的SQL解析器对数据库运行SQL查询。
- 实现Filter和Join操作符。这些操作符的Javadoc注释中包含了其工作细节。Project和OrderBy已实现,这可能有助于您理解其他操作符的工作方式。
- 实现IntegerAggregator和StringAggregator。需要编写逻辑来计算在一系列输入元组中,跨多个分组计算特定字段的聚合结果。由于SimpleDB仅支持整数,所以在计算平均值时请使用整数除法。StringAggregator仅需要支持COUNT聚合操作,因为其他操作对于字符串没有意义。
- 实现Aggregate操作符。和其他运算符一样,聚合运算符实现了 OpIterator 接口,这样它们就可以放在 SimpleDB 查询计划中。注意Aggregate运算符的输出是每次调用next()时整个组的聚合值,并且聚合构造器需要聚合和分组字段。
- 在 BufferPool 中实现与元组插入、删除和页面驱逐有关的方法。在这一点上,你不需要担心 transactions 的问题。
- 实现插入和删除操作符。像所有的操作符一样,Insert和Delete实现了OpIterator,接受一个要插入或删除的 tuple 流,并输出一个带有整数字段的单一 tuple ,表示插入或删除的 tuple 数量。这些操作者将需要调用BufferPool中的适当方法,这些方法实际上是修改磁盘上的页面。检查插入和删除 tuple 的测试是否正常工作。
P.S. 请注意,SimpleDB没有实现任何类型的一致性或完整性检查,所以有可能在文件中插入重复的记录,也没有办法强制执行主键或外键约束。
1. Filter and Join
a. 要修改的文件
- src/java/simpledb/execution/Predicate.java
- src/java/simpledb/execution/JoinPredicate.java
- src/java/simpledb/execution/Filter.java
- src/java/simpledb/execution/Join.java
b. 实现说明
SimpleDB OpIterator类实现了关系代数的操作,现在需要实现两个运算符,使你能够执行比表扫描更有趣的查询。
Filter: 这个操作符只返回满足 “谓词 “的 tuple ,”谓词 “是作为其构造函数的一部分指定的。因此,它过滤掉任何不符合谓词的 tuple 。
Join: 这个操作符根据作为其构造函数一部分传入的 “JoinPredicate “来连接其两个子代的 tuple 。我们只需要一个简单的嵌套循环连接,但你可以探索更有趣的连接实现。在你的实验报告中描述你的实现。
c. 实现目标
- 通过单元测试 PredicateTest、JoinPredicateTest、FilterTest 和 JoinTest
- 通过系统测试 FilterTest 和 JoinTest
d. 设计思路
Predicate类,即“谓词”,是用来辅助实现Filter操作的。构造函数中需要三个参数:需要进行比较tuple中的field的编号field、比较的操作符op(等于、大于等)、进行比较的值operand。
举个例子,当需要过滤出第2列大于200的tuple时,可以创建Predicate(field=2, op=GREATER_THAN, operand=200)
Predicate类的filter方法,传入一个tuple,返回该tuple是否满足“谓词条件”。
Filter类,继承了抽象类Operator。用来过滤出符合“谓词条件”的tuple,”谓词 “是作为其构造函数的一部分指定的。
可以看做一个自定义的迭代器,通过调用fetchNext()来获取符合要求的下一个tuple。
JoinPredicate类,与Predicate类类似,用来辅助Join操作。
JoinPredicate类的filter方法,传入两个tuple,返回这两个tuple是否满足“谓词条件”。
Join类,继承了抽象类Operator。用来将两张表进行连接。当且仅当表A与表B中的指定列的field的值相同时才连接。并且返回的tuple不去除重复的field值。
可以看做一个自定义的迭代器,通过调用fetchNext()来获取符合要求的下一个tuple。
e. 实现说明
- 参考OrderBy和Project类进行实现。
- 通过所有单元测试和系统测试。
2. Aggregates
a. 要修改的文件
- src/java/simpledb/execution/IntegerAggregator.java
- src/java/simpledb/execution/StringAggregator.java
- src/java/simpledb/execution/Aggregate.java
b. 实现说明
- 一个额外的SimpleDB操作符用GROUP BY子句实现了基本的SQL聚合。你应该实现五个SQL聚合(COUNT, SUM, AVG, MIN, MAX)并支持分组。你只需要支持单个字段的聚合,以及单个字段的分组。
- 为了计算聚合,我们使用一个Aggregator接口,将一个新元组合并到现有的聚合计算中。在构建过程中,Aggregator被告知它应该使用什么操作来进行聚合。随后,客户端代码应该为子迭代器中的每个元组调用Aggregator.mergeTupleIntoGroup()。在所有 tuple 被合并后,客户端可以检索到一个聚合结果的OpIterator。结果中的每个元组都是一对形式为(groupValue, aggregateValue)的元组,除非group by字段的值是Aggregator.NO_GROUPING,在这种情况下,结果是一个形式为(aggregateValue)的单一元组。
- 请注意,这个实现需要的空间与不同组的数量成线性关系。在本实验中,你不需要担心组的数量超过可用内存的情况。
c. 实现目标
- 通过单元测试IntegerAggregatorTest、StringAggregatorTest和AggregateTest
- 通过系统测试AggregateTest
d. 设计思路
Aggregate用来实现聚合操作(COUNT, SUM, AVG, MIN, MAX)的,并支持分组。
构造函数中需要传入参数:tuple迭代器child(遍历表中的每个元组)、进行聚合的列号afield、需要分组的列号gfield和聚合的符号aop。
Aggregator是一个接口,有IntegerAggregator和StringAggregator两个实现
void mergeTupleIntoGroup(Tuple)将传入的tuple加入到当前的聚合中。
OpIterator iterator()返回聚合结果的迭代器
主要是通过Map<Field, Tuple>将group field值映射到tuple,也就是分组的每个值对应一个tuple。若没有进行分组,则将field值置为null,放入map中。
目前是在每次调用mergeTupleIntoGroup()时更新map。
也可以保存max、min、sum、count等中间结果,等到OpIterator的open()时再统一添加tuple。
IntegerAggregator的实现有点粗糙:为了实时更新AVG的结果,需要知道sum和count的结果,每次重新计算平均值,因此另外设计了两个Map,来存放每个分组的sum和count。
通过所有单元测试和系统测试。
3. HeapFile Mutability
a. 要修改的文件
- src/java/simpledb/storage/HeapPage.java
- src/java/simpledb/storage/HeapFile.java
(Note that you do not necessarily need to implement writePage at this point).
b. 实现说明
现在将开始实现支持修改表格的方法。我们从单个页面和文件的层面开始。有两组主要的操作:添加 tuple 和删除 tuple 。
删除 tuple:要删除一个 tuple ,你需要实现deleteTuple。元组包含记录ID,允许你找到它们所在的页面,所以这应该很简单,只要找到元组所属的页面并适当地修改页面的标题。
添加 tuple:HeapFile.java中的insertTuple方法负责向一个堆文件添加元组。要向HeapFile添加一个新元组,你必须找到一个有空槽的页面。如果HeapFile中没有这样的页面存在,你需要创建一个新的页面并将其追加到磁盘上的物理文件中。你将需要确保元组中的RecordID被正确地更新。
为了实现HeapPage,你需要为insertTuple()和deleteTuple()等方法修改标题位图。你可能会发现,我们在实验1中要求你实现的getNumEmptySlots()和isSlotUsed()方法可以作为有用的抽象方法。请注意,有一个markSlotUsed方法作为抽象,用来修改页眉中元组的填充或清除状态。
注意,重要的是,HeapFile.insertTuple()和HeapFile.deleteTuple()方法要使用BufferPool.getPage()方法访问页面;否则,你在下一个实验的事务实现将不能正常工作。
在src/simpledb/BufferPool.java中实现insertTuple()和deleteTuple()。
这些方法应该调用HeapFile中属于被修改的表的适当方法(这个额外的间接层次是需要的,以便在未来支持其他类型的文件(例如索引))。
c. 实现目标
- 通过单元测试HeapPageWriteTest、HeapFileWriteTest和BufferPoolWriteTest
d. 实现说明
HeapPage中的插入与删除tuple
insertTuple:寻找空闲的槽位,插入新的tuple,并更新这个tuple的recoedId,调用markSlotUsed更新Page信息。
deleteTuple:调用markSlotUsed更新Page信息。
HeapFile中的插入与删除tuple
insertTuple:若有空闲的Page,则调用该Page的insertTuple插入;否则创建新的Page,再调用Page的insertTuple插入。返回更新过的Page(此处只修改了一个Page)。
deleteTuple:根据tuple的recordId信息找到对应的Page,调用Page的deleteTuple删除,并返回返回更新过的Page(此处只修改了一个Page)。
BufferPool中的插入与删除tuple
insertTuple:调用HeapFile中的insertTuple,并调用updateBufferPool()对BufferPool进行更新。
deleteTuple:调用HeapFile中的deleteTuple,并调用updateBufferPool()对BufferPool进行更新。
updateBufferPool:自己写的私有函数,用于更新BufferPool中指定的Page信息。对传入的List
,调用markDirty标记为脏页(之后需要写回磁盘)。若BufferPool中没有该页,则加入BufferPool。更新BufferPool中该页为最新传入的Page。 参考BTreeFile,实现了writePage()
测试BufferPoolWriteTest的handleManyDirtyPages中,首先创建了一个有一页的空Page的HeapFile,之后再加入10页,每页插入一条数据。然后测试文件中是否能读出十条数据。问题在于,HeapFileDuplicates的insertTuple()中是每次在文件中写入一个空页,然后创建一个新的HeapPage,再往HeapPage中插入数据,也就是数据没有写回文件中,仅仅保存在返回值List
中。 因此在BufferPool的insertTuple和deleteTuple里加一步更新BufferPool的操作,也就是将修改后发生变化的Page重新更新到BufferPool(但是可以不写回磁盘)
但是有个新的问题是,如果更新操作在BufferPool中执行,那么单独调用HeapFile的insertTuple就无法读到最新的Page信息。测试BufferPool的insertTuple中,是先在file中写入新的一页,再插入tuple,因此在HeapFile的insertTuple中对文件进行读写是可行的。因此直接在HeapFile的insertTuple操作中,若之前的Page都满了,则写入新的Page,并直接将新的Page写入磁盘。
发现之前的BufferPool有误,更正插入的第一页PageNo为0.
通过单元测试HeapPageWriteTest、HeapFileWriteTest和BufferPoolWriteTest
4. Insertion and deletion
a. 要修改的文件
- src/java/simpledb/execution/Insert.java
- src/java/simpledb/execution/Delete.java
b. 实现说明
现在你已经写好了所有用于添加和删除 tuple 的HeapFile机器,你将实现Insert和Delete操作。对于实现 “插入 “和 “删除 “查询的计划,最上面的运算符是一个特殊的 “插入 “或 “删除 “运算符,它修改了磁盘上的页面。这些操作符返回受影响 tuple 的数量。这是通过返回一个带有一个整数字段的单一元组来实现的,其中包含计数。
Insert:这个操作符将它从其子操作符中读取的 tuple 添加到其构造函数中指定的tableid’。它应该使用BufferPool.insertTuple()`方法来做这件事。
Delete: 这个操作符从其构造函数中指定的tableid中删除它从其子操作符中读取的 tuple。它应该使用BufferPool.deleteTuple()`方法来做这件事。
c. 实现目标
- 通过单元测试 InsertTest
- 通过系统测试 InsertTest 和 DeleteTest
d. 设计思路
- 操作符Insert和Delete的fetchNext()通过调用BufferPool中的insertTuple()和deleteTuple()实现。返回值为单个Tuple,TD为INT_TYPE,值为修改的tuple的数量。
- fetchNext()只能调用一次,第二次便返回null。因此设置一个私有变量,标记是否已经完成操作。
5. Page eviction
a. 要修改的文件
- src/java/simpledb/storage/BufferPool.java
b. 实现说明
在实验1中,我们没有正确观察到由构造参数numPages定义的缓冲池中最大页数的限制。现在,你将选择一个页面淘汰策略,并对以前任何读取或创建页面的代码进行编程,以实现你的策略。
当缓冲池中的页面超过numPages时,在加载下一个页面之前,应该将一个页面从缓冲池中淘汰。淘汰策略的选择由你决定;没有必要做一些复杂的事情。
注意BufferPool要求你实现flushAllPages()方法。这不是你在真正实现缓冲池时需要的东西。然而,我们需要这个方法用于测试。你不应该在任何真正的代码中调用这个方法。
由于我们实现ScanTest.cacheTest的方式,你需要确保你的flushPage和flushAllPages方法不从缓冲池中淘汰页面,以正确通过这个测试。
flushAllPages应该在BufferPool中的所有页面上调用flushPage,flushPage应该将任何脏页写入磁盘并标记为不脏,同时将其留在BufferPool中。
唯一应该从缓冲池中删除页面的方法是evictPage,它应该对它所淘汰的任何脏页面调用flushPage。
如果你没有在上面的HeapFile.java中实现writePage(),你也需要在这里实现它。最后,你还应该实现discardPage(),以便从缓冲池中删除一个页面而不写入磁盘。我们不会在本实验中测试discardPage(),但它在未来的实验中是必要的。
c. 实现目标
- 通过系统测试 EvictionTest
- 由于我们不会检查任何特定的驱逐策略,这个测试通过创建一个有16页的BufferPool(注意:虽然DEFAULT_PAGES是50,但我们初始化的BufferPool更少!),扫描一个超过16页的文件,看看JVM的内存使用率是否增加了5MB以上。如果你没有正确地实施淘汰策略,你将无法淘汰足够多的页面,并将超过大小限制,从而导致测试失败。
d. 设计思路
- flushPage():将脏页写入磁盘,并标记为不脏。(不需要从BufferPool中淘汰该页)
- flushAllPages():对BufferPool中的所有页面调用flushPage()。
- discardPage():从BufferPool中删除一个页面,并且不写入磁盘。
- evictPage():从BufferPool中淘汰一个页面。若为脏页,调用flushPage()将脏页写入磁盘;否则调用discardPage()直接删除页面。
e. 实现说明
- 更新了BufferPool,将把PageId映射到BufferPageId的Map更改为LinkedHashMap实现,这样就可以直接按照访存顺序获取到最近最久未访问的页面,便于页面淘汰。
- 相应地,将空页面的获取方式更改为了Queue实现。初始时,在队列中加载全部页面,之后每次需要时取出队列的第一个元素,将页面写入该位置的缓存。淘汰页面时,需要更新Queue,将淘汰页面的bpid重新加入Queue。
6. 小结
- 由于打包前要通过测试,所以一些各个Lab中通过打包实现的一些测试就先略过,最后再统一测试。
SimpleDB-Lab2