简介基于C的MiniOB数据库系统压缩包提供完整源码MiniOB由OceanBase与华中科技大学联合开发是面向在校学生的数据库内核入门实践工具。项目围绕存储管理、查询优化、事务处理等模块设计并简化并发等复杂环节适合初学者通过源码理解数据库内核的工作机制同时支持SELECT、INSERT、UPDATE、DELETE等基础SQL语句解析执行以及INI配置解析、文件读写等实用功能。资源共360个文件核心代码以C为主含121个h头文件、107个cpp源文件并辅以58个png示意图、19个md说明文档及多组test/result测试用例等包体大小约3.18MB目录结构清晰便于按模块对照学习。目前已有133人浏览学习对希望动手实践数据库内核的开发者而言是一份精简且可编译、可扩展的入门参考。1. C 写一个数据库系统先从这个 MiniOB 源码包开始用 C 写一个数据库系统听起来像大厂资深工程师才能碰的事。但 MiniOB 这个项目把门槛拉到了普通学生面前它是 OceanBase 团队开源的教学数据库内核用 C 实现代码规模控制在万行级目标是让你亲手把 SQL 解析、缓冲池、B 树索引、执行器、事务这些教科书概念落成能编译、能调试、能跑真实 SQL 的系统。这个 zip 解决的是三类诉求数据库课程设计拿不出像样的交付、学完数据库系统概论依然说不清一条 select 到底怎么执行、面试被追问索引原理时只能背八股。适合有一学期 C 基础、愿意花一个学期啃代码的人。别指望解压后双击能跑但按下面这套路径走两天内能见到第一个属于自己的数据库进程。2. 先拆代码地图MiniOB 的核心模块与一条 SQL 的旅程拿到一个数据库系统的源码最蠢的打开方式就是直接跑构建脚本。你连它是行存还是列存、一条 SQL 经过几个处理阶段都不知道编译报错也没法定位。我的习惯是花 30 分钟把目录翻一遍在脑子里建一张“代码地图”再动手。2.1 核心模块对照教科书每个目录对应数据库系统概论的一个章节MiniOB 的源码结构非常“教学”模块划分基本照着经典数据库教材的章节走。第一块是 SQL 解析词法分析器和语法分析器负责把字符串变成语法树这对应教科书里“关系系统的查询处理”前半段。第二块是执行器负责把语法树变成可执行的算子链对应查询执行那段。第三块是存储管理包括缓冲池和记录管理对应“数据库物理存储”章节。第四块是 B 树索引对应索引那一章。第五块是事务与并发控制对应事务章节。这五块在源码里就是几个目录的事。数据库系统概论第六版里分五章讲的东西被 MiniOB 压缩在一个工程里而且每块之间的调用关系是显式的。你不一定要每个目录都读透但必须知道它们在哪。拿到包后第一件事就是在src/observer下面找storage、sql、session这几个目录它们分别是存储引擎、SQL 处理链、会话管理的地盘。还有一个容易忽略的目录是事件调度相关的代码比如event。MiniOB 把一次请求拆成多个处理节点这种设计是工业数据库常见的做法好处是每一段的职责单一坏处是刚看源码的人容易绕晕。我的建议是先别管调度框架本身把注意力放在“一条 SQL 从进入到返回经历了哪些处理节点”上这一步走通整个项目就通了一半。2.2 一条 insert 从客户端到磁盘经历了什么我们在客户端敲下一条insert into t values (1, a)在 MiniOB 里的旅程大致是这样的。客户端把字节流发到服务端网络层把字节流还原成 SQL 字符串然后交给会话层。会话层把这个字符串丢给解析器解析器用词法分析拆 token、用语法分析按文法规则组装成一棵抽象语法树。紧接着语义分析阶段把语法树里的表名、字段名和实际表结构绑定起来字段类型不匹配、表不存在这类错误在这一层被拦下。绑定完成之后进入优化和执行阶段。优化器判断这条语句能不能走索引、条件的下推方式执行器按“火山模型”一层层拉取数据。到了存储层缓冲池先看目标页在不在内存不在就把磁盘页加载进来然后在页里找空闲槽位写入字段值把页标记为脏页最终返回成功。整个路径从内存到文件系统再到磁盘每一层都在前一层的基础上加了一点东西。这就是为什么考试背“查询处理五步骤”总是记不住。你背的时候没有一条真实的 SQL 在脑子里走完全程自然记不牢。MiniOB 的价值就在于这条链路的每一段你都能用调试器走一遍断点一打教科书上的流程图就变成真实的调用栈了。2.3 别急着构建先用 grep 把主链路捞出来很多同学喜欢在 vscode 里同时打开整个源码包从第一个文件开始读读十行就放弃。正确姿势是先把处理链路的入口函数名找出来再用调试器定位。unzip -q MiniOB.zip -d ~/miniob cd ~/miniob # 主链路通常由事件调度框架串起来stage 后缀的函数名就是断点位置 grep -rn derive\|execute\|parse src/observer/event --include*.cpp | head -40这段命令的逻辑是.zip解压后进入根目录然后直接搜事件调度目录里带parse、execute等关键词的函数把这些函数名当作整个主链路的“路标”。参数说明-rn表示递归搜索并显示行号--include*.cpp限定只看 C 源文件head -40只取前 40 行防止输出太长。搜完以后你通常能看到类似parse_stage、execute_stage这样的处理节点名每个名字就是一个可以下断点的位置。这一步花不了几分钟但价值很高。它把“读源码”这个模糊任务变成了“沿着主链路读 5 个关键函数”的具体任务。数据库系统的黑匣子就是从这些带stage后缀的函数逐步打开的。3. 从 zip 到能跑的数据库构建、启动与第一次建表代码地图有了现在开始让这个系统真正跑起来。MiniOB 的构建环境要求不复杂但有几个前置条件必须满足否则编译过程会非常折磨。3.1 构建前置别在 Windows 上硬刚用 Linux 或 WSLMiniOB 的构建依赖 g、make、cmake、flex 和 bison。其中 flex 和 bison 是解析器生成工具用来把语法描述文件转换成 C 代码这两个工具在 Windows 原生环境下的安装体验非常差。如果你只有 Windows 机器常见做法是装 WSL然后在 WSL 里操作。vscode 配置 C/C 环境时注意一点C/C 插件的编译器路径要指向 WSL 里的 g而不是 Windows 的编译器否则智能提示和实际编译环境不一致头文件路径会一片飘红。sudo apt update sudo apt install -y build-essential cmake flex bison flex --version bison --version第一行更新软件源第二行一次性安装编译工具链和解析器生成器第三行验证版本。build-essential包含 g、gcc 和 makecmake负责构建系统生成。注意 flex 和 bison 这两个包缺一不可漏掉的话编译到解析器部分会直接报找不到命令。3.2 用 build.sh 编译 debug 版并启动 observer源码包根目录通常自带构建脚本。常见做法是用bash build.sh debug编一个调试版本因为 debug 版默认带符号表、关闭了部分优化还开了更严格的断言检查对后续用 gdb 跟踪执行路径几乎必不可少。release 版虽然性能好但调试信息少定位问题很难受。cd ~/miniob bash build.sh debug ls build/bin cd build/bin ./observer -f ../../etc/observer.ini sleep 1 tail -f ../../log/observer.logls build/bin是为了确认产物生成成功里面一般能看到一个服务端程序observer和一个客户端程序client。启动服务端时-f指定配置文件路径配置文件里关键是端口、日志级别、缓冲池大小这些参数。让进程在后台运行sleep 1等一下再查看日志tail -f持续跟踪日志输出确认进程正常起来没有报错。如果启动失败第一时间看日志而不是猜。端口占用、配置路径错误、数据目录权限不足都会在日志里留下线索。这一步踩的坑我会在第 5 章专门展开。3.3 第一次建表create、insert、select 对应你刚看过的模块服务端起来以后另开一个终端连接客户端。这个客户端是一个命令行交互程序连上以后可以直接敲 SQL和操作 MySQL 的感觉很像。cd ~/miniob/build/bin ./client -p 6789 # 在客户端交互界面逐行执行 # create table t (id int, name char(20), score float); # insert into t values (1, Alice, 90.5); # select * from t;-p 6789指定服务端端口默认配置下服务端监听 6789。建表语句里char(20)是定长字段内部按固定 20 字节存储不是按实际字符长度动态分配float固定 4 字节。这条建表语句执行后MiniOB 会在数据目录创建一个对应表结构的文件字段元信息被持久化。第一次跑通这三条 SQL意味着你刚才在代码地图里看到的所有模块——解析器、执行器、存储层、页管理——已经以一个完整系统的形式运转起来了。接下来再往深处走才是真正拉开差距的部分。3.4 想改 SQL 语法parser 重编译的正确姿势跑到这一步很多人的第一个想法是“给 MiniOB 加一条自己的 SQL 语法”。这个想法非常好但会提前踩到一个经典坑。MiniOB 的解析器由.y文件描述语法规则bison 根据它生成对应的 C 文件。问题是如果你只改了.y文件然后直接重新构建增量构建很可能不认账生成文件不被重新编译你新增的语法毫无反应。cd ~/miniob # 修改 parser 目录下的 *.y 语法文件之后 # 手动清理生成的 parser 产物避免 make 增量构建不重建 rm -f src/observer/sql/parser/*tab.c src/observer/sql/parser/*tab.h bash build.sh debugrm -f删除 bison 生成的 tab 文件后面的bash build.sh debug全量重新生成。这里的关键理解是.y文件是语法规则的“源代码”tab 文件是生成物Make 对生成依赖的追踪有时不够可靠。手动删除生成物强制构建系统重新生成一遍是解析器开发里的标准操作。这一步做完MiniOB 就不再是一个只能跑现成 SQL 的玩具了它变成了你可以“改方言”的数据库系统。语法加在哪、执行逻辑怎么写这些问题会带着你进入下一章的核心实现。4. 存储与索引的最小实现页、B 树和事务的一次看懂现在到了整个项目最硬的部分。跑通 SQL 只是热身真正决定你能否自信地说“我懂数据库内核”的是存储与索引这层。这章我会用教学简版的代码示意把关键结构讲清楚重点在理解设计逻辑不是逐行背源码。4.1 记录怎么放在页上定长槽位与偏移数据库存储的最小单位是页MiniOB 的默认页大小通常是 4096 字节。一张表的所有记录被切分到若干页里每页内部有页头和槽位区。教学版常见的设计是定长记录插入记录时计算好每条记录占多少字节然后顺序放进页里。// 教学简版定长记录页头与槽位示意 struct PageHeader { int32_t page_no; // 物理页号对应文件内的偏移索引 int32_t slot_cnt; // 页内已有记录槽个数 int32_t free_off; // 空闲空间起点偏移 }; struct TableSlot { int32_t offset; // 记录在页内的偏移 int32_t length; // 记录长度 };page_no是这个页在表文件里的编号MiniOB 找页靠它。slot_cnt记录页里已经存了几条数据free_off指向下一个空闲写入位置。TableSlot让记录可以做到逻辑连续、物理不一定连续删除一条记录时不需要搬动后面的数据直接把槽位标记为可用即可。为什么教学版偏向定长记录因为变长记录要处理碎片回收、记录搬迁、槽位复用复杂度会指数上升。你建表时写的char(20)、int、float都是定长类型行大小一次算定页里能放多少条记录在表结构创建时就知道。如果你想验证这一点可以建一张字段很多的表插入满一页的数据后观察文件大小变化能直观体会页与槽的概念。4.2 Buffer Pool数据库为什么不直接读文件如果每次读取都走操作系统的文件接口数据库的性能会有巨大损耗。核心原因有两个一是系统调用和磁盘 I/O 的固定开销高二是数据库需要自己控制页的淘汰策略而不是依赖操作系统的 LRU。所以 MiniOB 内部维护了一个缓冲池所有页的读写都经过它。// 教学简版缓冲池里的帧结构 class Frame { public: char data[4096]; // 页的内容 int32_t pin_count; // 正在使用这个页的线程数 bool dirty; // 是否被修改过脏页需要回写 int64_t last_used; // 最近访问时间戳LRU 淘汰用 };data是页内容的内存镜像文件里读到什么就放这里。pin_count告诉缓冲池这块页“有人正在用”正在用的帧不能被淘汰。dirty标志位的存在意味着页被修改后不会立刻写盘而是等淘汰时或安全检查点再回写这就是经典的延迟写策略。last_used用于 LRU 淘汰每次访问刷新这个时间戳淘汰时找最早的。理解这几个字段等于理解了所有数据库缓冲池的共同骨架。MiniOB 在这种设计上加了自己的简化替换策略直接不处理极其精细的并发竞争但这不妨碍你理解核心机制。真正重要的是从这里能很自然地推导出数据库“为什么需要 checkpoint”——因为脏页越积越多系统宕机后恢复的代价就越大。这个认知比死记硬背定义要值钱得多。4.3 B 树索引找到记录位置再回表索引是数据库系统概论里最容易被抽象描述搞晕的部分。MiniOB 的 B 树实现把“索引到底长什么样”还原成了一段可读的代码从根节点出发按键值比较逐层下沉到达叶子节点后叶子节点保存的是键值和一条记录的物理位置——行号。拿到这个行号后再去对应的页里读取完整记录这个动作叫“回表”。插入时的分裂逻辑是 B 树的难点我用一个表把三种场景理清插入时空间情况动作需要修改的页叶子未满直接插入并保持键序叶子页叶子满、父节点未满叶子分裂新键插入父节点叶子页 父页叶子满、父节点也满叶子分裂、父节点分裂逐层向上传播叶子页 父页 祖父页甚至根这个表值得反复看。大多数人在 B 树上翻车不是因为不懂“分裂”这个概念而是没搞清分裂后父节点应该插什么键、根节点什么时候长高。MiniOB 的调试器可以帮上大忙在插入代码路径上下断点逐页打印节点内容你能亲眼看到一次插入如何把某个页挤爆然后触发新节点诞生。要特别记住“回表”这个动作。MiniOB 的索引结构里叶子节点存的是记录位置不是字段内容本身。这意味着走索引不一定总比全表扫描快——如果需要读取的行数很多回表的随机 I/O 代价可能超过顺序扫描。这个认知直接引出第 5 章的一个高频坑小表上建了索引查询却不用。4.4 事务与 MVCC教学版是怎么简化并发控制的事务这块MiniOB 做的是简化版的多版本并发控制。核心思想是每个事务获得一个递增的事务 ID记录的每一次修改都记录下事务 ID读取数据时通过比较事务 ID 判断这条记录对当前事务是否可见。这样读写互不阻塞读事务看到的是一个一致性的快照。实际教学版的实现比完整 MVCC 简单得多没有完整的 redo/undo 日志体系没有事务隔离级别的完整体面但骨架是真实的——begin 开始一个事务commit 让修改生效rollback 回滚。用调试器跑一下begin; insert; rollback; select;这个序列比看十遍理论都清楚。MiniOB 在这里的定位不是“把 InnoDB 重写一遍”而是让你能用第一行代码理解事务的本质。面试时被问“MVCC 怎么实现”你能从读视图、事务 ID、可见性判断这三个词讲出逻辑这个项目的教学使命就算完成了。5. MiniOB 避坑指南从编译失败到查询不报错但结果不对跑通系统之后真正的成长从踩坑开始。我把自己和其他人在这类教学数据库项目上踩过的高频坑整理成五条每条按现象、原因、解决的顺序写。这些坑的具体形态可能因代码版本有别但底层逻辑是通用的。5.1 bison/flex 生成 parser 时编译不过现象构建过程在解析器部分报错报错信息指向某段带yacc、lex字样的文件或者报一堆“本次生成的代码里缺头文件”。原因flex 和 bison 是解析器生成工具生成出来的 C 代码对工具版本有一定敏感性。系统里这两个工具没装全或者版本组合差异较大生成代码的函数签名与项目原有头文件对不上编译就断了。解决先确认工具确实装了flex --version bison --version都要能输出。如果版本差异太大用 apt 安装发行版自带的版本优先匹配系统源里的稳定版不要手动从源码编译最新版。最土但最有效的办法是彻底删除构建目录从零重新执行构建脚本排除增量编译搅局。5.2 改了语法文件却不生效现象修改.y文件新增了一条语法规则重新构建后执行新语法服务端还是报 “unknown syntax” 或类似错误。原因增量构建系统对.y文件的依赖追踪不可靠。bison 生成的文件是构建产物Make 判断它“没有变化”时就不会重新编译你改的规则根本没进入二进制。解决改动.y文件后手动删除生成的 tab 文件和对应的对象文件再完整重新构建。不要靠touch碰运气直接删干净最省心。这个坑执行十次能踩八次遇到“我明明改了为什么不生效”的头疼时刻先想这一步。5.3 client 连不上 observer现象启动 client 后提示Connection refused或者连接超时无论如何都进不去交互界面。原因最常见的两种情况——observer 没有正常启动或者端口不是你以为的那个。如果同时启动过多个 observer 实例或者上一次的进程残留没杀干净新进程可能没有绑定端口也可能绑到了别的端口。解决不要盲猜先查客观证据。ps aux | grep observer看进程在不在netstat -tlnp | grep 6789看端口被哪个进程监听然后看一眼日志——日志永远比错误提示更接近真相。如果端口确实被占杀掉旧进程重启如果进程没起来回第 3 章的启动步骤查配置路径。5.4 中文与超长字符串插入后乱码或截断现象插入中文字符串后查询出来是乱码或者只写入了一部分内容。原因char(n)在 MiniOB 这类定长存储里按字节计数的概率很大而 UTF-8 编码下一个中文占 3 字节。你建表时写char(10)想存 10 个汉字实际需要 30 字节的空间字段长度直接不够。另一种情况是终端和客户端的字符编码不一致问题不在存储层而在交互层。解决前期调试尽量用纯 ASCII 数据避免把编码问题混入存储问题。如果确实需要中文业务建表时把字段宽度按最大字节数预留而不是按字符数预留。区分“截断”和“乱码”是两类问题截断是存储层长度不够乱码是编码转换问题排查方向完全不同。5.5 建了索引但查询还是全表扫现象表上建了索引执行带等值条件的选择查询性能没有提升日志显示还是全表扫描。原因优化器选择索引是有条件的。条件字段与索引键完全匹配才能走 IndexScan条件表达式里如果套了函数、做了运算索引就失效还有一个反直觉的情况——表数据很少时全表扫描的顺序读可能比索引回表更快优化器选了它认为更优的策略。解决先看执行计划或执行日志确认到底有没有走索引然后把索引建在查询最频繁的等值条件字段上不要给选择性很低的字段建索引。想用这个现象深化理解是极好的机会把一条走全表扫的查询用调试器跟一遍执行器的算子调用顺序你就能直观看到 MiniOB 何时构造 TableScan 算子、何时构造 IndexScan 算子这比背优化器规则管用得多。6. 把 MiniOB 变成简历上能讲的项目三个往上走的实验系统跑通、坑也踩过一轮之后MiniOB 距离“你自己的项目”还差一次主动改造。我推荐三个进阶实验难度递增全部做完你对数据库的理解会比现在高出一个量级。实验一给 MiniOB 加一个count(*)聚合函数。这个实验动到两个层解析器里的函数语法、执行器里的聚合算子。做完以后能讲清楚一个非常关键的问题——聚合是在哪一层发生的以及它为什么必须做成“先拉数据、再聚合”的模式。这个问题的答案和教科书上“聚合操作在查询执行阶段”这句话之间的距离就是动手与不动的区别。实验二用 gdb 从客户端到存储层跟完一条 select。断点打在解析完成后、执行开始前的那个处理节点上然后一步步看算子调用链。花一个周末画一张执行树出来。从那之后面试被问“一条 select 怎么执行”你可以讲到第二层以上带着调用栈讲而不是带着八股讲。实验三给缓冲池加一个脏页率统计接口。在页标记为脏和回写的地方各放一个计数器分别跑顺序插入和随机插入你会看到脏页率的巨大差异。这个实验不做你可能永远理解不了 checkpoint 为什么存在于每一个真实数据库里。我当年啃完这三个方向之后最大的教训是数据库这个领域没有玄学任何看似复杂的概念最终都会落到某一行代码上。你踩过的坑不是浪费时间是比读十遍书都牢的 debug 记忆。把这套路径走完这台 MiniOB 就不只是网上下载的源码包而是你简历上有技术细节可讲、有真实问题可聊的项目。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?