MIT6.830 lab2 シンプルなデータベースの実装


序文

前回、このレポートを書くと言ってから、半年後、ようやくlab2を書くことにしました。

完全なコード


1. lab2 について

このラボ課題では、SimpleDB の一連の演算子を記述して、テーブルの変更 (レコードの挿入と削除など)、選択、結合、集計を実装します。これらは、ラボ 1 で作成した基盤の上に構築され、複数のテーブルに対して単純なクエリを実行できるデータベース システムを提供します。
さらに、ラボ 1 のバッファー プール管理の問題を無視しました。
データベースの存続期間中にメモリに収まりきらないページを参照するときに発生する問題に対処していません。ラボ 2 では、バッファ プールから古いページをフラッシュするエビクション ポリシーを設計します。
このラボでは、トランザクションやロックを実装する必要はありません。

コースアドレス

研究室の住所

2、lab2

1.演習1

演習 1 では、主に 2 つの演算子FilterJoin

  • フィルター: この演算子は、コンストラクターの一部として指定された条件を満たすタプルのみを返します。
    したがって、述語に一致しないタプルを除外します。

  • Join: この演算子は、コンストラクターの一部として渡された a に従って、2 つの子からのタプルを結合します。単純なネストされたループの結合のみが必要ですが、より興味深い結合の
    実装を調べることができます。ラボの書き込みで実装について説明します。JoinPredicate

here は、Filterたとえば、SQL ステートメント内の where の役割を指しますselect * from lists where id > 1重要な補助クラス here がありますPredicate。これは、タプル内のフィールドを指定されたフィールドと比較して、単一のタプルでフィルタリング操作を実装するために使用されます。下の図では、タプル内のデータのどの列を比較するかを表すPredicate3 つの重要なメンバー変数があることがわかります。これは、演算子を表す列挙型クラスであり、比較のロジックですこれは、比較される量を表します。比較した。fieldNumopEQUALS, GREATER_THAN, LESS_THAN, LESS_THAN_OR_EQ, GREATER_THAN_OR_EQ, LIKE, NOT_EQUALSoperand
ここに画像の説明を挿入

Predicateの関数を通じてfilter、比較したいものを渡しTuple、対応する列のデータを取得し、実装を通じて比較しますField(例として取り上げます)。compare IntField

	//Predicate
	public boolean filter(Tuple t) {
    
    
        // some code goes here
        boolean res = t.getField(this.fieldNum).compare(op, this.operand);
        return res;
    }

	//IntField
	public boolean compare(Predicate.Op op, Field val) {
    
    

        IntField iVal = (IntField) val;

        switch (op) {
    
    
            case EQUALS:
            case LIKE:
                return value == iVal.value;
            case NOT_EQUALS:
                return value != iVal.value;
            case GREATER_THAN:
                return value > iVal.value;
            case GREATER_THAN_OR_EQ:
                return value >= iVal.value;
            case LESS_THAN:
                return value < iVal.value;
            case LESS_THAN_OR_EQ:
                return value <= iVal.value;
        }

        return false;
    }

Predicate単一の one をフィルタリングできるようになったのでTupleselect * from lists where id > 1テーブル全体を操作する必要がある場合、必要な反復子は 1 つだけです。Filterイテレータがコンストラクタに渡され、各行を反復することがわかりますTuple

public Filter(Predicate p, OpIterator child) {
    
    
        // some code goes here
        this.predicate = p;
        this.opIterator = child;
        this.tupleDesc = child.getTupleDesc();
    }

Filterこれは継承されOperatorインターフェースをOperator実装し、next() と hasNext() の両方で抽象メソッド fetchNext() を呼び出します。OpIterator次に、fetchNext() を実装するだけです。

    protected Tuple fetchNext() throws NoSuchElementException,
            TransactionAbortedException, DbException {
    
    
        // some code goes here
        if (opIterator == null) {
    
    
            throw new NoSuchElementException("Operator Iterator is empty");
        }

        while (opIterator.hasNext()) {
    
    
            Tuple tuple = opIterator.next();
            if (predicate.filter(tuple)) return tuple;
        }

        return null;
    }

