MIT6.830-lab2-SimpleDB Operators(数据库的操作算子Filter、Join、Aggregates、Insertion and Deletion,以及页LRU淘汰策略)

👉 代码获取和项目文档

任务介绍

在6.830的Project2中,我们要实现常见的SQL语句所需要的数据库操作,在Project1中,我们已经实现了一种顺序扫描的操作SeqScan,它的实现方式是利用SimpleDB的Heap File类提供的迭代器来遍历整个数据表文件中的所有元组信息。而在Project2中,我们需要实现几种更复杂的算子,包括:

  • Filter:按条件筛选符合的元组;
  • Join:将两个表中符合条件的元组进行join操作;
  • Aggregate:按照一定的目标对表中的元组进行聚合运算;
  • Insert和Delete:插入、删除元组。

同时,对于需要修改文件内容的操作(这里只有Insert和Delete会修改已有文件中的内容,其他的都是另外生成新的内容),我们还需要在Buffer,File和Page三个层面来实现对应的修改操作,使得数据库能够保证一致性。

还有一项重要任务就是新增一个Buffer中的页淘汰策略。

exercise 1 - Filter,Join

主要是实现以下的类:

  • src/java/simpledb/execution/Predicate.java
  • src/java/simpledb/execution/JoinPredicate.java
  • src/java/simpledb/execution/Filter.java
  • src/java/simpledb/execution/Join.java

(1)Predicate

Filter操作是按照一定的条件对数据表中的元组进行过滤,这个判断的过程在SimpleDB中被抽象成了一个叫做Predicate的类,Predicate就是比较tuple某个字段的值与指定的值是否满足判断,这部分没什么难度,贴出成员变量:

private int field;
private Op op;
private Field operand;
  • 需要判断的属性值的列号

  • 判断的类型(大于,小于,等于,不等于),在SimpleDB中被抽象成了一个枚举类Op,具体操作如下:

    if (this == EQUALS)
        return "=";
    if (this == GREATER_THAN)
        return ">";
    if (this == LESS_THAN)
        return "<";
    if (this == LESS_THAN_OR_EQ)
        return "<=";
    if (this == GREATER_THAN_OR_EQ)
        return ">=";
    if (this == LIKE)
        return "LIKE";
    if (this == NOT_EQUALS)
        return "<>";
    
  • 判断的基准值,用之前说到过的Field类型来表示。

(2)JoinPredicate

JoinPredicate和Predicate其实很像,只不过比较的是两个tuple,也就是用于表join操作时的比较。看下成员变量:

private int field1;
private Predicate.Op op;
private int field2;

唯一的不同点在于保存的是两个field序号,分别在一个tuple中取出指定的field进行比较。

(3)Filter

Filter操作就是进行具体的过滤了,先看下成员变量:

private Predicate predicate;
private OpIterator child;
private TupleDesc tupleDesc;
private Iterator<Tuple> it;
private final List<Tuple> childTuple = new ArrayList<>();

大概得执行逻辑是:

  • 通过构造器拿到predicate和child,也就是过滤的判断过程和要过滤的所有tuple;
  • 最终的所有过滤结果是保存到it(一个迭代器)中,也就是通过调用这个filter的open()方法就会进行过滤,将满足条件的tuple全部保存到childTuple中,然后返回一个过滤器。
  • 通过这个过滤器可以获取所有满足条件得到tuple。

这里注意一下这个OpIterator,在lab1中,我们是不是实现了一个SeqScan,而这个SeqScan其实就是一个实现了OpIterator的迭代器,也就是,当我们如果没有建立索引,那我就是通过SeqScan顺序读取所有的tuple数据,然后传入Filter。

(4)Join

