简介MiniOB是一款由OceanBase与华中科技大学联合开发的数据库内核入门工具面向零基础数据库学习者的C源码项目包。它通过简化实现帮助读者快速理解数据库内核各模块的职责与关联并训练设计高效SQL。资源包共363个文件压缩包仅3.17MB。核心为119个.h头文件与107个.cpp源文件覆盖INI配置解析、IO操作、互斥锁管理、字符串处理、日志系统、MD5加密、随机数生成、正则表达式匹配、内存池管理、事务日志、记录管理、索引管理等基础模块另有58个png图片、19个md文档以及测试用例、Dockerfile等辅助资源便于按图索骥学习。目前已有74人学习浏览适合需要入门数据库内核、完成课程训练或设计高效SQL的学生与开发人员使用。借助完整源码、测试文件和项目文档读者可系统梳理从模块实现到整体协作的脉络获得扎实的数据库底层实践体验。1. 为什么选MiniOB入门数据库内核从OceanBase开源出来的C教学项目MiniOB是OceanBase与华中科技大学联合维护的一个C教学数据库系统源码精简到能在一门课里学完却不缺数据库最关键的几条链路词法语法解析、执行器、存储引擎、索引。它是“玩具”但五脏俱全——拿到这份源码包你能跑通建表、插入、查询、创建索引的全过程也能在调试器里看到一条SQL从字符串变成语法树再从语法树变成磁盘页读写的完整轨迹。适合正在学数据库系统概论、被B树和缓冲池折磨的同学也适合想用C做点拿得出手东西的入门者。我当时拆这个包的第一天做的第一件事不是读代码而是先把它编译跑起来——能不能跑通决定了后面所有复盘值不值得继续。事实证明坑不少flex/bison版本、pthread链接一个比一个玄学。所以这篇笔记按拆包顺序写先看源码结构再编译复现然后精读B树与缓冲池最后讲避坑和扩展方向。2. 源码结构拆解从lex_sql到bplus_tree先看懂文件再动手改拆任何C源码包我习惯先列文件清单。这份包里的核心源码文件不多但每一对都有明确分工先看一张表文件职责对应数据库层lex_sql.cpp词法分析把SQL文本切成tokenSQL解析yacc_sql.cpp语法分析把token组装成语法树SQL解析table.cpp表结构、记录读写存储层disk_buffer_pool.cpp磁盘页与内存页的映射缓存缓冲池bplus_tree.cppB树索引的插入、删除、查找索引层bplus_tree_test.cpp针对B树的单元测试测试clog提交日志崩溃恢复用的redo机制事务日志还有COPYING许可证文件以及observer.log.20221125、observer.log.20221126两份运行日志这两份日志对排错很有参考价值后面第5章会讲怎么用。下面把这些文件逐个拆开明确它们在一套数据库系统里到底站在什么位置。2.1 一条SQL是怎么进来的lex_sql.cpp与yacc_sql.cpplex_sql.cpp是flex生成的词法扫描器。它做的事情很机械拿到用户输入的SQL字符串按规则切成token。比如select * from t1会被切成SELECT、*、FROM、IDENTIFIER(t1)这样一个token流。判断关键字、识别数字和字符串常量、跳过分隔符全部写在一套状态机规则里。你要是改过flex的.l文件就会知道这个cpp基本是生成产物手改它等于跟状态机搏斗收益很低。yacc_sql.cpp就复杂一点它是bison根据文法文件yacc_sql.y生成的语法分析器。职责是把token序列按文法归约成抽象语法树AST。MiniOB的语法树节点定义在parse_defs.h一类头文件里每个SQL语句对应一个结构体SelectStmt、InsertStmt、CreateTableStmt。执行器的入口拿到这棵树才轮到后面的模块干活。这里有个常见误用很多人拿到源码包看到两个大cpp就想直接进去改SQL语法这是最没必要的动作。这两个文件由工具生成改完也会在下次生成时被覆盖。要加语法应该改.l和.y源文件再重新跑flex和bison。包里虽然没有放.l和.y但凡是MiniOB的源码仓库都能找到对应定义。我一般只把这两个cpp当“黑匣子边界”看待SQL从字符串变成语法树我只在黑匣子入口确认token切分正常、在黑匣子出口打印AST中间的逻辑交给工具链。2.2 数据落盘的核心disk_buffer_pool.cpp与clog数据库跟普通程序最大的差别是持久化。disk_buffer_pool.cpp负责把数据文件按固定大小分页在内存里维护页的缓存。MiniOB的缓冲池逻辑和主流数据库一致读页先查缓存缓存没有就从磁盘加载页被修改后标记脏页在合适时机写回。这个模块是后面理解B树能不能高效工作的前提因为索引节点同样要经过缓冲池读进内存不存在“直接读磁盘文件”的捷径。clog文件是commit log的缩写MiniOB用它模拟WAL也就是写前日志Write-Ahead Logging。事务提交前先把日志追加到clog保证系统崩溃后能按日志重放恢复。很多初学者第一次看到这三个字母会愣一下其实把它理解成“后悔药”就行数据还没来得及落盘但日志里已经写明了“我要干的事”重启后照着做一遍数据就回来了。你去看Main函数启动顺序通常就是先打开clog再做其他初始化这个顺序本身就是数据库系统的经典设计。2.3 表与索引的分工table.cpp和bplus_tree.cpptable.cpp管的是表结构本身列的类型定义、每条记录的存储位置、扫描表的完整流程。MiniOB的表记录按固定槽位组织你可以直接看到记录文件里一条数据是怎么按列拆开存进去的这也是SQL执行器最底层的拾取逻辑。实现里会看到插入记录时如何找空闲槽位、删除记录时如何标记槽位被释放这些细节跟《数据库系统概论》里讲的堆文件组织方式是对得上的。bplus_tree.cpp则是索引一侧。创建索引等于新建一棵B树树的每个叶子节点存的是索引键值和记录位置PageNumSlotNum。table.cpp在插入记录时会检查这张表有没有索引有就同步调B树的接口写入键值查询的时候执行器先在B树里找到目标记录位置再回表读数据。理解和验证这个配合关系是读完整个源码包最重要的收益点——我当年就是没注意回表这步导致自定义的SQL在索引场景下翻车后面第5章会把这个坑单独拿出来讲。3. 编译与复现在Linux上把MiniOB跑起来的完整步骤源码到手先编译这是所有后续工作的前提。MiniOB的构建体系不算复杂但依赖项和环境差异会带来一堆小问题。下面按我拆包时实际操作的顺序来写每一步都有明确目的。3.1 环境准备依赖、CMake与编译器版本MiniOB是C项目官方默认在Linux下开发。我拆包时用的环境信息如下操作系统LinuxUbuntu 20.04和22.04都试过Rocky 9也可以编译器g 7.1以上建议g 9.xC11标准起步CMake3.10以上flex和bison如果只是编译包里生成好的lex_sql.cpp和yacc_sql.cpp这两个可以缺省但如果你想重新生成语法分析代码必须装如果你的日常开发机是Windows别硬刚Mingw我一般用vscode配Remote-SSH连Linux或者直接用WSL省去一整套环境变量的折腾。vscode配置c/c环境在Remote-SSH场景下只需要装好C/C扩展和remote插件剩下的跟本地Linux没区别。数据库这种带文件锁、信号、端口监听的程序在原生Linux里复现最省心。3.2 编译流程与产出cd miniob mkdir -p build cd build cmake .. -DCMAKE_BUILD_TYPEDebug -DDEBUGON make -j4cmake那行说明-DCMAKE_BUILD_TYPEDebug决定调试符号和优化级别Debug模式下编译器基本不做激进优化方便gdb下断点看变量-DDEBUGON是MiniOB自己控制的开日志开关打开后observer会输出更多运行细节排错强烈依赖它。make -j4是四核并行编译机器核多可以换成-j8但别在小虚拟机里硬开-j32编译直接被IO拖死。编译成功后在build/bin下面会出现observer和client两个可执行程序。observer是服务端client是客户端。你还会看到libcommon.a、libstorage.a这类静态库课程设计里你要把自己的模块加进去改的就是这些库对应的源码目录。我第一次编译时忘了配-DDEBUGON结果跑起来日志聊胜于无出问题时只能靠printf瞎猜这是个很典型的自查环节漏项。3.3 启动服务端并跑通一条SQL先启动服务端cd build ./bin/observer -f ../etc/observer.ini-f指定配置文件observer.ini里包含监听端口、日志输出级别、数据文件目录这几类关键信息。MiniOB的主程序会解析这个INI配置初始化日志系统再拉起存储组件和网络服务。启动后没有报错并且能看到监听端口说明服务端就绪。再开一个终端启动客户端cd build ./bin/clientclient连上后依次输入下面几条命令show tables; create table t1(id int, name char(32)); insert into t1 values(1, miniob); select * from t1;如果看到一行记录说明整条链路已经通了。从SQL文本进入lex_sql.cpp切tokenyacc_sql.cpp建语法树再落进table.cpp和bplus_tree.cpp最后从磁盘捞回来还给你这些步骤在你面前真实发生了一遍。这个“可复现场景”比任何文章都有说服力。跑通之后再看日志文件observer.log里会留下对应的解析和执行痕迹这就对接上了包里的observer.log.20221125和observer.log.20221126——它们是同一类运行日志时间戳不同而已。4. 核心实现精读B树索引与磁盘缓冲池的配合第2章是看结构第3章是跑通这一章要解决的是核心疑问MiniOB的索引和缓冲池到底怎么协同工作这也是面试时最容易被人追问的部分。我从三个角度拆为什么选B树、缓冲池怎么工作、测试文件怎么验证正确性。4.1 B树为什么是数据库索引的默认选择MiniOB把B树作为唯一索引结构不是因为它最简单而是因为它最实用。B树所有数据都在叶子节点非叶子节点只存索引键这样同样的页能容纳更多键树的高度自然压低。三层B树就能覆盖千万级记录每次查询最多三次磁盘IO范围查询还能顺着叶子链表顺序扫。数据库系统概论课上背的“B树适合磁盘存储”这个结论在bplus_tree.cpp里能找到对应实现节点分裂、父节点上提、叶子节点链表。对比哈希索引哈希等值查询快但没有序做不了范围扫描跳表在内存里表现好落盘后对磁盘友好的程度远不如B树。MiniOB选B树还有一个教学考虑——它的分裂合并逻辑足够复杂能让学习者写出大量调试代码也因此更能理解索引维护的代价。创建索引不花代价是假的每次插入都要走树的查找路径叶子节点满了还要分裂这些你现在看不懂跑一遍带日志的插入就全懂了。4.2 disk_buffer_pool怎么管理页的换入换出磁盘缓冲池的接口按页号工作。常见做法是维护一组帧frame每个帧对应一个磁盘页。读页时先按页号查帧表命中直接返回内存指针未命中则选一个空闲帧或淘汰一个页把目标页从磁盘读进来。MiniOB的disk_buffer_pool大致是下面这种流程// 伪代码风格按MiniOB接口整理 BufferPool pool; pool.open(t1.data); // 打开数据文件 Page* page pool.allocate_page(); // 分配一个新页 pool.fetch_page(page_num, handle);// 按页号取页 handle.unpin(); // 用完后放回给淘汰器机会 pool.close();逻辑说明allocate_page返回一个还没有对应磁盘块的页通常用于插入新记录fetch_page是读取路径unpin很关键它表示使用者不再持有这个页淘汰器后续才能把它置换出去。很多人写自己的模块时漏掉unpin导致内存里的页被锁住缓冲池逐渐填满最后新页分配失败表现是运行一段时间后插入变慢甚至卡死。淘汰策略决定缓冲池的命中率。MiniOB用简化策略但足以让你看清LRU的移动轨迹每次命中一个页就把帧移到链表头部淘汰时从尾部下手。把这两块的替换逻辑弄懂回去看MySQL的InnoDB缓冲池就有一种“这我见过”的亲切感。你还可以自己动手加一个统计接口打印缓冲池命中率观察全表扫描和走索引的查询在命中率上的差异——这个数据比任何理论描述都直观。4.3 用bplus_tree_test.cpp验证索引正确性包里那个bplus_tree_test.cpp是我最推荐先跑的文件。它直接实例化B树不经过SQL层把插入、查找、删除、范围扫描几件事在内存里做一遍。我的习惯是跑通它的单测后自己再补一个小程序模拟“插入十万条随机键再顺序读”的压力路径#include random #include bplus_tree.h int main() { std::mt19937 rng(42); BplusTree tree; tree.open(test_index.bt, 0, 0); // 插入随机键值 for (int i 0; i 100000; i) { int key rng(); RecordId rid {key % 1000, i}; tree.insert(key, rid); } // 顺序扫描校验叶子链表 tree.find_first(); int prev -1; while (tree.next()) { int cur tree.current_key(); if (cur prev) { printf(order broken: %d %d\n, cur, prev); return 1; } prev cur; } tree.close(); return 0; }参数说明open的第一个参数是索引文件名第二个和第三个参数是索引元数据MiniOB里通常传0RecordId里我塞了页号和槽位号你可以换成任意结构。顺序扫描那段就是在验证B树的叶子链表是否严格有序一旦插入逻辑有缺陷这里立刻报order broken。std::mt19937是C STL里的随机数引擎固定种子让每次跑出来的数据完全一致这个习惯建议保留——随机测试一定要固定种子否则出了bug无法复现。这段程序的编译要链接storage相关源码我一般直接在原有CMake工程里加一个可执行target省得自己拼g参数。或者你只想看结论跑包里自带的bplus_tree_test也行。但自己写一遍的好处是你能看到B树在随机键下的真实分裂次数这个数字会刷新你对索引维护代价的认知。5. 避坑指南编译报错、连接失败与数据异常的排查记录这章写几个我拆包时真实遇到的坑每条按“现象、原因、解决”三步走后面再遇到直接照着查。5.1 编译报错undefined reference to pthread_create现象make到最后link阶段报一堆pthread_*未定义observer和bplus_tree_test都生成不了。原因项目用到了std::thread或pthread相关API但链接参数里没有显式加-lpthread。CMake的旧写法有时依赖编译器默认行为而部分Linux发行版的g默认不启用线程库。解决cmake .. -DCMAKE_CXX_FLAGS-pthread提示如果工程里已经用了find_package(Threads)就看target_link_libraries里有没有Threads::Threads少了就补上。这个坑在Linux下90%的多线程C项目都可能遇到不是MiniOB特有。我当时花了一个小时才意识到是CMake缓存没刷新改完flags要重新cmake直接make不会生效。5.2 运行时故障client连不上observer现象client启动后提示连接失败但observer进程明明还在运行。原因两种最常见。第一种端口没对上observer.ini里配的是8888client默认连的却是另一个端口第二种防火墙拦截本机虚拟机里少见云服务器上很常见安全组没放行对应端口。解决先用ss -lntp确认observer实际监听的地址和端口再对照client的启动参数。自己在本地调试时把client和observer放在同一台机器上用默认端口跑真要多机测试就放行对应端口。我见过有人连不上就反复重启observer其实问题只是client打错了一个数字这种低级错误靠检查监听状态一次就能定位。5.3 数据异常插入中文后select出来是乱码现象insert into t1 values(2, 张三)执行成功select * from t1显示乱码。原因终端编码和程序解析不一致。MiniOB大多按字节存储不做字符集转换终端用GBK程序按UTF-8解析中文就会面目全非。这是数据库系统里很基础的字符集问题迷你库里没做转码坑就暴露得更明显。解决先把客户端所在的shell环境设置成UTF-8export LANGC.UTF-8再启动client。如果是做课设建议表结构里字符串字段先用ASCII场景验证功能逻辑中文最后再测。我后来养成的习惯是所有涉及字符串的测试用例都单独写一个文件避免手动输入编码混乱。5.4 代码生成踩坑改了yacc文件后出现shift/reduce conflict现象重新生成yacc_sql.cpp时bison报警说检测到多个shift/reduce冲突有的SQL语句解析行为变得不符合预期。原因新增语法规则没有考虑优先级比如表达式混合和*时没有声明%left、%right。bison默认按移位优先处理冲突结果就是语法树建得跟心里想的不一样。解决包里生成的cpp文件一般情况下别动真要扩展语法在.y文件里把运算符优先级声明清楚再重新生成。我见过有人直接在那个几千行的yacc_sql.cpp里硬插代码两天后自己都不知道哪里是手改哪里是生成——别学这是血泪经验。改文法的正确姿势是先在小规模样例上验证token流再调优先级最后全量回归。5.5 调试翻车gdb下断点后变量全被优化了现象gdb下断点后print一个局部变量gdb显示值不可用或者整个变量消失。原因编译时默认开了O2优化局部变量被寄存器替换或直接折叠调试器拿不到可靠信息。解决编译用Debug模式也就是第3章那条-DCMAKE_BUILD_TYPEDebug。另外要注意MiniOB默认把网络请求处理放在工作线程断点最好打在handle方法内部比打在入口函数更稳否则多线程场景下断点命中位置不稳定会误导判断。6. 进阶玩法把MiniOB变成你的课程设计项目MiniOB本身就是个很好的课设底座。我见过三种最常见的扩展方向按工作量从低到高排。第一加字段类型。比如补一个DATE类型路径是语法层允许识别DATE关键字类型系统里注册新类型存储层确定字节宽度显示层按格式输出。改完用create table d(day date);、insert、select三步验证。这是练手最舒服的一条线涉及链路最短适合C入门不久的同学。第二加聚合函数。从count开始执行器里在建表或投影时识别aggregation节点分组计算的逻辑写在select执行路径里。MiniOB的select实现相对简单你能在不太长的代码里看清楚元组是怎么流动的加完之后对SQL执行的理解会上一个台阶。第三实现等值连接join。这个工作量大但面试讲项目时最加分。MiniOB的select暂支持单表查询你需要在语法树上增加join子节点执行时做嵌套循环连接。做之前把第4章的索引路径吃透否则性能很难看——真正的数据库系统里join没有索引支撑就是灾难这个认知可以在这里亲身体会。验证方法养成三层习惯改一次跑三层验证。第一层跑bplus_tree_test.cpp确认索引没被你改坏第二层用client跑一遍建表、插入、查询的回归SQL第三层留一份关键查询语句的预期输出改完对拍。我自己的翻车教训是有一回只改了table.cpp的插入路径忘了几张有索引的表要同步维护B树结果走索引的select查不到刚插入的数据全表扫描却能查到——数据不一致真的是最隐蔽、最要命的坑。从那以后我每次改完存储层都强制把bplus_tree_test.cpp和走索引的select各跑一遍双验证不跳过。希望帮到你。本文还有配套的精品资源点击获取