news 2026/9/14 14:56:56

MiniOB源码解析:用C++亲手实现一个数据库内核

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MiniOB源码解析:用C++亲手实现一个数据库内核

简介:这份基于C++的MiniOB数据库系统源码包,是OceanBase与华中科技大学联合开发的数据库内核入门实践项目。它面向在校学生和数据库初学者,重点帮助理解存储管理、查询优化、事务处理等模块,通过简化实现降低学习门槛,并支持SELECT、INSERT、UPDATE、DELETE等常用SQL操作。资源共360个文件,压缩包仅3.18MB,主体为121个h头文件与107个cpp源文件,另配PNG架构图、Markdown文档、测试用例、INI配置及辅助脚本,完整覆盖词法语法分析、B+树索引、缓冲池、执行器等核心机制,便于对照源码逐层研读。目前已有131人学习下载,适合作为数据库课程设计、内核入门或项目实践的参考资料。借助完整工程、构建脚本与测试程序,读者可快速搭建MiniOB环境,结合文档和示例理解SQL从解析、优化到执行的完整链路,也可按模块逐段调试,是深入学习数据库内核的实用素材。

1. MiniOB 是什么,为什么值得读这套 C++ 数据库源码

MiniOB 是蚂蚁 OceanBase 团队开源的教学用数据库内核,整套系统用 C++ 写成,目标不是生产,而是把"数据库到底怎么工作"拆到一门课能讲完的规模。你拿到手的这个压缩包,就是一份完整可编译的 MiniOB 源码:有 SQL 解析、执行器、B+ 树索引、Buffer Pool 缓冲池,还有配套的客户端和服务端。对想补数据库内功的 IT 从业者、正在做数据库课程设计的学生来说,读这套源码比背 C++ 八股有用得多——你能亲手看到一条select从字符串变成执行计划再落盘的全过程。后面按架构、编译、改代码、排错四条线展开,所有操作都能在你本地这份源码里复现。

2. 从 Buffer Pool 到执行器:MiniOB 的 C++ 架构拆解

2.1 源码目录:先定位 6 个核心模块

拿到 zip 解压后,第一件事不是急着编译,而是先把目录结构过一遍。MiniOB 的源码规模不大,但分层很规矩,常见的布局如下:

miniob/ ├── src/observer/ │ ├── sql/ │ │ ├── parser/ # lex_sql.l / yacc_sql.y / parse_defs.h │ │ ├── expr/ # 表达式求值 │ │ ├── optimizer/ # 逻辑计划与物理计划 │ │ ├── operator/ # 火山模型执行算子 │ │ └── executor/ # 语句分发与执行入口 │ ├── storage/ │ │ ├── buffer/ # Buffer Pool 缓冲池 │ │ ├── record/ # Record Manager 记录管理 │ │ └── table/ # 表定义与关联索引 │ └── event/ # 会话事件 ├── etc/observer.ini # 服务端配置 ├── unittest/ # 单元测试 └── build.sh / CMakeLists.txt
模块核心职责建议入口文件
Buffer Pool页缓存、磁盘页加载与淘汰buffer_pool_manager.cpp
Record Manager记录槽位与增删改原语record_file_handler.cpp
IndexB+ 树键值维护bplus_tree.cpp / index.cpp
ParserSQL 字符串转语法树yacc_sql.y / parse_defs.h
Operator火山模型算子operator/ 目录
Executor语句分发与结果组装default_stage.cpp / 各 Stmt 工厂

不同版本目录名会有出入:老版本把执行逻辑集中放在default_stage.cpp的一个大分发里,新版本拆成了Stmt -> LogicalOperator -> PhysicalOperator的工厂链。判断标准只有一个——从src/observer/init.cppmain函数顺着调用链走,语义边界跑不出上面六类。这一步值得花十分钟,因为后面每次加功能,你都得先回答"改动落在哪个模块"。

2.2 Buffer Pool 与 Record Manager:数据落盘的最小闭环

数据库和普通文件读写最大的区别,是它不信任操作系统替你规划的缓存。MiniOB 的 Buffer Pool 自己管理若干页,页大小由源码里的BP_PAGE_SIZE这类常量定义,一般是 4KB 到 16KB。所有对表数据的读取都先查缓冲池,未命中才从磁盘加载页;淘汰策略在课程版本里通常就是简单的 LRU 或寻找空闲 frame。这块代码建议重点看两个方法:

// buffer 模块中两个核心接口的简化形态 Page *BufferPoolManager::get_page(int page_num) { // 1. 先查内存映射表,命中直接返回,避免磁盘 IO // 2. 未命中时分配一个 frame,用文件描述符读入页数据 // 3. 记录 pin_count,防止页在引用期间被换出 } void BufferPoolManager::unpin_page(int page_num) { // 减少 pin_count // 当 dirty 标记置位时,由后续 flush 或淘汰时机统一写回磁盘 }

这里有一个容易被忽视的细节:pin_count不为 0 的页不能被换出,这是正确性的底线。课程实验里改坏 Buffer Pool 的典型症状是"查询结果偶发错乱",根因往往就是把仍在引用中的页提前刷掉了。验证方法很简单,把表数据量加到超过缓冲池容量,再反复全表扫描对比结果。B+ 树的叶子节点内部对键值的查找用的就是二分查找,复杂度 O(log n),这部分代码恰好把数据结构课上的 C++ 二分查找落到了真实场景里。

Record Manager 负责把表文件里的记录按固定槽位组织,向上层提供insert_recorddelete_recordget_record原语。它与 Buffer Pool 的分工是:上层只认逻辑记录号,底层才关心页号与页内偏移。改这两层时,建议在get_page入口打印页号访问序列,你会直观看到一条 SQL 背后发生了多少次"缓冲命中/未命中"的切换。

提示:修改 Buffer Pool 淘汰逻辑后,一定要做"数据量大于缓冲池容量"的压力测试,只跑小表验证不出换页路径。

2.3 SQL 解析与执行器:一条 select 的旅程

MiniOB 的词法与语法是 flex/bison 生成的老搭档:.l文件把字符串切成 token,.y文件按文法归约成抽象语法树,语法树节点定义在parse_defs.h。一条select * from t where id = 1会先变成SelectSqlNode,里面挂着投影列、表名和条件列表,单个条件就是ConditionSqlNode

// parse_defs.h 中条件节点的典型定义 struct ConditionSqlNode { char *left_operand; // 字段名或常量 char *right_operand; CompOp comp; // 等于、不等于、大小比较 };

解析完之后进入执行器,MiniOB 采用经典的火山模型:每个算子实现一个next(),从子节点拉一条元组,处理完向上抛出。最常见的链路是TableGetOperator -> PredicateOperator -> ProjectOperator,分别对应"扫表、过滤、投影"三个动作。调试执行计划时,在PredicateOperator::next()里打断点,就能看到每一行数据是怎么被where条件拦下来的。

理解这条链路的价值在于:第四章加 UPDATE 语句时,词法加一个关键字、语法加一条产生式、语法树加一个节点、执行器加一个算子,四处改动对应四个模块,链路是完整的。很多人改到一半编译报错就慌,其实报错位置本身就是最好的导航——编译器会告诉你改漏了哪一环。

3. 本地编译与启动:用 CMake 把 MiniOB 跑起来

3.1 环境准备与编译参数

MiniOB 对平台有点挑剔:官方支持 Linux 和 macOS,Windows 上最稳的路是 WSL 里的 Ubuntu,不要在原生 Windows 环境里跟 MSVC 较劲。这套代码不依赖外部数据库,但需要 g++、cmake、make、flex、bison。一个常见干扰项是:在 Windows 上装 Python 相关包时撞见error: microsoft visual c++ 14.0 or greater is required,那是 pip 装包需要 MSVC 编译器,和 MiniOB 构建没有关系,换到 WSL 里编译可以完全绕开。

# 以 Ubuntu/WSL 为例,装齐构建工具链 sudo apt-get install -y g++ cmake make flex bison libjsoncpp-dev # 在解压后的源码根目录执行 cmake -S . -B build -DDEBUG=ON -DCMAKE_BUILD_TYPE=Debug cmake --build build -j "$(nproc)" ls build/bin/

-DDEBUG=ON打开调试日志与断言,课程实验阶段建议一直保持;-j后面的并发数按 CPU 核数给,避免小机器编译内存溢出。如果你用 VSCode 读代码,建议顺手让 CMake 生成compile_commands.json,配合 C/C++ 插件的跳转和补全会准很多。第一次全量编译大约几分钟,编译产物都落在build/bin/下,这是后面所有操作的基础。

3.2 启动 observer 并用客户端连上

编译产物里有两个可执行文件:observer是服务端,obclient是配套的命令行客户端。服务端默认监听 6789 端口,配置文件在etc/observer.ini