Join操作是对两个数据表进行操作,将分别来自于两个表的,满足一定条件的元组a和b合成一个新的元组c,并且c的所有属性是a和b汇总得到的,比如a元组的是 ( a 1 , a 2 , a 3 ) (a_1,a_2,a_3) (a1,a2,a3), b元组是 ( b 1 , b 2 ) (b_1,b_2) (b1,b2) 且ab满足join的判断条件,那么新生成的c就是 ( a 1 , a 2 , a 3 , b 1 , b 2 ) (a_1,a_2,a_3,b_1,b_2) (a1,a2,a3,b1,b2)

而join操作也需要进行条件判断,和Filter不同的是,这里的判断的参数变成了两个元组,所以SimpleDB设计了一个类Join Predicate,也就是前面那个。

先看下成员变量:

private JoinPredicate joinPredicate;
private OpIterator child1;
private OpIterator child2;
private TupleDesc tupleDesc;
private Iterator<Tuple> it;
private final List<Tuple> childTuple = new ArrayList<>();

Join其实和Filter差不多,区别在于:

  • tupleDesc保存的是join中两张表的tupleDesc合并;
  • 比较时,是进行一次时间复杂度为O(n*n)的双层循环比较,以child1为驱动表,进行判断。

exercise 2 - Aggregates

主要是实现以下类:

  • src/java/simpledb/execution/IntegerAggregator.java
  • src/java/simpledb/execution/StringAggregator.java
  • src/java/simpledb/execution/Aggregate.java

Aggregate操作是对数据表中的元组按照一定的规则进行聚合,常见的Aggregate操作包括:

  • 计数类Count
  • 求和类Sum / Avg
  • 最值类Max / Min

Aggregate常常和Group By一起使用,Group By就是聚合的依据,当它指定了某个属性之后,该属性相同的元组会被聚合成一个结果,这时候Aggregate操作返回的结果就是形如**(groupValue, aggregateValue)** 的元组,对每个不同的group value分别进行聚合,而如果没有Group By关键字,那么返回的结果就只剩一个aggregateValue,在lab2中,我们需要先分别实现两种不同数据类型(Int 和 String)各自的聚合运算,然后合并成SimpleDB的聚合运算符。

而判断有没有group主要是两种判断方法(任意一个都可以):

  • gbField为-1;
  • 分组的结果Map<Field, List<Field>> group中,key为null;

其实聚合操作的整体思想和Filter很像,都是将具体的过滤条件封装到一个类中,然后遍历每一个tuple去判断,将满足条件的field存入Map<Field, List<Field>>中,其中如果没有group by的话key就是null,也就是只有这一个key。然后使用迭代器的思想,将Map进行聚合操作后产生一个List,然后以此获取到结果的迭代器。

值得注意的是,在我们获取最终的结果时,需要自己定义返回结果的TupleDesc(就是上面提到的两种格式,判断依据是有没有进行group)。也就是迭代器输出的结果中,每一个tuple的TupleDesc都是根据具体情况进行定义的,而具体哪一种TupleDesc就是上面的两种判断方式,并且tuple的field个数判断方式也是一样的。

现在开始讲一下这三个类文件的实现和自己定义的结果集迭代器。

(1)Aggregate

Aggregate并不是真的进行聚合操作,就相当于exercise 1中filter,会根据传入的条件封装一个IntegerAggregator或StringAggregator,然后对每一个tuple进行判断。

看下成员变量:

private OpIterator child;
private int afield;  //要聚合的field
private int gfield;  //group by的field
private Aggregator.Op aop;  //聚合的操作
private final Aggregator aggregator;  //具体的聚合器,用于过滤
private OpIterator opIterator;  //最终结果的迭代器

具体的逻辑方面,主要就是聚合器的构造要注意一下。根据有没有group by字段,以及聚合字段是什么类型分别进行创建:

if(gfield!=-1){
    if(fieldType.equals(Type.INT_TYPE)){
        //这里要改一下
        aggregator = new IntegerAggregator(gfield,child.getTupleDesc().getFieldType(gfield),afield,aop);
    }else if(fieldType.equals(Type.STRING_TYPE)){
        aggregator = new StringAggregator(gfield,child.getTupleDesc().getFieldType(gfield),afield,aop);
    }else{
        aggregator = null;
    }
}else{
    if(fieldType.equals(Type.INT_TYPE)){
        aggregator = new IntegerAggregator(gfield,null,afield,aop);
    }else if(fieldType.equals(Type.STRING_TYPE)){
        aggregator = new StringAggregator(gfield,null,afield,aop);
    }else{
        aggregator = null;
    }
}

然后open()函数中,根据传入的数据迭代器迭代每一个tuple,用聚合器进行聚合,以此生成一个最终结果的迭代器。

(2)IntegerAggregator

这就是具体聚合字段为Integer类型的聚合器了。

private int gbfield;
private int afield;
private Type gbfieldType;
private Op what;
private Map<Field, List<Field>> group;

其实就是根据Aggregate调用mergeTupleIntoGroup函数,将同一组的field写入到map的对应value中。这个group就是聚合的中间结果集,key是group by字段或者null,List<Field>就是当前组的所有聚合字段。

看一下具体的逻辑:

public void mergeTupleIntoGroup(Tuple tup) {
    // some code goes here
    Field aField = tup.getField(afield);
    Field gField = null;
    if(this.gbfield!=-1){
        gField = tup.getField(this.gbfield);
    }
    if(this.group.containsKey(gField)){
        group.get(gField).add(aField);
    }else{
        List<Field> list = new ArrayList<>();
        list.add(aField);
        group.put(gField,list);
    }
}

就是对传入的tuple进行分组,传入到对应的Map<Field, List<Field>>结构中。

public OpIterator iterator() {
    return new AggregateIter(group,gbfield,gbfieldType,what);
}

主要就是根据group进行构建迭代器。这个迭代器就是自己定义的迭代器了。

(3)StringAggregator

这个类的实现和IntegerAggregator基本相同,就不说了。

(4)AggregateIter

这个迭代器应该是这个exercise中最重要也是最难实现的一部分了,主要就是对group中的每一组field进行相应的聚合操作。

先看下成员变量:

private Iterator<Tuple> tupleIterator;
private Map<Field, List<Field>> group;
private List<Tuple> resultSet;  //用来存储聚合后的数据
private Aggregator.Op what;
private Type gbFieldType;
private TupleDesc tupleDesc;  //如果有group by就是两条,否则就是一条。
private int gbField;

tupleIterator就是最终聚合结果所有tuple的迭代器。

tupleDesc是要自己根据有无group by进行定义的,如果gbField=-1,那么tupleDesc就只有一个Type,也就是全部数据的聚合结果(在构造器中进行判断):

if(gbField!=-1){
    Type[] type = new Type[2];
    type[0] = gbFieldType;
    type[1] = Type.INT_TYPE;
    this.tupleDesc = new TupleDesc(type);
}else{
    Type[] type = new Type[1];
    type[0] = Type.INT_TYPE;
    this.tupleDesc  = new TupleDesc(type);
}

而在该迭代器的open()函数中,就是进行具体的聚合操作,并放入到结果集中,其实max、min、count这些聚合操作大体逻辑相同,这里就列出min的来看看:

...
}else if(what == Aggregator.Op.MIN){
    for(Field field:group.keySet()){
        int min = Integer.MAX_VALUE;
        Tuple tuple = new Tuple(tupleDesc);
        for(int i=0;i<this.group.get(field).size();i++){
            IntField field1 = (IntField)group.get(field).get(i);
            if(field1.getValue()<min){
                min = field1.getValue();
            }
        }
        if(field!=null){
            tuple.setField(0,field);
            tuple.setField(1,new IntField(min));
        }else{
            tuple.setField(0,new IntField(min));
        }
        resultSet.add(tuple);
    }
}  
...

这里注意一下,如果key只有一个null的话,外面的大循环只会执行一次,然后存入到结果集中。

然后通过结果集得到一个迭代器,通过这个迭代器迭代所有的结果。

