MIT6.5830-2022 Lab 1: SimpleDB

发布时间 2023-04-19 12:57:33作者: seaupnice

SimpleDB 组成:

  • classes: 表示 fields, tuples, tuple schemas。
  • classes: 作用于 tuples 的谓词和条件类。
  • methods: 在硬盘上存储关系(如heap files),处理并发控制和事务。
  • classes: 处理 tuples 的操作类(select, join, insert, delete 等)。
  • buffer pool: 在内存中缓存活跃的 tuples、pages,处理并发控制和事务。
  • catalog: 存储可使用的 table 和对应 schemas 的信息。

simpleDB 部分不支持的特性:

  • (lab1) SQL 前端或解析工具。查询使用一组操作符手动构建 query plan。
  • 视图
  • 只支持整形和定长字符串
  • (lab1) 查询优化
  • (lab1) 索引

lab1 需要关注的类:

Database:
- getCatalog(): catalog 是数据库中所有 tables 的集合。
- getBufferPool(): buffer pool 缓存常驻内存的数据库文件页。
- getLogFile(): log file。
Catalog:
- tables: 数据库中所有的 tables,使用 ConcurrentHashMap<DbFile ID, Table> 存储 table。

Table:
- DbFile file: table 是数据的逻辑表示,以文件进行物理存储,即一个逻辑上的table对应数据库中一个 HeapFile(implements DbFile) 对象。
- Stirng name: table 的表名。
- String primary_key: table的主键。
Buffer Pool:
- pages: 缓存数据库最近访问过的页。ConcurrentHashMap<PageID, Page>
HeapFile(Implements DbFile):
- File file:
- TupleDesc td:
- DbFileIterator it: 该文件上可迭代的 HeapPage.
HeapPage(Implements Page):
- HeapPageId pid: 由 tableid 和 pageno 确定, tableid 是唯一对应 table 所在表示的物理文件,pageno 是这个物理文件上的页号。由 pageID id 可以确定当前缓存的是哪个 table 上对应的物理文件的哪一页。
- TupleDesc td:
- Byte header[]: 记录对应 slot 上是空还是有tuples。
- Tuple tuples[]: 记录这个 Page 上存储的所有 slots 上的 tuples。

上述类之间的关系:

Catalog 是数据库中所有 table 的集合,table 作为逻辑表示,对应的物理表示是文件,即 HeapFile(implements DbFile),位于 table 和文件之间的中间表示是 HeapPage(implements Page), 由 Buffer Pool 进行管理,使得对数据库的操作并不是直接对文件进行操作,而是对内存中的 HeapPage。Tuple 是表中的一条记录,记录由 Field 组成,Tuple 和 schema 可以确定一个逻辑上的 Table。

Exercise 1

Tuple 由一组 Field 对象组成。Field 是一个可以表示不同数据类型的接口。Tuple 对象的底层存储可以是 heap file 或者 B-trees。Tuples 的类型(schema)称为 tuple descriptor,TupleDesc 对象表示,由 Type 对象集合表示。

// TupleDesc.java
private TDItem[] tdItems;

// 获取迭代
Arrays.stream(tdItems).iterator()
// Tuple.java
private TupleDesc tDesc;
private Field[] fields;
private RecordId recordId;

Exercise 2

Catalog 由数据库中的 tables 和对应的 schemas 组成。支持添加 table,获取特定 table 的信息。每个 table 关联一个 TupleDesc 确定表的字段类型和字段数量。全局的 catalog 可以通过 Database.getCatalog() 获取。

public static class Table implements Serializable {
    private static final long serialVersionUID = 1L;

    public DbFile file;
    public String name;
    public String pkeyField;

    public Table(DbFile file, String name, String pkeyField) {
        this.file = file;
        this.name = name;
        this.pkeyField = pkeyField;
    }

    public String toString() {
        return name + "(" + pkeyField + ")";
    }
}

// tableId, Table
private Map<Integer, Table> tables;

// 遍历tables,并删除特定表
public void addTable(DbFile file, String name, String pkeyField) {
    // Done: some code goes here

    // if a table name conflict exists, remove old table
    Iterator<Map.Entry<Integer, Table>> iterator = this.tables.entrySet().iterator();
    while (iterator.hasNext()) {
        Map.Entry<Integer, Table> entry = iterator.next();
        if (entry.getValue().name.equals(name)) {
            iterator.remove();
            break;
        }
    }

    Table table = new Table(file, name, pkeyField);
    this.tables.put(file.getId(), table);
}

Exercise 3

BufferPool 负责缓存内存中最近从硬盘中读出的页。所有的读写页操作都是通过 buffer pool 进行的。buffer pool 由固定数量的页集合组成,定义为 numPages。之后的实验会实现页淘汰策略。本次实验只需实现 getPage() 。本实验的测试不会使得 buffer pool 存储的页超过 numPages,之后会实现淘汰策略。

