lab2实现数据库的一些基本操作,例如Insert和Delete等,从而能够对数据库中的数据进行更改以及分组或过滤挑选等。Lab2中的实现会在Lab1的基础上进行,并且还会对Lab1的一些方法进行完善和修改。
代码链接:SimpleDB Lab2
提交历史
设计思路
Exercise1 Filter and Join
在这部分会实现两个操作符:Filter 和Join,在此之前会需要实现SimpleDB中的Predicate和JoinPredicate类,这两个类相当于一个需要满足的条件,Filter会返回满足Predicate的tuples,而Join会根据JoinPredicate连接tuples。
Predicate
在这个类的作用和结构大概如下:
private final int field;
private final Op op;
private final Field operand;
-
Op:
需要满足的关系符号(Operator),包括大于、大于等于、等于等; -
field:
实际上是被比较的属性的序号,在进行Filter时,会将Tuple的某一属性与某个操作数(operand)进行比较,此处的field就是该属性的序号 -
operand:
即被用于比较的操作数,当tuple与这个operand间满足Op关系时(tuple.getField(field).compare(op,operand)== True),则会把该tuple加入到结果。
JoinPredicate
这个类的结构与作用与Predicate大致相似:
private final int field1;
private final int field2;
private final Predicate.Op op;
-
filed1 : 是第一个Tuple中被比较的Filed的序号;
-
field2 : 是第二个Tuple中被比较的Field的序号;
-
op : 两个Tuple中指定的属性需要满足的关系
Filter
这个类相当于一个过滤器,过滤出满足Predicate的Tuples,其结构和重要方法如下:
-
成员变量Predicate p:即要满足的条件;
-
成员变量 OpIterator child:child实际上是一个Tuple类型的迭代器,这个类会从中读取Tuple来进行过滤;
-
成员方法 fetchNext():这个函数会从child中找寻下一个符合Predicate的Tuple并返回,如child已经迭代到底,则返回null。
其余详细代码可见仓库
Join
Join操作符用于联合两个来自不同table的Tuple,当它们符合条件时将其连接,重要变量和方法的设计思路如下:
-
成员变量 JoinPredicate : 即两个Tuple要连在一起时需要满足的条件;
-
成员变量 OpIterator child1 :从中读取进行比较的第一个Tuple;、
-
成员变量 OpIterator child2 :从中读取进行比较的第二个Tuple;
-
成员方法getTupleDesc():要调用TupleDesc中的merge()方法,联合两个TupleDesc;
-
成员方法 fetchNext():不同于Filter简单遍历即可,Join中需要进行双层遍历,外层是child1中的Tuple,内层是child2中的Tuple,从而遍历完所有Tuple的组合,并联合满足要求的。
其余详细代码可见仓库。
Exercise2 Aggregates
在这部分要实现基础的SQL Aggregate,即分组聚合。Lab2中只要求完成(SUM
COUNT AVG MAX MIN)五种运算符条件下的分组聚合,而如果被用于参考进行聚合分组的属性是一个String类型的属性,则只用实现COUNT运算符下的分组。此部分要实现InterAggregate
StringAggregate 以及Aggregate。
InterAggregate和StringAggregate
这两个类之间大同小异,只是StringAggregate只支持COUNT运算符,大致的结构和重要方法如下:
private int gbfield;
private Type gbfieldtype; //gbfield的类型
private int afield;
private Op what; //进行Group分组时的操作符
private TupleDesc td; //聚合后返回的Tuple的TupleDesc
private Map<Field,Integer> groupMap; //分组后的结果
private Map<Field , Integer[]> avgMap;
-
gbfield和afield:这两个field容易混淆,gbfield是用于groupby的参考属性,会根据这一属性进行分组;而afield则是想要得到的groupby后的结果属性。例如有一张包含若干学生各科成绩的表,想要获取其每个人总成绩是可以用
SELECT sum(成绩) FROM 成绩表 GROUP BY 姓名
则gbfield是姓名,afield是成绩;
-
Td: 是返回的分组后的tuple的TupleDesc,一般情况下是(groupValue, aggregateValue),而当分组依据gbfield=NOGROUPING时,只返回一个(aggregateValue)
形式的tuple; -
Map<Field,Integer> groupMap: 这个Map中的键是不同的gbfieldtype,对应的值是group后的结果,如sum或max或count等;
-
Map<Field , Integer[]> avgMap:这是为了平均值操作符AVG单独创建的Map,数组Interger[]中会有两个值,一个是个数COUNT,一个是总和SUM,这样设计的原因是考虑到之后会实现SUM_COUNT和SC_AVG,则在这个Map中可以方便的同时读取COUNT与SUM,再进行进一步运算;
-
成员方法mergeTupleIntoGroup(Tuple):这个方法会将Tuple进行分组,但要注意当分组标准是NOGROUPING时,则不会进行严格分组,而是全部放到一起。
在这个函数中要根据Op的种类分类讨论,如果是MIN,则groupMap中的value就是该组的最小值,以此类推。
-
一个OpIterator类型的迭代器iterator:这个迭代器返回分组后的Tuples。可以创建一个ArrayList,再遍历一遍groupMap,将Tuple加入进去,最后返回这个ArrayList的迭代器即可。
Aggregate
这部分会根据一个Aggregator进行分组,大致的设计思路如下:
private OpIterator it; //需要被groupby分组的tuples的迭代器
private int afield; //与上一节的afield含义相同
private int gfield; //与上一节的gbfield含义相同
private Aggregator.Op aop; //groupby分组时的运算(MIN,MAX,AVG...)
private Aggregator aggregator; //分类器,根据gfield的类型创建为IntegerAggregator或StringAggregator
private TupleDesc child_td; //是it中需要被分组的Tuple的TupleDesc
private TupleDesc td; //分组后的TupleDesc,如果是NOGROUPING,则是简单的(aggregateValue),此外一般都是(groupValue, aggregateValue)
private Type gbfieldtype; //gfield的类型
private Type afieldtype; //afield的类型
private OpIterator opIterator; //分组后的Tuples的迭代器
重要的方法有:
-
构造函数Aggregate():在构造函数中,会根据gfield的类型建立相应的aggregator和TupleDesc;
-
opIterator的相关函数:如open()和close()等,在open()中会调用aggregator的mergeIntoGroup来进行分组,并且构造成一个迭代器,让其他子操作符使用。
Exercise3 HeapFile Mutability
从这部分开始实现tuple的insert和delete,首先实现在HeapFile和HeapPage的物理层面进行的insert和delete,因此需要实现HeapFile和HeapPage中剩余的一些方法[]{#exercise3}。
HeapPage
HeapPage中有一个Tuple类型的数组Tuples,并且由Lab1知HeapPage有一个BitMap用于记录各块Tuple是否记录了数据,因此在page中插入或删除一个Tuple,会需要更新Tuples中的值和BitMap对应的位,要实现的重要方法有:
- markSlotUsed(int i, boolean value):这个函数会标记第i位BitMap为value(存有数据为1,无数据为0),此处设计用到了位运算,能方便的转换第i为的值:
byte b = header[Math.floorDiv(i, 8)];
byte m = (byte) (1<<(i%8));
if(value){
header[Math.floorDiv(i,8)] = (byte) (b|m);
}else{
header[Math.floorDiv(i,8)] = (byte) (b&(~m));//利用位运算方便地实现headers头的更改
}
-
insertTuple(Tuple t):该函数会先遍历BitMap,找到空(为0)项,然后在Tuples数组的此处放入t,注意要设置t.RecordId;
-
deleteTuple(tuple t): 先从t.RecordId中获取t的序号,然后将BitMap对应位置0,再将Tuples中此处tuple置为null。
-
此外还有与脏页有关的函数如markDirty等,主要是用于获取更改此页数据的Transaction和设置此页是否为脏页等,这些函数实现较为简单,主要和之后BufferPool中的insert和delete有关。
HeapFile
此部分也是实现Tuple的insert与delete:
-
insertTuple(TransactionId tid, Tuple t):遍历HeapFile中的HeapPage,找到有空位的一页,然后在这一页中调用HeapPage.insertPage(t),如果该HeapFile没有空页,则为其写入一页空页,
HeapPageId newid = new HeapPageId(this.getId(),numPages());//ceate a new page, HeapPage blankPage = new HeapPage(newid,HeapPage.createEmptyPageData()); numPage++; writePage(blankPage);
然后在这一页中插入Tuple,然后返回这一页,并将其设置为dirty。
-
deleteTuple(Transaction tid, Tuple t):根据t的RecordId找到对应Page的Id,然后在HeapFile中找到此页,并删去t。
-
writePage(Page page):往现有的HeapFile中继续写入一页数据:
try(RandomAccessFile f = new RandomAccessFile(File,"rw")){ f.seek(page.getId().getPageNumber()*bp.getPageSize()); byte[] data = page.getPageData(); f.write(data); }
BufferPool
BufferPool中也是实现insertTuple和deleteTuple函数:
-
insertTuple(Transaction tid, int tableId, Tuple t):首先从Catalog中根据tableId找到对应要插入Tuple的表table,再调用HeapFile.insertTuple方法,插入Tuple,并将该方法返回的操作过的Page标记为dirty,然后将插入后的Page放入BufferPool。注意,当BufferPool已满时,要进行内存页的汰换evictPage()。
HeapFile table = (HeapFile) Database.getCatalog().getDatabaseFile(tableId); ArrayList<Page> affectedPages = table.insertTuple(tid,t); for(Page page : affectedPages){ page.markDirty(true, tid); if (idToPage.size() == numPages) { evictPage(); //when bufferpool is full, evict page } idToPage.put(page.getId(), page); }
-
deleteTuple(Transaction tid, Tuple t): 通过t的RecordId找到PageId,进而找到tableId,然后在Catalog中找到该table,调用HeapFile.deletePage删去Tuple t;
Exercise4 Insertion and deletion
该部分实现Insert和Delete的操作符,通过调用BufferPool的insertTuple和deleteTuple实现插入与删除。
Insert
该部分的大致结构和重要方法有:
private TransactionId t; //执行insert操作的业务
private OpIterator child; //待插入的Tuples的迭代器,来自其他操作符运算后的结果
private int tableId; //要插入的表的id
private int count; //已经插入的Tuples的数量
private TupleDesc returnTP; //在fetchNext中要返回一个仅一个Field的Tuple,值为count
private boolean isAccessed; //该insert操作是否已经调用
protected Tuple fetchNext() throws TransactionAbortedException, DbException {
// some code goes here
if ( isAccessed )
return null; //已经调用则返回null
isAccessed = true;
while (this.child.hasNext()) {
Tuple t = this.child.next();
try {
Database.getBufferPool().insertTuple(this.t, this.tableId, t); //不断插入child中的Tuple
this.count++;
} catch (IOException e) {
e.printStackTrace();
break;
}
}
Tuple tuple = new Tuple(this.returnTP);
tuple.setField(0, new IntField(this.count));
return tuple; //返回一个要求的Tuple
}
其余方法都较为简单,都是child迭代器的open()与close()等,详细可见仓库。
Delete
Delete与Insert大同小异,都有一个OpIterator ffchild,其中是需要被删去的Tuples,Delete中重要的方法即是fetchNext函数,一个个删去child中的Tuple,然后返回一个单Field的Tuple,值是被删去的Tuples的数量count。基本上与Insert相同的结构与方法,详细可见仓库。
Exercise5 Page eviction
先前在插入Tuple时提到,如果此时BufferPool中Page已满,则调用evictPage()方法汰换内存页,从而放入新的插入了Tuple的一页。
该部分在进行汰换内存页时,用到了最近最少使用页面置换算法的思想,将最近最少使用的页面替换掉,这样能防止较常调用的页面被替换从而降低效率。大致要实现的重要方法有:
- flushPage(PageId
pid):该方法用于将缓冲区BufferPool中的内存页刷新至磁盘,以便如若要将该页替换出去时发生数据丢失。此方法调用writePage即可:
HeapPage dirty_page = (HeapPage) idToPage.get(pid); //要刷新至磁盘的内存页
HeapFile table = (HeapFile) Database.getCatalog().getDatabaseFile(pid.getTableId());
table.writePage(dirty_page); //写至磁盘
dirty_page.markDirty(false, null); //更改脏页
-
discardPage(PageId pid):删去一页,只用在BufferPool中建立的idToPage中调用remove方法即可;
-
flushAllPages():对所有内存页调用flushPage()。该函数只为了测试用,此外不可调用;
-
evictPage():此方法是核心。在设计汰换方法时,我先在BufferPool中新建了一个链表recentUsedPages用于记录最近使用的内存页,以及一个moveToHead()函数,用于将最近调用的一页移至顶部:
private LinkedList<Page> recentUsedPages; private void moveToHead(int i){ Page page = recentUsedPages.get(i); recentUsedPages.remove(i); recentUsedPages.add(0,page); }
在getPage()函数中将调用moveToHead(),而在新建页的时候会调用recentUsedPages.add(0,newPage)。由此,在evictPage()中只用删去该链表最后一项的页即可。
Page page = recentUsedPages.removeLast(); //find the last used page try{ flushPage(page.getId()); //flush this page to the disk }catch (IOException e){ e.printStackTrace(); } discardPage(page.getId()); //remove it from the BufferPool
重难点
Exercise2 中的Aggregator
由于要实现不同运算符(MIN、MAX、AVG…)情况下的分组,gruopby的方案就不一样,需要谨慎考虑。同时在实现AVG时用了与其他运算符不同的方法,这是考虑到未来实现SUM_COUNT的方便,具体可见Exercise 2中的实现思路。
Exercise3 中的insert与delete
该部分是我认为Lab2中的重难点,Exercise3中实现insertTuple和deleteTuple是一环扣一环,首先实现HeapFile和HeapPage中的insert和delete方法,要修改对应的BitMap等如果HeapFile中没有了空页,还要调用writePage新建空白页再插入。而在BufferPool中则是在顶层调用HeapFile的insert和delete方法进行修改,具体可见Exercise 3中的实现思路。
Exercise5 evictPage
此部分尝试了不同的替换方案,一开始直接打算替换掉idToPage中的第一个Page,但是这样没有考虑到数据库运行的效率,最理想的情况自然是替换掉最不常使用的内存页,于是新增一个表示使用情况的链表recentUsedPages,并根据BufferPool中内存页的调用实时更新,替换时只用删去最后一项即可,用到了LRU算法的思想,具体可见evictPage的实现思路。
改动部分
在Lab1基础上做了一些改动,主要是在HeapFile 和BufferPool两个个文件中。
HeapFile
- HeapFile中更改了numPages()函数,这是因为在调试进行BufferPoolWriteTest时handleManyDirtyPages报错,原因是没有正确地返回HeapFile中的页数,导致无法正确在BufferPool中放入新页。这是由于在Lab1中我用了一个int numPages变量相对静态的存储页数,而没有想到为HeapFile写入新页时Page数目的变化,导致出错,因此Lab2中改为:
public int numPages() {
numPage = (int)(File.length()/(bp.getPageSize()));
return numPage;
}
BufferPool
-
完善了getPage():Lab1中粗略写了getPage函数,但是没有考虑满页时的替换策略,Lab2中完善了该函数,当满页时执行evictPage()释放空间。
-
新增recentUsedPages链表以记录最近使用内存页,在evictPage()时会删去最末尾项。
Query walkthrough
可见返回结果符合
SELECT *
FROM some_data_file1, some_data_file2
WHERE some_data_file1.field1 = some_data_file2.field1
AND some_data_file1.id > 1
Query Parser
新建以下数据:
创建以下catalog.txt:
创建数据库:
执行SQL语句select d.f1, d.f2 from data d;
可见,结果正确,成功创建了一个简单数据库并提取出数据