实现一个简单SQL引擎
本文旨在帮助开发者在C/C++项目中,使用C++编程接口打造自己的SQL引擎。考虑到存储系统并非本文关注的重点,我们将简化存储层——使用内存表作为底层存储,这可以让我们更好关注引擎实现以及存储系统适配这些细节。
在深入详述实现细节以前,让我们简要概述实现一个简单内存表SQL引擎需要的步骤:
设计内存表结构
实现数据接口(
Catalog
,TableHandler
)子类:SimpleCatalog
,SimpleTableHandler
构造和执行引擎
完整的代码可参考simple_engine_demo
1. 内存表存储

typedef std::deque<std::pair<uint64_t, Row>> MemTimeTable;
typedef std::map<std::string, MemTimeTable> MemSegmentMap;
上图描述了内存表的存储结构:
内存表每个索引维护一个
SegmentMemMap
SegmentMemMap
是key
到时序表MemTimeTable
的映射。key
是根据数据的索引表达式值。key
相同的数据按时间排序后组织成内存时序表MemTimeTable
,最终绑定到key
上。
2. 实现数据接口子类
以及TableHandler
访问接口。
内部结构
我们在SimpleCatalog
内部维护hybridse::type::Database database_
和std::map<std::string, std::shared_ptr<SimpleCatalogTableHandler>>> table_handlers_
来维护和管理数据库和表信息。其中, table_handler_
是表名到SimpleCatalogTableHandler
的映射。SimpleCatalogTableHandler
的实现细节后面将会阐述。
接口实现
这里列出几处关键的函数和接口实现(更多细节可查阅simple_catalog.h和simple_catalog.cc)
构造函数
SimpleCatalog
的构造函数几乎没有额外工作,进提供index-based-optimzation
的开关初始化。这意味在,初始化后的SimpleCatalog
的数据库元信息和数据都是空的。
SimpleCatalog::SimpleCatalog(const bool enable_index)
: enable_index_(enable_index) {}
SimpleCatalog::~SimpleCatalog() {}
bool SimpleCatalog::IndexSupport() { return enable_index_; }
数据库、表元信息查询接口
std::map<std::string, std::shared_ptr<type::Database>> databases_;
std::map<std::string,
std::map<std::string, std::shared_ptr<SimpleCatalogTableHandler>>>
table_handlers_;
std::shared_ptr<type::Database> SimpleCatalog::GetDatabase(
const std::string &db_name) {
return databases_[db_name];
}
std::shared_ptr<TableHandler> SimpleCatalog::GetTable(
const std::string &db_name, const std::string &table_name) {
auto &dict = table_handlers_[db_name];
return dict[table_name];
}
数据操作
对于Catalog
来说,AddDatabase
和 InsertRows
都不是必须的。我们引入这两个操作方便添加元数据和数据。
内部结构
首先,它在内部维护table_storage
来维护完整的[内存表存储](#1. 内存表存储):
std::map<std::string, std::shared_ptr<MemPartitionHandler>> table_storage;
其中,MemPartitionHandler
就是MemSegmentMap
的TableHandler
实现,它内部就维护一个MemSegmentMap
。使用MemPartitionHandler
可以方便实现GetIterator
,GetWindowIterator
接口。
此外,它还维护了一个full_table_storage_
,来维护无索引场景下的全表内存。这种设计对是对内存的浪费,但我们作为演示,性能不是我们考虑的重点。
std::shared_ptr<MemTableHandler> full_table_storage_;
接口实现
我们列出几处关键函数和接口实现(更新细节可查阅simple_catalog.h和simple_catalog.cc:
构造函数
SimpleCatalogTableHandler
的构造函数通过分析传入的数据库名和表的TableDef
来初始化表的元信息,包括表类型信息TableDef
,索引列表IndexHint
,列类型信息Types
等。
数据库、表元信息查询接口
直接返回初始化好的各类表、索引、列信息即可
const Types &SimpleCatalogTableHandler::GetTypes() { return this->types_dict_; }
const IndexHint &SimpleCatalogTableHandler::GetIndex() {
return this->index_hint_;
}
const Schema *SimpleCatalogTableHandler::GetSchema() {
return &this->table_def_.columns();
}
const std::string &SimpleCatalogTableHandler::GetName() {
return this->table_def_.name();
}
const std::string &SimpleCatalogTableHandler::GetDatabase() {
return this->db_name_;
}
GetWindowIterator
使用MemPartitionHandler
使得分组迭代器GetWindowIterator()
的实现变得很简单,仅需要根据index_name
找到对应的分组表MemPartitionHandler
,然后直接调用MemPartitionHandler->GetWindowIterator()
。更多细节请查询mem_catalog.cc
std::unique_ptr<WindowIterator> SimpleCatalogTableHandler::GetWindowIterator(
const std::string &index_name) {
if (table_storage.find(index_name) == table_storage.end()) {
return nullptr;
} else {
return table_storage[index_name]->GetWindowIterator();
}
}
GetIterator
使用MemTableHandler
使得全表迭代GetIterator()
的实现变得很简单:
std::unique_ptr<RowIterator> SimpleCatalogTableHandler::GetIterator() {
return full_table_storage_->GetIterator();
}
更多细节请查询MemTableHandler::GetIterator()
GetCount和At接口
对于没有准备好支持的接口,可以返回一些默认值,并打印ERROR日志。很遗憾,目前HybridSE还没有错误系统来管理这些错误。
const uint64_t SimpleCatalogTableHandler::GetCount() {
LOG(ERROR) << "Unsupported operation: GetCount()";
return 0;
}
hybridse::codec::Row SimpleCatalogTableHandler::At(uint64_t pos) {
LOG(ERROR) << "Unsupported operation: At()";
return hybridse::codec::Row();
}
3. 构造和执行引擎
构建引擎
构造
SimpleCatalog
// build Simple Catalog
auto catalog = std::make_shared<SimpleCatalog>(true);
// database simple_db
fesql::type::Database db;
db.set_name("simple_db");
// prepare table t1 schema and data
fesql::type::TableDef table_def;
{
table_def.set_name("t1");
table_def.set_catalog("db");
{
::fesql::type::ColumnDef* column = table_def.add_columns();
column->set_type(::fesql::type::kVarchar);
column->set_name("col0");
}
{
::fesql::type::ColumnDef* column = table_def.add_columns();
column->set_type(::fesql::type::kInt32);
column->set_name("col1");
}
{
::fesql::type::ColumnDef* column = table_def.add_columns();
column->set_type(::fesql::type::kInt64);
column->set_name("col2");
}
}
*(db.add_tables()) = table_def;
catalog->AddDatabase(db);
准备数据库数据
// insert data into simple_db
std::vector<Row> t1_rows;
for (int i = 0; i < 10; ++i) {
std::string str1 = "hello";
codec::RowBuilder builder(table_def.columns());
uint32_t total_size = builder.CalTotalLength(str1.size());
int8_t* ptr = static_cast<int8_t*>(malloc(total_size));
builder.SetBuffer(ptr, total_size);
builder.AppendString(str1.c_str(), str1.size());
builder.AppendInt32(i);
builder.AppendInt64(1576571615000 - i);
t1_rows.push_back(Row(base::RefCountedSlice::Create(ptr, total_size)));
}
if (!catalog->InsertRows("simple_db", "t1", t1_rows)) {
return SIMPLE_ENGINE_DATA_ERROR;
}
配置引擎
EngineOption
我们简单只用默认引擎配置EngineOptions options;
EngineOptions options;
构造
Engine
示例
Engine engine(catalog, options);
编译和运行
编译
std::string sql = "select col0, col1, col2, col1+col2 as col12 from t1;";
base::Status get_status;
BatchRunSession session;
// compile sql
if (!engine.Get(sql, "simple_db", session, get_status) ||
get_status.code != common::kOk) {
return SIMPLE_ENGINE_COMPILE_ERROR;
}
执行
std::vector<Row> outputs;
// run sql query
if (0 != session.Run(outputs)) {
return SIMPLE_ENGINE_RUN_ERROR;
}
// print result
PrintRows(session.GetSchema(), outputs);
4. 运行SimpleEngineDemo
cd hybridse/build
cmake ..
make simple_engine_demo
./src/simple_engine_demo
Last updated