# 终端 A:前台启动服务端,日志直接打到标准输出 ./build/bin/observer -f ./etc/observer.ini # 终端 B:连接本机 6789,进入 SQL 交互 ./build/bin/obclient -h 127.0.0.1 -p 6789

连上之后先跑一套最朴素的增删改查,验证内核基本功能正常:

create table student (id int, name char(32), score float); insert into student values (1, 'alice', 90.5); insert into student values (2, 'bob', 85.0); select * from student where id = 1; create index student_idx on student (id); delete from student where id = 2;

能依次拿到正常结果,说明解析、执行、存储、索引四条链路都是通的。这里有一个值得做的验证:create index建完索引后,再执行select * from student where id = 1,观察日志里走的是全表扫描还是索引查找。MiniOB 课程版的优化器不一定总是选中索引,这个"知道有索引但不一定用得上"的取舍,正是你后续自己补成本估算逻辑的起点。

3.3 配置文件与常见启动失败排查

etc/observer.ini里最常调的是缓冲池大小和日志级别,以你这份源码里真实的键名为准,一般能看到类似结构:

[server] PORT = 6789 MAX_CONNECTION_NUM = 100 [buffer_pool] BUFFER_POOL_SIZE = 1024 # 单位是页,不是字节 [log] LOG_LEVEL = INFO

启动失败的高频原因集中在这三类:

症状原因处理
connect refused端口没起来、被占用或客户端端口写错lsof -i:6789确认,改了 PORT 同步改客户端参数
启动即段错误配置里的数据目录用了相对路径数据目录与日志路径全部改绝对路径
WSL 下编译极慢源码放在/mnt/c/跨文件系统整个目录复制到家目录再编译

注意:把数据目录和日志文件路径都写成绝对路径,是 MiniOB 启动排错的第一步,八成路径相关的问题都出在这里。

日志级别从INFO调到DEBUG后,日志里会直接打印每条 SQL 对应的解析树特征和执行算子类型,这个习惯会贯穿第五节的排错过程。

4. 动手改功能:以 UPDATE 语句为例走通 MiniOB 全链路

4.1 词法与语法:在 lex_sql.l 和 yacc_sql.y 里加规则

很多课程版本把update留作实验题,新版本自带实现。不管自带还是自补,链路完全一样,只是你要不要亲手写一遍的区别。先从词法开始,把UPDATE加进lex_sql.l的关键字表,否则词法分析器会把它当成普通标识符。然后在yacc_sql.y里参照 insert 的规则,加一条产生式:

