SimpleDB数据库 Lab2实现

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;

在这里插入图片描述

可见,结果正确,成功创建了一个简单数据库并提取出数据

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值