exercise 3 - HeapFile Mutability

主要是实现以下部分:

  • src/simpledb/HeapPage.java
  • src/simpledb/HeapFile.java
  • src/simpledb/BufferPool.java 中的
    • insertTuple()
    • deleteTuple()

接下来我们还要实现Delete和Insert两种操作,这两种操作和前面的区别在于,Delete和Insert会改变数据表的Page中存储的元组信息,比如Insert会找到一个空的slot并插入元组,Delete会把对应slot上的元组标记为invalid,这样就表示这段位置上的元组已经失效,可以填入其他的元组。

为此我们要先实现每个Heap Page上的插入删除操作,然后实现每个Heap File上的插入删除,然后再实现BufferPool中的插入删除。这和我们在lab1中构建SimpleDB的存储体系的顺序是一样的。

  • 同时,在一个Page发生了修改之后,这个Page就变成了脏页,需要标记为dirty,并让BufferPool在适当的时机写回磁盘存储的文件中
  • 之后所有涉及到页修改的操作都需要通过BufferPool对外提供的方法进行,这其中执行的逻辑是BufferPool先判断要操作的页在不在Buffer中,如果不在就先去磁盘中把对应的Page读进来,然后在这个页上进行相关操作

其实在lab1中我就已经完成这部分的内容了,只是还缺少了BufferPool两个函数的构建,这里简单给出insertTuple的函数构造:

public void insertTuple(TransactionId tid, int tableId, Tuple t)
    throws DbException, IOException, TransactionAbortedException {
    DbFile dbFile = Database.getCatalog().getDatabaseFile(tableId);
    //注意,insertTuple函数并不会
    List<Page> pages = dbFile.insertTuple(tid, t);
    for(Page page : pages){
        page.markDirty(true,tid);
        buffer.put(page.getId().hashCode(),page);
    }
}

exercise 4 - Insertion and Deletion

主要实现以下类:

  • src/simpledb/Insert.java
  • src/simpledb/Delete.java

在实现了存储系统中的修改操作之后,我们才能进一步实现Delete和Insert操作的运算符,这里的运算符在进行页的修改的时候都需要通过BufferPool提供的方法来修改,我们只需要再次基础上实现每个运算符的fetchNext方法就行。

值得注意的是,Insert和Delete操作的运算符返回的结果依然是元组的形式,但是这个元组只有1个属性,并且这个属性值代表了Insert和Delete操作影响的元组数量。

(1)Insert

先看下成员变量吧:

private TransactionId t;
private OpIterator child;
private int tableId;
private ArrayList<Tuple> tupleList = new ArrayList<>();
private Iterator<Tuple> iterator;

在构造器中,会传入child和tableId,child就是所有要插入的tuple,tableId就是要插入的table。

而主要的逻辑就是在于open()函数:

child.open();
int count = 0;
while(child.hasNext()){
    Tuple next = child.next();
    count++;
    try {
        Database.getBufferPool().insertTuple(this.t,this.tableId,next);
    } catch (IOException e) {
        e.printStackTrace();
    }
}
Tuple tuple = new Tuple(getTupleDesc());
tuple.setField(0, new IntField(count));
tupleList.add(tuple);
iterator = tupleList.iterator();
super.open();

就是迭代每一个要插入的元素,然后将最后插入的元素个数组成一个tuple,形成一个迭代器。

(2)Delete

和insert几乎差不多:

child.open();
int count = 0;
while(child.hasNext()){
    Tuple next = child.next();
    count++;
    try {
        Database.getBufferPool().deleteTuple(t,next);
    } catch (IOException e) {
        e.printStackTrace();
    }
}
Tuple tuple = new Tuple(getTupleDesc());
tuple.setField(0,new IntField(count));
tupleList.add(tuple);
iterator = tupleList.iterator();
super.open();

exercise 5 - pageEviction