update_stmt: UPDATE IDENT SET update_units WHERE where_clause { // 归约动作里把表名、更新单元列表、条件列表 // 组装成一个 UpdateSqlNode,挂到 parse result 上 } ;

注意update_units需要单独定义成列表规则:update_units : update_unit | update_units COMMA update_unit,这是 yacc 里表达"一个或多个"的标准写法。语法归约只负责把 token 变成结构,不要在这里做类型检查,词法层拿到的都是裸字符串。

4.2 语法树节点与语句分发

语法树节点的定义放在parse_defs.h,字段就是你在 SQL 里能写出来的内容的镜像:

struct UpdateUnit { char *field_name; // 要更新的列名 Value value; // 新值,先不推导类型 }; struct UpdateSqlNode { char *table_name; // 目标表 std::vector<UpdateUnit> update_units; // 支持一次更新多列 std::vector<ConditionSqlNode> conditions; // WHERE 条件 };

Value是 MiniOB 对字段值的统一封装,内部用AttrType区分整型、浮点、字符串。分发的中心在新版本是Stmt::create_stmt工厂,老版本在default_stage.cpp的大 switch 里:按SqlNodeType进入对应分支,把ParsedSqlNode翻译成可执行的UpdateStmt。这层要做真正的元数据校验——表是否存在、列是否存在、值类型是否匹配。报错越早,后面的执行器越省事;凡是能在这层拦下来的错误,都不要拖到执行期。

4.3 执行器:读取、改写、写回与索引维护

执行阶段分三步:定位记录、改写字段、写回。核心流程如下:

RC UpdateExecutor::execute() { // 1. 通过表对象拿到 record_handler // 2. 遍历表记录或走索引定位,对命中行复制一份内存副本 // 3. 在内存副本上按 update_units 写入新值 // 4. 调用写回接口刷盘,并逐个同步一级、二级索引 }

动手时有两个坑必须处理。第一个坑是字符串变长:原记录里char(32)存了 10 个字符,改写成 40 个字符就会超过槽位。MiniOB 的简化存储结构不能像 InnoDB 那样把行迁移到溢出页,所以要么拒绝过长的值并返回明确错误码,要么在解析层就按字段长度截断,不要静默写坏相邻记录。第二个坑是索引同步:更新了作为索引键的列,必须先删旧键再插新键,顺序反了会导致 B+ 树里出现两份键值。验证顺序的办法是在索引模块的插入和删除函数里各加一行日志,用一条带唯一索引的更新语句观察调用次序。

提示:MiniOB 课程版事务能力很薄,多数版本只支持单语句隐式事务。两个客户端同时 update 同一行,不要把它当作预期能通过的功能,那是后续自己实现锁管理器时才覆盖的场景。

5. 验证与排错:三个立刻能用的 MiniOB 调试技巧

5.1 把日志级别调到 DEBUG,直接看执行算子

observer.ini里的日志级别从INFO改成DEBUG并重启。每条 SQL 执行后,日志会打出解析出的语句节点类型、优化器选择的算子链,以及各算子处理的记录数。排查"语法对了但结果错"这类问题时,这是最快的手段:先确认语法树字段对不对,再确认执行算子链选没选对,最后看过滤条件是否生效。

5.2 在算子的 next() 上打断点

针对"只影响特定 SQL"的 bug,用 gdb 挂在算子方法上:

gdb ./build/bin/observer break PredicateOperator::next run -f ./etc/observer.ini # 另开终端用 obclient 发一条带 where 的 select # 命中断点后 print 当前元组内容,continue 观察逐行过滤

断点命中后配合bt看调用栈,能同时看到解析与执行上下文。如果算子内是链表或数组遍历,给断点加condition指定行号区间,避免每行都停。

5.3 用 ctest 跑回归用例,别只靠手工 SQL

MiniOB 自带unittest/目录,构建完成后用ctest统一跑:

cd build ctest --output-on-failure # 也可以直接运行单个测试二进制,例如最小化复现 B+ 树问题 ./bin/bplus_tree_test

加新功能的正确姿势是双保险:先在解析层写一个"输入 SQL 字符串、断言语法树字段值"的用例,再到执行层写"建表、插数据、执行语句、查询断言结果"的用例。这样改坏 Buffer Pool 或 B+ 树时,是单测先红,而不是你手工敲 SQL 敲到怀疑人生。拿到一份新版本 MiniOB 源码时,我的习惯是先跑一遍基线测试全绿,再动任何代码。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 14:56:09

MATLAB实现MIT-BIH心电信号预处理:从WFDB读取到QRS检测

简介&#xff1a;面向MIT-BIH心律失常数据库的MATLAB心电信号预处理程序&#xff0c;适合生物医学工程、数据科学及心脏病学领域的研究者与工程师&#xff0c;用于去除ECG中的基线漂移、肌电干扰和电源噪声&#xff0c;提升后续分析可靠性。压缩包共含2个文件&#xff0c;以m脚…

作者头像 李华
网站建设 2026/9/14 14:55:44

Windows内存占用居高不下?从任务管理器到RAMMap的排查实战

前几天一个朋友找我说&#xff0c;电脑没开游戏、也没跑渲染&#xff0c;刚开机一会儿内存占用直接飙到90%&#xff0c;鼠标飘得跟喝醉了一样。我远程一看&#xff0c;任务管理器里一堆奇奇怪怪的进程排在前排&#xff0c;罪魁祸首根本不是他以为的“某个软件”&#xff0c;而是…

作者头像 李华
网站建设 2026/9/14 14:53:52

MATLAB时频分析实战:STFT、CWT与EMD-HHT全解析

简介&#xff1a;这份MATLAB时频分析程序包面向信号处理初学者与工程师&#xff0c;覆盖短时傅里叶变换、小波变换、Wigner-Ville分布及EMD/EEMD等常见方法&#xff0c;配套大量带exa编号的示例脚本&#xff0c;可系统学习时频分析原理与实现。压缩包共40个文件&#xff0c;以3…

作者头像 李华
网站建设 2026/9/14 14:51:50

TurboQuant技术:大模型KV Cache高效压缩方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 14:48:41

火鸟门户V4.8部署指南:PHP地方门户框架环境配置与五端同步实践

简介&#xff1a;这是一套面向地方政务与社区服务场景的PHP开源门户系统&#xff0c;适用于有本地化运营需求的开发者、中小型企业及地方政府信息化建设团队&#xff0c;可快速搭建集资讯发布、用户互动、养老服务与社群管理于一体的五端同步&#xff08;PC、H5、小程序、APP、…

作者头像 李华