CMU_15445_P3_数据库在内存中存储的形式
数据库在内存的存储形式
我们将从小到大介绍一下在 bustub
框架中数据库在内存中的存储形式.
在 bustub
中所有数据的存储单元是 Page, 而管理内存中的 Page 的是 BufferPoolManager
, 这部分在前面的 Project1 已经介绍过了, 这部分是前面部分的延申, 具体一张表, 一个数据库在内存中是按照什么结构存储的呢.
先看一些关于表的定义信息:
Column
类存储的是数据库的一个属性的信息, 例如属性名, 属性的数据类型, 可变长属性的最大长度等.Schema
是一组属性名, 可以看作一张表的 Columns.
我们可以看到关于 Table 的信息抽象如下:
/**
* The TableInfo class maintains metadata about a table.
*/
struct TableInfo {
/**
* Construct a new TableInfo instance.
* @param schema The table schema
* @param name The table name
* @param table An owning pointer to the table heap
* @param oid The unique OID for the table
*/
TableInfo(Schema schema, std::string name, std::unique_ptr<TableHeap> &&table, table_oid_t oid)
: schema_{std::move(schema)}, name_{std::move(name)}, table_{std::move(table)}, oid_{oid} {}
/** The table schema, the column information about the Table */
Schema schema_;
/** The table name */
const std::string name_;
/** An owning pointer to the table heap, it point to the data of this table */
std::unique_ptr<TableHeap> table_;
/** The table OID */
const table_oid_t oid_;
};
这里说明一张表(Table) 的信息存储在 TableHeap
中. 我们要理解的就是 TableHeap
在内存的组织方式.
另一个需要理解的是检索(Index), 检索可以快速的在内存中使用检索定位到具体的 Tuple
. 在代码中, 检索的抽象如下:
/**
* The IndexInfo class maintains metadata about a index.
*/
struct IndexInfo {
/**
* Construct a new IndexInfo instance.
* @param key_schema The schema for the index key, 检索包含了这张表的哪些属性
* @param name The name of the index 检索的名称
* @param index An owning pointer to the index
* @param index_oid The unique OID for the index
* @param table_name The name of the table on which the index is created 检索作用与哪张表
* @param key_size The size of the index key, in bytes 检索的 Key 的大小
*/
IndexInfo(Schema key_schema, std::string name, std::unique_ptr<Index> &&index, index_oid_t index_oid,
std::string table_name, size_t key_size)
: key_schema_{std::move(key_schema)},
name_{std::move(name)},
index_{std::move(index)},
index_oid_{index_oid},
table_name_{std::move(table_name)},
key_size_{key_size} {}
/** The schema for the index key
* 由于index key 可能是由多个属性决定的, 所以是 Schema 类型
*/
Schema key_schema_;
/** The name of the index */
std::string name_;
/** An owning pointer to the index */
std::unique_ptr<Index> index_;
/** The unique OID for the index */
index_oid_t index_oid_;
/** The name of the table on which the index is created */
std::string table_name_;
/** The size of the index key, in bytes */
const size_t key_size_;
};
直观上看, 一张表的数据 TableHeap
更多的是包含表的行相关的信息, 也就是 Tuple
的信息, 将这些行存储起来就得到一张表. 而 Index
更多的是 columns
相关的信息, 它要比 TableHeap
更加复杂, 需要检索到具体行的具体列存储的信息.
数据库表的信息在内存中的存储形式
因此我们自底向上的存储方式描述数据库表在内存中的存储结构.
我们首先看一下 tuple 类, 它包含的属性如下:
class Tuple {
friend class TablePage;
friend class TableHeap;
friend class TableIterator;
private:
// Get the starting storage address of specific column
auto GetDataPtr(const Schema *schema, uint32_t column_idx) const -> const char *;
RID rid_{}; // if pointing to the table heap, the rid is valid
std::vector<char> data_;
};
我们需要知道的是 Tuple
并不表示 Tuple
在内存的存储形式, 它是对一个 Tuple
的抽象, 它包含了, RID
(Tuple在内存中索引), 以及数据部分, 其中 RID
是结构性的, 它的定义如下:
class RID {
private:
page_id_t page_id_{INVALID_PAGE_ID};
uint32_t slot_num_{0}; // logical offset from 0, 1...
};
它包含了一个 Page_ID 信息以及一个 slot_num(位置信息). 我们很容易推论到, 我想要做的是, 将一个 Tuple
对象的 data_
部分存储到某一个 Page 的某个位置上. 这个存储形式是什么样子的呢? 下图描述了这个存储结构:
我们结合一下 TablePage
的定义就可以很清楚的看到上面的描述信息了:
/*
* Header format (size in bytes):
* ----------------------------------------------------------------------------
* | NextPageId (4)| NumTuples(2) | NumDeletedTuples(2) |
* ----------------------------------------------------------------------------
* ----------------------------------------------------------------
* | Tuple_1 offset+size (4) | Tuple_2 offset+size (4) | ... |
* ----------------------------------------------------------------
*
* Tuple format:
* | meta | data |
*/
class TablePage {
private:
// 这里使用的是标准库的 tuple 数据类型, 不是我们这里定义的 tuple 的数据类型
using TupleInfo = std::tuple<uint16_t, uint16_t, TupleMeta>;
char page_start_[0];
page_id_t next_page_id_;
uint16_t num_tuples_;
uint16_t num_deleted_tuples_;
TupleInfo tuple_info_[0];
static constexpr size_t TUPLE_INFO_SIZE = 16;
static_assert(sizeof(TupleInfo) == TUPLE_INFO_SIZE);
};
其中 Header 的部分包含了
page_id_t next_page_id_;
uint16_t num_tuples_;
uint16_t num_deleted_tuples_;
一共 8 个字节. 每一个 TupleInfo
包含了这个 Tuple 的下列信息:
- 这个 Tuple 在这个 Table_Page 中的地址偏移
- 这个 Tuple 的数据部分的大小
- 这个 Tuple 的
TupleMeta
信息, 主要记录这个 Tuple 的事务信息.
需要注意的一点是, 在 Table_Page 中,TupleInfo
是从前向后插入的, 并且每次插入的时候, 将下标的 slot_id 写入到这个 Tuple 的 RID 中, 但是对应的 data_ 在存储的时候, 是从后向前存储的, 所以中间部分是空的.
显然, 通常一张表的实际数据比较大, 一个 Page 肯定存储不下, 要使用多张 Pages 存储这个 Table, 多张 Pages 在内存中组织的形式是将这个 TablePage 组织成一个 TableHeap
的形式. 如上图所示.
检索一个 Tuple 在内存的步骤
上述介绍了 Tuple 在内存中自底向上的存储形式, 我们反过来可以从一个 Tuple 中找到它在内存的具体地址. 首先在 TableInfo 获取指向 TableHeap
的指针, 找到 TableHeap
中的第一页(TablePage), 找到这一页之后通过链表的结构, 找到这个 Tuple 在 BufferPool 中的那一页, Tuple 指定了 Page_ID. 然后根据这个 Tuple 中 RID 信息, 可以得到在这个 TablePage 中的 TupleInfo
, 获取这个 Tuple 的data 在这个 TablePage 中的偏移, 即可找到这个 Tuple 在内存存储的真实地址.
需要注意的时候, Tuple 是只能在 Table 中修改的, 也就是说, 不会存在单独修改一个 Tuple->data_
这种用法, 只有具体修改这个 Tuple 在表中的数据才有意义, 列入修改一个 Tuple
之后进行一次插入操作.
bustub 中检索的工作原理
数据库中检索的作用是快速在表中定位到一个 tuple 的位置, 索引并不直接与内存打交道, 因为Index并不直接存储这个 Tuple 中的所有数据, 更多的是和 Columns 相关, 找到这个 Tuple 在内存的位置即可. 我们知道 Tuple 在内存的位置通常使用 RID 存储, 所以它就是检索的 Value 值, 而 Key 则是这个 Tuple 的检索属性存储的值. 因此索引定义了下列的属性信息:
/**
* class IndexMetadata - Holds metadata of an index object.
*
* The metadata object maintains the tuple schema and key attribute of an
* index, since the external callers does not know the actual structure of
* the index key, so it is the index's responsibility to maintain such a
* mapping relation and does the conversion between tuple key and index key
*/
class IndexMetadata {
private:
/** The name of the index */
std::string name_;
/** The name of the table on which the index is created */
std::string table_name_;
/** The mapping relation between key schema and tuple schema */
const std::vector<uint32_t> key_attrs_;
/** The schema of the indexed key */
std::shared_ptr<Schema> key_schema_;
};
其中 key_schema_
是这个 Index 使用的这个 Table 中的部分属性 Columns 作为索引, 表示的是属性信息. 那么既然已经有属性信息了, 为什么还需要 key_attrs_
呢, 这是因为 key_attrs_
存储的是 key_schema_
在 Table_schema
中的位置, 这是因为对于单个 Tuple 来说, 无法直接使用 Table_schema
的子集获取对应的元素, 只能通过 Table_schema
和 Column_idx 获取Tuple中某个属性的元素, 这里核心的函数就是构建一个 Index 的时候需要使用下面的函数, 读取这个 Tuple 的 key_schema_
属性的元素, 这个函数如下:
auto Tuple::KeyFromTuple(const Schema &schema, const Schema &key_schema, const std::vector<uint32_t> &key_attrs)
-> Tuple {
std::vector<Value> values;
values.reserve(key_attrs.size());
for (auto idx : key_attrs) {
values.emplace_back(this->GetValue(&schema, idx));
}
return {values, &key_schema};
}
我们可以看到获取这个 Tuple 的某个属性的 Value 的时候, 使用函数 GetValue()
, 只能使用这个 Table 的 schema 以及 column_idx. 这就是 key_attrs_
的作用了.
构建索引的方式
根据前面实现索引方式的不同, 我们可以使用 B+ 树索引, 或者可扩展 Hash 索引, 为一张表构建索引的核心部分代码如下:
// define a index
auto index = std::make_unique<BPlusTreeIndex<KeyType, ValueType, KeyComparator>>(std::move(meta), bpm_);
// Populate the index with all tuples in table heap
auto *table_meta = GetTable(table_name);
for (auto iter = table_meta->table_->MakeIterator(); !iter.IsEnd(); ++iter) {
auto [meta, tuple] = iter.GetTuple();
// KeyFromTuple get the Index corresponding value in the tuple, therefore, using index columns in tuple could
// implement the index for the tuple Rid
index->InsertEntry(tuple.KeyFromTuple(schema, key_schema, key_attrs), tuple.GetRid(), txn);
}
其中 BPlusTreeIndex
是我们使用之前实现的 B+ 树索引构造的一个 B+ 树的检索, 从这里可以看出, 它的插入步骤如下:
INDEX_TEMPLATE_ARGUMENTS
auto BPLUSTREE_INDEX_TYPE::InsertEntry(const Tuple &key, RID rid, Transaction *transaction) -> bool {
// construct insert index key
KeyType index_key;
// deep copy the data from the key, should not move, otherwise the data in tuple will be lost
index_key.SetFromKey(key);
// Insert the Key_Value into the B_Plus_Tree_Index
return container_->Insert(index_key, rid, transaction);
}
从其中可以看出, 插入的键值对的 Key 是这个 Tuple 索引属性对应的 data 部分, 而 Value 是这个Tuple 的 RID, 这样就可以快速的通过检索数据结构得到一张表中的某个 tuple 在内存中的位置信息了.
我们也可以在函数 GetTableIndexes
看到 Index 的用法:
/**
* Get all of the indexes for the table identified by `table_name`.
* @param table_name The name of the table for which indexes should be retrieved
* @return A vector of IndexInfo* for each index on the given table, empty vector
* in the event that the table exists but no indexes have been created for it
*/
auto GetTableIndexes(const std::string &table_name) const -> std::vector<IndexInfo *> {
// Ensure the table exists
if (table_names_.find(table_name) == table_names_.end()) {
return std::vector<IndexInfo *>{};
}
// there are maybe more than one indexes in a table
auto table_indexes = index_names_.find(table_name);
BUSTUB_ASSERT((table_indexes != index_names_.end()), "Broken Invariant");
std::vector<IndexInfo *> indexes{};
// preserve a space to save the indexes for a table
indexes.reserve(table_indexes->second.size());
for (const auto &index_meta : table_indexes->second) {
auto index = indexes_.find(index_meta.second);
BUSTUB_ASSERT((index != indexes_.end()), "Broken Invariant");
// in this part, we shoud get the source pointer of the unique_ptr of the indexes
indexes.push_back(index->second.get());
}
return indexes;
}
上面是我们从一个 Catalog 中获取一张表的检索信息, 需要注意的是, 在 Catalog 中, 存储的本来都是指向检索的智能指针, 我们需要获得它的原指针(source pointer).
这也表明了 Catalog 的本质就是一个画布, 上面存储了我们数据库中的所有的表和这些表对应的索引信息.