TOC
Open TOC
info
远程仓库
https://github.com/oceanbase/miniob
2022 OceanBase 数据库大赛
https://open.oceanbase.com/competition/index
官网教程
https://ask.oceanbase.com/t/topic/35601006
第一届比赛预赛题目
https://open.oceanbase.com/train?questionId=200001
往届参赛选手经验
https://zhuanlan.zhihu.com/p/445201899
https://zhuanlan.zhihu.com/p/455956866
头歌
https://www.educoder.net/paths/e26a9zcj
setup
部分外部依赖通过 submodule 引入,官方提供了 build.sh
脚本用于初始化环境
log
gtest 测试中无法打印日志
日志在 observer.log.*
中查看
设置级别为 trace
storage
file 由一系列 pages 组成
page 的布局如下
frame 中存放 page,通过 BufferPoolManager
管理
BufferPoolManager
的成员如下
BPFrameManager
,继承自 MemPoolSimple
内存池,使用 lru 策略
- name / fd ->
DiskBufferPool
,其中 fd 代表 file 的文件描述符
BufferPoolManager
全局唯一,用于打开 file 构造 DiskBufferPool
每一个 file 的第一个 page 的开始部分为 BPFileHeader
其余 page 可用于存放 record,其开始部分为 PageHeader
,或者用于存放 index
使用 RecordPageHandler
和 RecordFileHandler
管理 record
overview
- network
Server::recv
- seda (stage event driver architecture)
- 处理顺序在 observer.ini 中定义
- session
push_callback
- Server::send
- create
SQLStageEvent
- handle
- query_cache (empty)
- parse
push_callback
- done_immediate SessionEvent
- handle
- done_immediate
SQLStageEvent
- resolve
Stmt::create_stmt
预处理 insert / select / delete
- handle
- plan_cache (empty)
- optimize (empty)
- execute
- set_response
SessionEvent
- (default_storage)
test
官方配有测试脚本,迁移到 https://gitee.com/oceandb-space/miniob-test 中
测试分类如下
DDL
- drop table
- unique index
- multi index -> drop / date
DML
- multi insert
- update
- select
- group by with aggregation
- order by
- expr
- sub query
- join
field
- date
- null -> aggregation / cross join
- text
今年新增的测试用例
- typecast
- like
- update-select
- alias
- function
- clog
- show-index
analysis
create table
create table t1 (a int)
t1.table
table_meta
- sys_fields
__trx
- fields
__trx / a
- record_size 8
t1.data
(page 0)
- page_num
- BPFileHeader
- page_count
- allocated_pages
- bitmap
insert
insert into t1 values (1)
t1.data
(page 1)
- page_num
- PageHeader
- record_num = 1
- record_capacity = 2013
record_capacity * 8 + record_capacity / 8 + 1 <= 16384 - 4 - sizeof(struct PageHeader)
- record_real_size 对齐前
- record_size 对齐后
- first_record_offset = 272
align8(sizeof(struct PageHeader) + 2013 / 8 + 1)
- 相对于 data 区的 offset,不包含 page_num
- bitmap
- records
再插入一条记录 insert into t1 values (2)
select
create index
create index i_a on t1 (a)
假设已有一条记录
具体分析 BplusTreeHandler::insert_entry()
- user_key = 1
- rid = [page 1, slot 0]
key = user_key + rid
插入的是 (key, rid),对应的大小为 20
t1-i_a.index
(page 1)
- page_num
- IndexFileHeader
- root_page = 2
- internal_max_size = (16384 - 12 [page type + item number + parent page id]) / 16 [attr len + RID + page num] = 1023
- leaf_max_size = (16384 - 20 [page type + item number + parent page id + prev and next brother]) / 20 [attr len + RID + RID] = 818
- attr_length = 4
- key_length = 12
- attr_type = INTS
(page 2)
- page_num
- header
- page type = is leaf
- item number = 1
- parent page id = -1
- prev and next brother = -1
- items
在插入新的 record 时,会在 Table::insert_record
中调用 Table::insert_entry_of_indexes
更新索引
frontend
flex and bison
char
默认长度为 4
插入时,超过长度的部分会被截断,以下图 t2.data
为例,字段长度对齐为 16
插入 '111aa' / '222bb' / '111aabb' / '222' / ''
的结果如下
具体逻辑在 Table::make_record
中
miscellaneous
代码是写给人看的
http://www.ruanyifeng.com/blog/2016/01/commit_message_change_log.html
https://github.com/commitizen-tools/commitizen
tech
output parameters
返回值类型统一为 RC
,为了返回更多的信息,使用 output parameters 技术
Exception vs. RC
golang
defer
启发于 golang
adapter pattern
RecordReaderScanAdapter
接口适配
sql design
对于 DDL 而言,bind 之后可以直接执行
主要考虑 DML
nanodb
ANTLR parser -> NanoSQLTranslator
-> prepare plan -> planner -> executor
语法规则似乎不必手写
bustub
调试,重定向输入
postgres parser -> binder (transformer) -> planner -> optimizer -> executor
TableReferenceType
ExpressionType
BusTub 养成记:从课程项目到 SQL 数据库
miniob
- group by with aggregation
max / min / count / avg
聚合函数中的参数不会是表达式
思路参考 NanoDB Assignments
在内存中提前排序
left deep tree
递归解析
字段拼接
算术表达式的语法规则
env 机制
只有 where subquery
in / not in - 向量
cmp - 标量
schema 应匹配
cmake
官方写的 CMakeLists.txt
实在有点无语,稍微修改了一下
使用如下命令构建
perf
inner join 的最后一个测试用例超时,考虑使用 perf 跑个火焰图
参考了 Use perf and FlameGraph to profile program on Linux | Nan Xiao’s Blog
发现对 predicate 求值的时间太长,于是考虑谓词下推
clog
分 block 存储,第一个 block 为文件头,每个 block 为 512 字节
buffer 为 4096 字节
insert into t1 values (1)
insert
- header
- lsn - 0
- trx id - 1
- type - REDO_INSERT
- log record len - 80
- table name - t1
- rid - [page 1, slot 0]
- data len - 12
- data
- padding - align to 8
commit
- header
- lsn - 80
- trx id - 1
- type - REDO_MTR_COMMIT
- log record len - 16
commit 后,clog 会落盘,此时数据尚未落盘
restart server 后,server 会根据 clog 恢复出 CLogRecord,然后进行如下处理
这里说明在非 trx multi operation mode 下,不必手动添加 REDO_MTR_BEGIN
类型的 CLogRecord
之后遍历 log_redo_list
,进行如下处理
一个简单的测试用例
client1
client2
client1
restart server
client1
restart server
client1
refactor
sql parser
前端解析考虑使用开源的 parser,这里使用了 hsql
需要单独处理 insert value 中出现负数字面量
同时,考虑到复杂表达式在语法分析过程中,其原始形式的信息会丢失,所以考虑手写一个简单的 parser 解析 select values 及其 aliases
binder
经过 hsql 解析后的 sql 命令已经有了较好的形式,但是为了后端处理的方便,需要通过 binder 进行进一步转换
例如对于 select 语句,hsql 会解析为如下的结构体
需要根据 TableRef
得到 from schema,建立 enclosing schema,从而在子查询中能够对 column expr 求值
需要将所有的 Expr
转换为 AbstractExpression
,其中提供了 eval / traverse 等方法
planner
这一部分和 nanodb 类似,不多赘述
由于中间表示的不同,需要单独为 update 和 delete 语句写一个简单的 planner
executor
执行阶段会根据 sql 语句是 DDL 还是 DML 进行相应的处理
对于 DML 语句,会构造相应的 binder 和 planner,最后对 plan node 调用下述接口
schema and tuple
这一部分位于 sql 层的 table 包中
Schema
就是 Column
的列表,Column
中包含的信息与框架代码中的 FieldMeta
类似,由于 FieldMeta
会与 json 文件进行序列化和反序列化,所以添加了 Column
这个类
Tuple
类和框架代码中的 Record
类似,为了获取 tuple 中的 value 信息,需要提供 schema,以调用 type system 中的 deserialize_from
方法
type system
参考了 bustub 的设计
Type
类提供了类型的单例,Type
类内部抽象了 value 和 values 之间会进行的操作
对于一个 Value
类的实例,只需根据自身获取类型的单例,然后调用 Type
类的抽象方法即可
这其中便涉及 implicit typecast 和 null comparison 等问题
对于 null comparison,miniob 规定 null 与任何数据对比都是 false
一般而言,null 与任何数据对比都是 unknown
于是在 in expr 中,若 lhs 或 rhs 中存在 null value
则求值的结果应为 false,然而注意到这里有个 not 取反,所以这里需要引入 unknown 三值逻辑
简单起见,若 bool value 的 is null 字段为 true,则认为是 unknown
在布尔值计算的根节点,判断若为 unknown 则计算为 false
对于三值逻辑,考虑有一个元组 (1)
的表 t
思考,unknown 的比较运算
function
在目前的题目中,function 可以分为 simple function 和 aggregate function,这两者均属于 scalar function,即返回值为标量
ScalarFunction
类提供 get_return_type
获取返回值类型
function 在 binder 中会转换为 FunctionCall
,其中包含了对应的 AbstractFunction
对于 aggregate function,在 group aggregate node 中会将 FunctionCall
转换为 ColumnValueExpression
,所以在其 FunctionCall
的 evaluate 方法中,只需根据 schema 获取对应的 column value 即可
对于 simple function,在其 FunctionCall
的 evaluate 方法中,应调用 SimpleFunction
提供的 evaluate 方法,其对应 expr 的类型不会进行转换
值得注意的一个 function 是 round,会对浮点数四舍五入到指定的精度,然而,前端解析和存储浮点数时可能存在误差,例如 select round(2.5);
,其中的 2.5 会被存储为 2.4999,此时后端就会四舍五入为 2,与实际不符,所以一种面向用例的解决方案是在后端多次 round,即 round(2.4999*)
实际的执行流为 round(round(2.4999*, 1), 0)
visitor pattern
主要体现在 AbstractExpression
中 traverse 方法,该方法入参为 ExpressionProcessor
,其中定义了 enter 和 leave 接口,enter 和 leave 接口的入参为 AbstractExpression
这样不同类型的 expr 只需实现 enter 和 leave 接口,就可以传入不同的 ExpressionProcessor
,对 expr 进行处理
ranking