在lab2的最后,我们还需要实现页的置换算法,因为BufferPool是有容量上限的,如果我们在不断读入页的时候把容量用完了,那么就必须将一部分页置换出去,这样才能让新的页不断读入内存中,常见的页置换策略有LRU、Clock等等,我们在lab2中也需要自己实现一种Buffer中页的置换算法。

于是我选择了最简单的实现方式——替换Buffer中的第一个页,具体方法是每当要发生置换的时候,就用Buffer的迭代器获取位于最前面的页,然后将其替换,这个实现过程非常简单,也比较有效。

这里使用的是LRU策略,主要就是链表+map实现:

class DLinkedNode {
    K key;
    V value;
    DLinkedNode prev;
    DLinkedNode next;
    public DLinkedNode() {}
    public DLinkedNode(K _key, V _value) {key = _key; value = _value;}
}

private Map<K,DLinkedNode> cache = new ConcurrentHashMap<K,DLinkedNode>();
private int size;
private int capacity;
private DLinkedNode head, tail;

链表是为了记录每个page的最近一次获取顺序并进行淘汰,map主要是用来判断buffer是否存在该page。

每次通过key获取元素,如果存在就移到头部:

public synchronized V get(K key){
    DLinkedNode node = cache.get(key);
    if(node==null){
        return null;
    }
    //如果存在,移到头部
    moveToHead(node);
    return node.value;
}

新增元素的时候,如果存在该元素,就修改;否者插入头部,然后判断有没有达到容量上限,超过上限的话,就淘汰最后一个元素:

public synchronized void put(K key,V value){
    DLinkedNode node = this.cache.get(key);
    if(node==null){
        //新增
        DLinkedNode newNode = new DLinkedNode(key,value);
        this.cache.put(key,newNode);
        addToHead(newNode);
        this.size++;
        //判断是否达到上限
        if(this.size>this.capacity){
            //删掉最后一个元素
            DLinkedNode tmp = tail.prev;
            //先在链表中移除
            removeNode(tmp);
            //然后在map中移除
            this.cache.remove(tmp.key);
            this.size--;
        }
    }else{
        //修改
        node.value = value;
        moveToHead(node);
    }
}

而在我们的BufferPool中,首先就要将buffer的Map结构换成LRUCache:

private LRUCache<PageId,Page> buffer;

然后实现以下函数:

  • flushAllPages:将所有的page进行刷盘,准确来说是将所有脏页进行刷盘;
  • discardPage:从BufferPool中移除指定PageId的page;
  • flushPage:将指定pageId的脏页刷盘;
  • flushPages:将指定TransactionId的所有脏页刷盘;
  • evictPage:从buffer中选择一页淘汰,这里最好是选择一个非脏页,因为脏页要等到一次事务结束进行刷盘。(可以等到lab4事务中再去完成)

flushAllPages函数:

public synchronized void flushAllPages() throws IOException {
    // some code goes here
    // not necessary for lab1
    LRUCache<PageId, Page>.DLinkedNode head = buffer.getHead();
    LRUCache<PageId, Page>.DLinkedNode tail = buffer.getTail();
    while(head!=tail){
        Page page = head.value;
        if(page!=null && page.isDirty()!=null){
            DbFile dbFile = Database.getCatalog().getDatabaseFile(page.getId().getTableId());
            //记录日志
            try{
                Database.getLogFile().logWrite(page.isDirty(),page.getBeforeImage(),page);
                Database.getLogFile().force();

                dbFile.writePage(page);
            }catch (IOException e){
                e.printStackTrace();
            }
        }
        head = head.next;
    }

}

discardPage函数:

public synchronized void discardPage(PageId pid) {
    // some code goes here
    // not necessary for lab1
    LRUCache<PageId, Page>.DLinkedNode head = buffer.getHead();
    LRUCache<PageId, Page>.DLinkedNode tail = buffer.getTail();
    while(head!=tail){
        PageId key = head.key;
        if(key!=null && key.equals(pid)){
            buffer.remove(head);
            return;
        }
        head = head.next;
    }
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值