Filterとの実現が完了するとPredicate、次JoinのことJoinPredicateが容易になります。Join私たちがここにいるのは内なるつながりであることは注目に値します。つまり、次のようなステートメントの機能を実現したいと考えていますSELECT * FROM a INNER JOIN b ON a.No=b.No内部結合と外部結合がわからない学生は、左右結合と内部結合の違いを読むことができます

の場合JoinPredicate、およびPredicate同様に、Join2 つのタプルの特定のフィールドを比較する役割を持つヘルパー クラス。

	//JoinPredicate
    public boolean filter(Tuple t1, Tuple t2) {
    
    
        // some code goes here
        Field operand1 = t1.getField(fieldNum1);
        Field operand2 = t2.getField(fieldNum2);
        return operand1.compare(op, operand2);
    }

また、次に fetchNext() を実装しますJoinここでの考え方は、2 つのイテレータ child1 と child2 を使用して 2 つのテーブルのデータをループ処理することです。最初に child1 のタプルを取得して t に割り当て、次に t を child2 のタプルと比較し、条件を満たす t2 に接続します。接続条件 そして、接続された newTuple を返します。

    protected Tuple fetchNext() throws TransactionAbortedException, DbException {
    
    
        // some code goes here
        while (this.it1.hasNext() || this.t != null) {
    
    
            if (this.it1.hasNext() && this.t == null) {
    
    
                t = it1.next();
            }
            while (it2.hasNext()) {
    
    
                Tuple t2 = it2.next();
                if (p.filter(t, t2)) {
    
    
                    TupleDesc td1 = t.getTupleDesc();
                    TupleDesc td2 = t2.getTupleDesc();
                    TupleDesc newTd = TupleDesc.merge(td1, td2);
                    Tuple newTuple = new Tuple(newTd);
                    newTuple.setRecordId(t.getRecordId());
                    int i = 0;
                    for (; i < td1.numFields(); ++i)
                        newTuple.setField(i, t.getField(i));
                    for (int j = 0; j < td2.numFields(); ++j)
                        newTuple.setField(i + j, t2.getField(j));
                    if (!it2.hasNext()) {
    
    
                    	//child1匹配到的刚好是child2的最后一条记录,
                    	//不重置的话取出child1的下一个tuple就不是与child2的第一个tuple进行比较了)
                        it2.rewind();
                        t = null;
                    }
                    return newTuple;
                }
            }
            //child1的其中一个tuple与child2所有tuple都不匹配
            //同样不重置的话取出child1的下一个tuple就不是与child2的第一个tuple进行比较
            it2.rewind();
            t = null;
        }
        return null;
    }

2.演習2

演習 2 では、主に 5 つの型の整数と文字列 (GROUP BY、COUNT、SUM、AVG、MIN、MAX) でのグループ化をサポートする SQL 集計を実装できます。文字列型は COUNT のみを実装する必要があります。次のような SQL ステートメントを例にとると、select sum(money) as 总收入 from table GROUP BY xxこの実験では、1 つのフィールドに基づいてグループ化と集計を実装するだけで済みます。つまり、グループ化フィールドと集計フィールドは 1 つだけです。ここではグループ化フィールドxxmoney集約フィールド