// hashmap 中对 key 的比较是通过对象的 equals() 和 hashCode() 实现的

public Page getPage(TransactionId tid, PageId pid, Permissions perm)
        throws TransactionAbortedException, DbException {
    // Done: some code goes here
    if (this.pages.containsKey(pid)) {
        return this.pages.get(pid);
    }

    if (this.pages.size() >= this.numPages) {
        throw new DbException("More than numPages, later should implement an eviction policy.");
    }

    Page page = Database.getCatalog().getDatabaseFile(pid.getTableId()).readPage(pid);
    this.pages.put(pid, page);

    return page;
}

Exercise 4

从硬盘读写数据的方式称为 access methods。通常的方法有 heap files(无序文件或者tuples)。本次实验只需实现 heap file 访问方法。

HeapFile 对象由 pages 集合组成,每个 page 有固定数量的字节为存储 tuples 和 headers。SimpleDB 中一个 table 对应一个 HeapFile 对象。每个 page 由 slots 集合组成,每个 slot 可以存储一个 tuple(SimpleDb 中给定 table 中的 tuples 大小相同)。除了 slots ,每个 page 有一个 header 作为 bitmap ,表示每个 slot 中 tuple 是否有效(无效可能是被删除或者没有初始化)。pages 存储在 buffer pool 中,读写通过 HeapFile

SimpleDB 对存储在硬盘上的 heap files 和存储在内存中的 heap files 有相同的格式。每个文件的 page data 在硬盘上连续存储。每个页至少有一个或多个字节表示 header,header 之后是实际的页内容。所以每个 tuple 实际占用 \(tuple\ size * 8\ bits\) 的内容和 \(1\ bit\) 的header信息。所以一页可以容纳的 tuple 数量是 \(tuples\ per\ page = floor((page\ size * 8) / (tuple\ size * 8 + 1))\),向下取整是因为不想在一页中存储部分 tuples。

计算出 tuples per pages 后,可以计算 header 占用的字节数:\(header\ bytes = ceiling(tuples\ per\ page / 8)\),向上取整是因为header的存储想要以字节为单位。

每个字节的最低位表示靠前的 slot 状态,如第一个字节的第一位表示这个页第一个 slot 是否可用。最后一个字节的高位可能和 slot 不对应,因为 slot 数量可能不是 \(8\) 的倍数。

// HeapPage.java
public Iterator<Tuple> iterator() {
    // Done: some code goes here
    return new Iterator<Tuple>() {
        private int curror = -1;

        @Override
        public boolean hasNext() {
            while (this.curror + 1 < HeapPage.this.numSlots) {
                if (HeapPage.this.isSlotUsed(this.curror + 1)) {
                    return true;
                }
                this.curror++;
            }
            return false;
        }

        @Override
        public Tuple next() {
            if (this.curror + 1 >= HeapPage.this.numSlots) {
                throw new NoSuchElementException();
            }
            return HeapPage.this.tuples[++this.curror];
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException("Unimplemented method 'remove'");
        }

    };
}

Exercise 5

为了从硬盘上读取一页,需要计算文件的正确偏移。需要在任意偏移下随机访问文件。不能调用 BufferPool 实例方法从硬盘上直接读取一页,BufferPool 应该通过 HeapFile 从硬盘上读取页形成 HeapPage,使得和硬盘的数据交互通过 BufferPool 进行隔离。

实现 HeapFile.iterator() 方法,可以迭代访问 HeapFile 中的每个页中的 tuples。迭代必须使用 BufferPool.getPage() 方法访问 HeapFile 中的页。这个方法将页导入 buffer pool,最终需要实现基于锁的并发控制和恢复。在 open() 中不要导入整个 table ,会导致 out of memory error 错误(导入table的第一页即可)。

private File file;
private TupleDesc tDesc;