IntegerAggregator のメンバー変数は次のとおりです。
gbFieldIndex: グループ化フィールドのシリアル番号
gbFieldType: グループ化フィールドのタイプ
aggFieldIndex: 集計フィールドのシリアル番号
gbHandler:gbResult集計結果を保存するために使用されるマップである抽象クラス、および分類が必須でない場合、そのキー値は分類されたフィールドでありNO_GROUPING_KEY、その値は集計されたフィールドです。さまざまな実装の抽象メソッド handle() は、特定の操作を実装します. 実装されたGBHandlerクラスには、CountHandler、SumHandler、MaxHandler、MinHandler、および AvgHandler が含まれます。

    private abstract class GBHandler{
    
    
        ConcurrentHashMap<String,Integer> gbResult;
        abstract void handle(String key,Field field);
        private GBHandler(){
    
    
            gbResult = new ConcurrentHashMap<>();
        }
        public Map<String,Integer> getGbResult(){
    
    
            return gbResult;
        }
    }
    private class CountHandler extends GBHandler {
    
    
        @Override
        public void handle(String key, Field field) {
    
    
            if(gbResult.containsKey(key)){
    
    
                gbResult.put(key,gbResult.get(key)+1);
            }else{
    
    
                gbResult.put(key,1);
            }
        }
    }
    private class SumHandler extends GBHandler{
    
    
        @Override
        public void handle(String key, Field field) {
    
    
            if(gbResult.containsKey(key)){
    
    
                gbResult.put(key,gbResult.get(key)+Integer.parseInt(field.toString()));
            }else{
    
    
                gbResult.put(key,Integer.parseInt(field.toString()));
            }
        }
    }
    private class MinHandler extends GBHandler{
    
    
        @Override
        void handle(String key, Field field) {
    
    
            int tmp = Integer.parseInt(field.toString());
            if(gbResult.containsKey(key)){
    
    
                int res = gbResult.get(key)<tmp?gbResult.get(key):tmp;
                gbResult.put(key, res);
            }else{
    
    
                gbResult.put(key,tmp);
            }
        }
    }
    private class MaxHandler extends GBHandler{
    
    
        @Override
        void handle(String key, Field field) {
    
    
            int tmp = Integer.parseInt(field.toString());
            if(gbResult.containsKey(key)){
    
    
                int res = gbResult.get(key)>tmp?gbResult.get(key):tmp;
                gbResult.put(key, res);
            }else{
    
    
                gbResult.put(key,tmp);
            }
        }
    }
    private class AvgHandler extends GBHandler{
    
    
        ConcurrentHashMap<String,Integer> sum;
        ConcurrentHashMap<String,Integer> count;
        private AvgHandler(){
    
    
            count = new ConcurrentHashMap<>();
            sum = new ConcurrentHashMap<>();
        }
        @Override
        public void handle(String key, Field field) {
    
    
            int tmp = Integer.parseInt(field.toString());
            if(gbResult.containsKey(key)){
    
    
                count.put(key,count.get(key)+1);
                sum.put(key,sum.get(key)+tmp);
            }else{
    
    
                count.put(key,1);
                sum.put(key,tmp);
            }
            gbResult.put(key,sum.get(key)/count.get(key));
        }
    }

これでグループ化と集計の機能ができました.次にTupleを渡して呼び出すだけです.グループ化の型が不正な場合は例外がスローされます.正常な場合は通常のhandle()を呼び出すことができます.マップ操作。

    public void mergeTupleIntoGroup(Tuple tup) {
    
    
        // some code goes here
        if(gbFieldType!=null&&(!tup.getField(gbFieldIndex).getType().equals(gbFieldType))){
    
    
            throw new IllegalArgumentException("Given tuple has wrong type");
        }
        String key;
        if (gbFieldIndex == NO_GROUPING) {
    
    
            key = NO_GROUPING_KEY;
        } else {
    
    
            key = tup.getField(gbFieldIndex).toString();
        }
        gbHandler.handle(key,tup.getField(aggregateFieldIndex));
    }

GBHandler集計結果に対するイテレータを返します

    public OpIterator iterator() {
    
    
        // some code goes here
        Map<String,Integer> results = gbHandler.getGbResult();
        Type[] types;
        String[] names;
        TupleDesc tupleDesc;
        List<Tuple> tuples = new ArrayList<>();
        if(gbFieldIndex==NO_GROUPING){
    
    
            types = new Type[]{
    
    Type.INT_TYPE};
            names = new String[]{
    
    "aggregateVal"};
            tupleDesc = new TupleDesc(types,names);
            for(Integer value:results.values()){
    
    
                Tuple tuple = new Tuple(tupleDesc);
                tuple.setField(0,new IntField(value));
                tuples.add(tuple);
            }
        }else{
    
    
            types = new Type[]{
    
    gbFieldType,Type.INT_TYPE};
            names = new String[]{
    
    "groupVal","aggregateVal"};
            tupleDesc = new TupleDesc(types,names);
            for(Map.Entry<String,Integer> entry:results.entrySet()){
    
    
                Tuple tuple = new Tuple(tupleDesc);
                if(gbFieldType==Type.INT_TYPE){
    
    
                    tuple.setField(0,new IntField(Integer.parseInt(entry.getKey())));
                }else{
    
    
                    tuple.setField(0,new StringField(entry.getKey(),entry.getKey().length()));
                }
                tuple.setField(1,new IntField(entry.getValue()));
                tuples.add(tuple);
            }
        }
        return new TupleIterator(tupleDesc,tuples);
    }