public Page readPage(PageId pid) {
    // Done: some code goes here
    RandomAccessFile randomAccessFile = null;
    try {
        int pageNumber = pid.getPageNumber();
        int pageSize = BufferPool.getPageSize();
        int offset = pageNumber * pageSize;

        randomAccessFile = new RandomAccessFile(file, "r");
        randomAccessFile.seek(offset);

        byte[] buffer = new byte[pageSize];

        if (randomAccessFile.read(buffer) < pageSize) {
            throw new IllegalArgumentException("readPage: the read bytes length is less than pageSize");
        }

        return new HeapPage(new HeapPageId(pid.getTableId(), pageNumber), buffer);
    } catch (IOException e) { // only catch IOException
        e.printStackTrace();
    } finally {
        try {
            randomAccessFile.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    throw new IllegalArgumentException("readPage: IOException");
}

public DbFileIterator iterator(TransactionId tid) {
    // TODO: some code goes here
    return new AbstractDbFileIterator() {
        private Iterator<Tuple> iterator = null;
        private HeapPage page = null;
        private int pageNo = 0;

        /**
         * Opens the iterator
         *
         * @throws DbException when there are problems opening/accessing the database.
         */
        @Override
        public void open() throws DbException, TransactionAbortedException {
            this.pageNo = 0;
            PageId pageId = new HeapPageId(HeapFile.this.getId(), this.pageNo);
            this.page = (HeapPage) Database.getBufferPool().getPage(tid, pageId, Permissions.READ_ONLY);
            this.iterator = this.page.iterator();
        }

        /**
         * Resets the iterator to the start.
         *
         * @throws DbException When rewind is unsupported.
         */
        @Override
        public void rewind() throws DbException, TransactionAbortedException {
            this.close();
            this.open();
        }

        /**
         * Reads the next tuple from the underlying source.
         *
         * @return the next Tuple in the iterator, null if the iteration is finished.
         */
        @Override
        protected Tuple readNext() throws DbException, TransactionAbortedException {
            if (this.iterator == null) {
                return null;
            }
            if (this.iterator.hasNext()) {
                return this.iterator.next();
            }
            // the current page iterator finish
            while (this.pageNo + 1 < HeapFile.this.numPages()) {
                PageId pageId = new HeapPageId(HeapFile.this.getId(), ++this.pageNo);
                this.page = (HeapPage) Database.getBufferPool().getPage(tid, pageId, Permissions.READ_ONLY);
                this.iterator = this.page.iterator();
                if (this.iterator.hasNext()) {
                    // the new page has Tuple in the iterator
                    return this.iterator.next();
                }
            }
            // all page iterator finish
            this.iterator = null;
            return null;
        }

        /**
         * Closes the iterator.
         */
        @Override
        public void close() {
            super.close();
            this.iterator = null;
            this.page = null;
        }

    };
}

Exercise 6

Operators 负责 query plan 的真正执行,是关系代数的操作实现。在 SimpleDB 中,operators 基于迭代器,每个 operator 实现了 DbIterator 接口。

plan 是通过将低级的 operators 传入高级 operators 的构造函数中,将 operators 连在一起。特殊的 access method operators 处于 plan 的叶子节点,负责从硬盘读取数据(它们是最基础的operators,其下没有其他 operators)。

在 plan 的顶层,和 SimpleDB 交互的程序调用根 operators 的 getNext(),依次调用子 operators 的 getNext() ,直到叶子节点。从硬盘中读取 tuples 通过 getNext() 返回到 plan 树中的父 operators。tuples 在 plan 中的向上传播直到根 operator,或者被 plan 中的其他 operator 拒绝或合并。

public SeqScan(TransactionId tid, int tableid, String tableAlias) {
    // Done: some code goes here
    this.transactionId = tid;
    this.tableID = tableid;
    this.tableAlias = tableAlias;
    this.dbFileIterator = Database.getCatalog().getDatabaseFile(tableid).iterator(tid);
}

A simple query

simpledb.jar convert 和 print 程序:

some_data_file.txt 文件是数据的逻辑表示,通过 convert 程序转化为对应的物理表示 some_data_file.dat ,该文件即对应HeapFile,逻辑上是一张 table,数据按照 HeapPage 组织,内部含有 header 信息和 tuples 信息;print 程序通过 HeapFile 实现的 iterator 方法对所有的 tuples 进行遍历, 从而实现 select * from table;因为 convert程序将 filed 读入文件流中的条件是读入分隔符或者\n或\r,所以 txt 文件的最后一个字符必须是\n或者\r,否则程序无法读入最后一个 filed,后续会导致一些难以排查的错误。

test 程序:

实现 select * from table; 。表只有整数型 field,先通过 Type 和 fieldName 创建 TupleDesc,初始化 HeapFile 对象,将它对应的 table 加入 Catalog。初始化数据库系统完成后,创建 query plan。本程序之包含了 SeqScan operator,可以扫描读取硬盘上的 tuples。通常,operators 的实例化是通过表的引用(如seqScan)完成初始化,或者通过子 operator (如 Filter)完成初始化

总结

lab1 实验需要理解 Catalog,BufferPool,HeapPage,HeapFile 之间的关系,table 的数据逻辑表示和数据物理表示之间的联系,包括 table 和 HeapFile 之间的关系,table 和 HeapPage 之间的关系,tuples 和 HeapPage 之间的关系,HeapPage 和 BufferPool 之间的关系等。

了解 query plan 的基本原理。

熟悉 select * from table; 在数据库系统中的实现流程:如何将table的逻辑表示转化为heapfile(conver程序);如何将heapfile对应的table加入catalog(test程序);seqscan 如何读取table的所有tuples(HeapFile.iterator)。