StringAggregator の実装は IntegerAggregator と同様であり、Handler 部分は COUNT を実装するだけでよいため、再度説明しません。次に、Aggregate の実装だけを残します. Aggregate クラスは、前の 2 つの Aggregators をカプセル化したものです. 集計フィールドの型に応じて、異なる Aggregators 内の Iterator() が呼び出され、結果セットの反復子が返されます.

3.演習3

演習 3 は、主に HeapPage、HeapFile、BufferPool でのタプルの挿入と削除を実装することです.ここでは、1 ページ内の HeapPage データの追加と削除を最初に実現し、HeapFile では各ページをトラバースするだけで済みますPage でメソッドを呼び出す BufferPool での実装 同様に、Database.getCatalog().getDatabaseFile(tableId) を呼び出すだけです。

まず HeapPage の挿入の実装を見てください. 最初に getNumEmptySlots() メソッドを呼び出してビット操作で空きスロットの数を取得します. 十分なスペースがない場合は例外がスローされます. 十分なスペースがある場合は,最初に遭遇した空のスロット スロット マークのマークが使用され、スロットの対応する位置にデータが挿入されます。タプルを削除するには、対応する位置を null に設定し、対応するスロットを未使用としてマークするだけです

    public void insertTuple(Tuple t) throws DbException {
    
    
        // some code goes here
        // not necessary for lab1
        if (getNumEmptySlots() == 0) throw new DbException("Not enough space to insert tuple");
        if (!t.getTupleDesc().equals(this.td)) throw new DbException("Tuple's Description is not match for this page");

        for (int i = 0; i < tuples.length; i++) {
    
    
            if (!isSlotUsed(i)) {
    
    
                markSlotUsed(i,  true);
                tuples[i] = t;
                tuples[i].setRecordId(new RecordId(pid, i));
                break;
            }
        }
    }

    public void deleteTuple(Tuple t) throws DbException {
    
    
        // some code goes here
        // not necessary for lab1
        RecordId recordId = t.getRecordId();
        int tupleIndex = recordId.getTupleNumber();
        if (recordId != null && pid.equals(recordId.getPageId())) {
    
    
            if (tupleIndex < getNumTuples() && isSlotUsed(tupleIndex)) {
    
    
                tuples[tupleIndex] = null;
                markSlotUsed(tupleIndex, false);
                return;
            }
            throw new DbException("can't find tuple in the page");
        }
        throw new DbException("can't find tuple in the page");
    }
    
    public int getNumEmptySlots() {
    
    
        // some code goes here
        int res = 0;
        for (int i = 0; i < numSlots; i ++ ) {
    
    
            if ((header[i / 8] >> (i % 8) & 1) == 0) {
    
    
                res ++;
            }
        }
        return res;
    }
    /**
     * Returns true if associated slot on this page is filled.
     */
    public boolean isSlotUsed(int i) {
    
    
        // some code goes here
        int index = i / 8; //在bitmap的第几个字节中
        int offset = i % 8; //在第几个字节中的偏移量
        return (header[index] & (1 << offset)) != 0; //判断是否为1,即该slot是否填充
    }

    /**
     * Abstraction to fill or clear a slot on this page.
     */
    private void markSlotUsed(int i, boolean value) {
    
    
        // some code goes here
        // not necessary for lab1
        int index = Math.floorDiv(i, 8);
        byte b = header[index];
        byte mask = (byte) (1 << (i % 8));

        // change header's bitmap
        if (value) header[index] = (byte) (b | mask);
        else header[index] = (byte) (b & (~mask));
    }

HeapPage の挿入と削除を実装し、空きスロットのあるページを見つけるためにすべてのページをトラバースする HeapFile を実装しました. すべてのページは BufferPool から取得されることに注意してください. BufferPool が存在しない場合は、それらを見つけるためにディスクに移動します. 現在のテーブルのすべてのページに残りのスペースがない場合は、新しいページを書き込む必要があります (つまり、writePage())。削除は簡単です。ページの削除を呼び出すだけです。しかし、挿入および削除するたびに変更されたページのリストを返さなければならないのはなぜでしょうか? これは、ダーティ ページを返し、バッファ プールでダーティ マークを付けるためです。

    public List<Page> insertTuple(TransactionId tid, Tuple t)
            throws DbException, IOException, TransactionAbortedException {
    
    
        ArrayList<Page> list = new ArrayList<>();
        BufferPool pool = Database.getBufferPool();
        int tableid = getId();
        for (int i = 0; i < numPages(); ++i) {
    
    
            HeapPage page = (HeapPage) pool.getPage(tid, new HeapPageId(tableid, i), Permissions.READ_WRITE);
            if (page.getNumEmptySlots() > 0) {
    
    
                page.insertTuple(t);
                page.markDirty(true, tid);
                list.add(page);
                return list;
            }
        }

        HeapPage page = new HeapPage(new HeapPageId(tableid, numPages()), HeapPage.createEmptyPageData());
        page.insertTuple(t);
        writePage(page);
        list.add(page);
        return list;
    }

    public ArrayList<Page> deleteTuple(TransactionId tid, Tuple t) throws DbException,
            TransactionAbortedException {
    
    
        // some code goes here
        ArrayList<Page> list = new ArrayList<>();
        HeapPage page = (HeapPage) Database.getBufferPool().getPage(tid, t.getRecordId().getPageId(), Permissions.READ_WRITE);
        page.deleteTuple(t);
        list.add(page);
        return list;
        // not necessary for lab1
    }

    public void writePage(Page page) throws IOException {
    
    
        // some code goes here
        int pageNumber = page.getId().getPageNumber();
        if (pageNumber > numPages()) {
    
    
            throw new IllegalArgumentException("page is not in the heap file or page'id in wrong");
        }

        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
        randomAccessFile.seek(pageNumber * BufferPool.getPageSize());
        byte[] pageData = page.getPageData();
        randomAccessFile.write(pageData);
        randomAccessFile.close();
        // not necessary for lab1
    }

最後に BufferPool への挿入と削除を実装する.databaseFile.insertTuple(tid, t)修正したダーティページを取得したら, markDirty を呼び出してマークダーティ操作を行う. これがlruCache.put(page.getId(), page)次の実験で実装する LRU キャッシュの処理である. 削除操作は同じです。

    public void insertTuple(TransactionId tid, int tableId, Tuple t)
        throws DbException, IOException, TransactionAbortedException {
    
    
        // some code goes here
//        List<Page> pageList = Database.getCatalog().getDatabaseFile(tableId).insertTuple(tid, t);
//        for (Page page : pageList) {
    
    
//            addToBufferPool(page.getId(), page);
//        }
        // not necessary for lab1
        DbFile databaseFile = Database.getCatalog().getDatabaseFile(tableId);
        //System.out.println(tid.getId());
        List<Page> pages = databaseFile.insertTuple(tid, t);
        //System.out.println(tid.getId());
        for (Page page : pages) {
    
        //用脏页替换buffer中现有的页
            page.markDirty(true, tid);
            lruCache.put(page.getId(), page);
        }
    }

    public  void deleteTuple(TransactionId tid, Tuple t)
        throws DbException, IOException, TransactionAbortedException {
    
    
        // some code goes here
//        DbFile dbFile = Database.getCatalog().getDatabaseFile(t.getRecordId().getPageId().getTableId());
//        dbFile.deleteTuple(tid, t);
        // not necessary for lab1
        DbFile dbFile = Database.getCatalog().getDatabaseFile(t.getRecordId().getPageId().getTableId());
        List<Page> pages = dbFile.deleteTuple(tid, t);
        for (int i = 0; i < pages.size(); i++) {
    
    
            pages.get(i).markDirty(true, tid);
        }
    }

4.演習4

演習 4 には Insert と Delete の 2 つの演算子が必要です. 演習 3 の挿入と削除により, ここでは非常に簡単です. 演習 1 と同様に, Insert は Operator を継承します. 実装する必要があるのは fetchNext() だけです. 返されるフィールドは数値です.ここでの状態は、呼び出しの繰り返しを避けるためです。Delete の実装も同様であるため、ここでは繰り返しません。

	//Insert
    protected Tuple fetchNext() throws TransactionAbortedException, DbException {
    
    
        // some code goes here
        if (state) return null;

        Tuple t = new Tuple(this.td);
        int cnt = 0;
        BufferPool bufferPool = Database.getBufferPool();
        while (child.hasNext()) {
    
    
            try {
    
    
                bufferPool.insertTuple(tid, this.tableID, child.next());
            } catch (IOException e) {
    
    
                throw new DbException("fail to insert tuple");
            }
            cnt++;
        }

        t.setField(0, new IntField(cnt));
        state = true;
        return t;
    }

5.演習5

ページ削除戦略を実装する. インターネット上のこの実験に参加した全員が LRU を実装している. アイデアは非常に単純である. 両端リンクリストとマップで問題を解決できる.

public class LRUCache<K,V> {
    
    
    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;

    public LRUCache(int capacity) {
    
    
        this.size = 0;
        this.capacity = capacity;
        // 使用伪头部和伪尾部节点
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        head.prev = tail;
        tail.prev = head;
        tail.next = head;
    }


    public int getSize() {
    
    
        return size;
    }

    public DLinkedNode getHead() {
    
    
        return head;
    }

    public DLinkedNode getTail() {
    
    
        return tail;
    }

    public Map<K, DLinkedNode> getCache() {
    
    
        return cache;
    }
    //必须要加锁,不然多线程链表指针会成环无法结束循环。在这卡一天
    public synchronized V get(K key) {
    
    
        DLinkedNode node = cache.get(key);
        if (node == null) {
    
    
            return null;
        }
        // 如果 key 存在,先通过哈希表定位,再移到头部
        moveToHead(node);
        return node.value;
    }
    public synchronized void remove(DLinkedNode node){
    
    
        node.prev.next = node.next;
        node.next.prev = node.prev;
        cache.remove(node.key);
        size--;
    }

    public synchronized void discard(){
    
    
        // 如果超出容量,删除双向链表的尾部节点
        DLinkedNode tail = removeTail();
        // 删除哈希表中对应的项
        cache.remove(tail.key);
        size--;

    }
    public synchronized void put(K key, V value) {
    
    
        DLinkedNode node = cache.get(key);
        if (node == null) {
    
    
            // 如果 key 不存在,创建一个新的节点
            DLinkedNode newNode = new DLinkedNode(key, value);
            // 添加进哈希表
            cache.put(key, newNode);
            // 添加至双向链表的头部
            addToHead(newNode);
            ++size;
        }
        else {
    
    
            // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
            node.value = value;
            moveToHead(node);
        }
    }

    private void addToHead(DLinkedNode node) {
    
    
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    private void removeNode(DLinkedNode node) {
    
    
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }


    private void moveToHead(DLinkedNode node) {
    
    
        removeNode(node);
        addToHead(node);
    }

    private DLinkedNode removeTail() {
    
    
        DLinkedNode res = tail.prev;
        removeNode(res);
        return res;
    }

}

要約する

今日、家庭教師から新年明けて登校させてくださいとの連絡があり、半年遅れていた実験レポートも再開されました. もちろん次のレポートがいつ書けるかは定かではありません. . 次のものを事前に発表して、lab4 と lab3 を書きます. 私はそれをしませんでした. なぜなら, コンテンツのこの部分は解析の最適化を達成するためのものです. 私はこの側面にあまり関与したくありません.この方向性を学んだ上でデータベース開発エンジニアになり、分散ストレージか何かに従事します。

Supongo que te gusta

Origin blog.csdn.net/weixin_44153131/article/details/128807382
Recomendado
Clasificación