
简介基于C开发的MiniOB数据库系统是OceanBase与华中科技大学联合推出的数据库教学实践项目面向零基础学生与数据库内核初学者旨在帮助读者快速理解数据库各模块的功能与关联并学会设计高效SQL。压缩包共363个文件、约3.17MB核心由119个C头文件和107个C源文件构成完整呈现SQL词法语法解析、B树索引、磁盘缓冲池、记录管理、表管理等关键模块配套19个Markdown文档用于解释设计思路和使用方法58张PNG图片可辅助查看模块架构与运行流程另有test/result、txt、sh等文件支持测试与脚本执行。包里还附带日志与配置样例便于读者结合运行结果排查问题、加深理解。目前已有74人学习下载适合作为数据库内核入门实战参考读者可依据源码、文档与测试线索系统梳理索引、事务、日志等核心机制。1. MiniOB 是什么一个能跑起来的教学数据库系统拆它的源码值在哪如果你拿到一个名为“基于C的MiniOB数据库系统”的源码压缩包先别把它当成又一个交完就忘的课程设计。MiniOB 是一个面向数据库系统教学的最小实现它真的会启动一个服务端进程监听连接、接收 SQL 文本、解析语句、维护表和索引再把查询结果返回给客户端。和 MySQL 的区别不在“能不能用”而在代码规模——主体工程往往只有几万行 C一个人完全能从头读到尾也能改得动。对正在学《数据库系统概论》的学生、想补数据库底层知识的 C 开发者它是最适合拿来拆解的一手材料。本文会按一条可复现的路径走拿到 zip 后怎么编译、怎么跑通客户端、SQL 能用到什么程度、代码链路怎么展开、哪些坑最常见最后用 GDB 把它变成自己的调试沙盒。2. 把 C 工程变成可执行文件MiniOB 编译步骤与依赖排错拿到源码包第一件事不是读代码而是先把它编译出可执行文件。MiniOB 这种教学工程通常用 CMake 组织构建配合 flex/bison 生成解析器代码。很多人卡在第一步就因为漏装了生成工具或者跑错了构建目录。这一章按我常用的顺序来先准备依赖再确定构建方式最后启动服务端并用客户端验证连接。2.1 编译前的依赖准备C 构建链与 flex/bisonMiniOB 是纯 C 工程代码里用到了 C11/14 的特性所以编译器至少要支持 C11。在 Ubuntu/Debian 系统上最省事的做法是一次性把构建链和解析器生成工具装齐sudo apt update sudo apt install -y build-essential cmake flex bison libjsoncpp-devbuild-essential提供 g、gcc、make、ld 等基础构建工具是 C/C 构建的最低配置。cmake负责读取 CMakeLists.txt 生成 Makefile 或 Ninja 构建文件。flex和bison用来生成词法分析器 scanner 和语法分析器 parser。MiniOB 的 SQL 解析器就是从.l和.y文件生成 C 源码的缺了它们CMake 在预处理阶段就会报错。libjsoncpp-dev是 MiniOB 配置解析常用的 JSON 库如果构建时报找不到 json/json.h说明依赖没装齐。如果你平时用的是 VS Code记得在c_cpp_properties.json里把编译器路径指到/usr/bin/g并把源码目录加入compileCommands或includePath。这个工程不建议在 Windows 原生环境直接编路径、socket、共享内存这类实现都是按 POSIX 写的Windows 上得用 WSL 或 Linux 虚拟机。常见做法是用 WSL2 装好依赖再把 zip 解压到 Linux 文件系统里编译和调试都顺畅。2.2 从源码到可执行文件build.sh 还是直接 cmake解压后的目录里通常会有build.sh和CMakeLists.txt。许多课程镜像会自带build.sh它封装了 debug/release 模式切换和产出目录管理。如果你第一次编译建议直接用脚本cd miniob # 换成你解压出的目录名 ls -la bash build.sh debugdebug参数对应-DCMAKE_BUILD_TYPEDebug会带上-g调试信息后续用 GDB 调试时必须用它。如果脚本没执行权限先chmod x build.sh。脚本执行完可执行文件一般在build/bin或output/bin下具体看脚本里的OBJ_DIR定义。如果你手里没有build.sh或者想自己折腾构建过程可以直接用 cmakemkdir -p build cd build cmake -DCMAKE_BUILD_TYPEDebug .. make -j$(nproc)-DCMAKE_BUILD_TYPEDebug指定调试构建等会儿bt能看到带符号的调用栈。-j$(nproc)自动用满 CPU 核数。如果你在虚拟机里内存只有 2GB建议-j2甚至-j1否则容易 OOM。如果 CMake 配置阶段弹出缺少flex或bison的错误回到 2.1 补齐再重新跑。注意 CMake 失败后会有CMakeCache.txt缓存改完依赖最好把build目录删掉重建避免缓存干扰。编译成功的标志是产出了两个关键文件服务端observer和客户端obclient。如果不知道产物去了哪用 find 定位find . -name observer -type f find . -name obclient -type f常见产物路径是build/bin/observer和build/bin/obclient。如果能找到说明编译链路基本通了。2.3 启动 observer 并连接 obclient验证最小可用系统编译通过只是第一步能连上数据库才算“跑起来”。MiniOB 的进程模型很简单先启动服务端observer再启动客户端obclient客户端通过本地 socket 连上去。cd /path/to/miniob # 务必回到项目根目录 ./build/bin/observer -f etc/observer.ini-f指定配置文件。etc/observer.ini里通常配置监听的 socket 文件路径、日志文件名、缓冲区大小等。这里有个高发坑必须在项目根目录运行 observer否则它相对路径找不到observer.ini或者找不到配置里的 socket 路径启动就会报错。如果你发现“启动后立刻退出”先看终端输出的是不是Failed to open ... observer.ini。服务端起来后另开一个终端./build/bin/obclient客户端启动后不会立刻退出而是进入交互式提示符。这时别急着敲复杂 SQL先验证通信链路create table test_table(id int);如果返回SUCCESS说明服务端已经成功完成“接收 SQL → 解析 → 建表 → 写元数据”的完整流程。如果服务端终端有日志输出那就更确认了。如果提示语法错误或连接失败先把 observer 进程停掉重新用strace -o trace.log -f ./build/bin/observer -f etc/observer.ini跟踪一下看 socket 初始化到哪里断的。这一步做好后续 SQL 操作才有意义。3. 在 MiniOB 里跑 SQL语法边界、客户端操作与一张表的完整生命周期MiniOB 不是 MySQL它的目标是教学演示所以 SQL 支持范围被刻意缩到很小。很多人在这个阶段容易犯“按 MySQL 习惯敲 SQL结果报错”的毛病。这章先讲清楚它支持什么、不支持什么再带你完整建一张表、插入数据、查询数据最后解释几个类型上的经典坑。3.1 MiniOB 的 SQL 子集支持哪些语句大概率不支持哪些从历年常见 MiniOB 版本来看核心支持范围大致是建表create table 表名 (列名 类型, ...)插入insert into 表名 values (值, ...)查询select * from 表名或select 列1, 列2 from 表名带条件删除delete from 表名 where 列名 值更新update 表名 set 列名 值 where 条件索引create index 索引名 on 表名 (列名)简单表达式与比较where id 3、and、or这类基本条件它大概率不支持多表 join很多二次开发版本会加原版默认没有子查询、group by、having事务相关语句begin/commit/rollback复杂的表达式函数concat、now()这类内置函数外键、唯一约束、自增列你不需要死记这份清单最直接的办法是把 README 里的支持语法当基准再在 obclient 里敲一句试一句。MiniOB 对不支持的 SQL 通常反馈语法错误或 “unsupported” 之类提示不会像 MySQL 那样给你一个错误码。3.2 用 obclient 跑通建表、插入和查询的完整链路打开 obclient 后按顺序执行下面这组 SQL这就是一张表从诞生到被查询的完整生命周期create table student ( id int, name char(16), score float ); insert into student values (1, zhang, 88.5); insert into student values (2, li, 92.0); insert into student values (3, wang, 73.2); select * from student; select id, name from student where score 90.0;create table的列类型支持int、char(n)、float等基础类型。char(16)意思是这个字段定长 16 字节。insert的 values 必须和建表时的列顺序一致数量和类型都要对上。MiniOB 通常没有自动类型转换88插入 float 列可能行但如果字符串里混入了非数字会直接报错。select *返回全部列select id, name返回指定列。where score 90.0是条件过滤注意这里等值比较用单个不是。如果你输入select * from student;后能看到三行记录说明从解析到存储再到输出整条链路都通了。这条链路也正是第三章代码剖析的入口。3.3 类型与格式细节char 填充、float 精度和 NULL 的坑教学实现里最容易让新手翻车的是类型细节尤其这三个第一char(n)是定长字段。插入zhang时内部会把它补成 16 字节后面用 0 或空格填充。所以你在客户端看查询结果时name字段后面可能会多出一串空格。这不是 bug是定长存储的必然结果。想验证这一点插入一条短字符串再 select观察输出对齐就能看出来。第二float不要用等值比较。where score 88.5很可能查不到因为浮点数的二进制表示和十进制字面量之间会有误差。MiniOB 一般把值按 float 类型存成 4 字节运算和比较时转成 double精度损失会放大这个差异。正确写法是范围比较where score 88.4 and score 88.6。第三MiniOB 对NULL的处理很“原始”。大部分教学版本不允许某列为空插入时必须给全所有列的值。如果你尝试insert into student (id) values (1)大概率会收到“字段数量不匹配”之类的错误。这和学习时的直觉不同——数据库系统概论里讲过NULL是一种特殊值但 MiniOB 用“每列必须有值”换来了实现上的简单。这些细节在代码层面都能解释定长 char 对应存储引擎里固定大小的 Record schemafloat 的等值比较在执行器里直接做字节比较就会有问题NULL 支持意味着 Record 里要额外维护一个 null bitmap。MiniOB 去掉这些正是为了把核心链路露出来让你看。4. 从 SQL 到存储MiniOB 源码链路、解析器改造点和第一个调试钩子能跑通 SQL 之后你自然会想知道一条 SQL 在源码里是怎么被吃掉的这章带你把 zip 里的目录结构当作战地图定位解析器、执行器和存储层的位置。你在 MiniOB 里做的大部分二次开发改造点都落在这三层里。4.1 源码地图先找到解析器和执行器的位置解开源码包后不要一上来就看所有文件。按核心目录找起cd miniob find . -maxdepth 2 -type d | sort通常会看到一个src目录里面包含observer、common、include等。其中src/observer是程序主体再往下会出现sql/parser、sql/executor、storage这样的子目录。你可以用 grep 验证一下解析器的存在grep -rn create_table src/observer/sql/parser | head -20出来的结果里如果有.y、.l或.cpp说明语法定义在这些文件里。如果 grep 不到换insert、select这些关键词再试。这一步是“源码二次开发”的定点技能比记死路径管用。展开一个典型的目录树src/observer/ ├── main.cpp ├── sql/ │ ├── parser/ │ │ ├── lex.l │ │ └── parser.y │ └── executor/ │ ├── create_table_executor.cpp │ ├── insert_executor.cpp │ ├── select_executor.cpp │ └── ... └── storage/ ├── table/ ├── index/ └── buffer_pool/不同二次开发版本的文件名会有出入但职责划分很稳定parser负责把 SQL 文本变成结构化语法节点executor负责遍历语法节点并触发存储层操作storage负责文件、页、记录、索引。4.2 链路拆解从 socket 缓冲区到表记录MiniOB 的完整链路大致是客户端把 SQL 写到 socket → observer 主循环读到字节流放到输入缓冲 → 触发词法扫描flex和语法分析bison生成语法树 → 根据语句类型分发给对应 executor → executor 调用 table/index 的读写接口 → 存储引擎把记录写进页或从页读出来 → 结果拼成响应写回 socket。这条链路很像《数据库系统概论》里讲的“查询处理”流程只不过 MiniOB 砍掉了查询优化器的大部分功能。它的 executor 几乎是一对一翻译语法create_table就去找 Table 模块建表insert就去找 Record 模块追加记录。这种“直译式”实现对你学原理反而是好事——你不需要理解复杂的关系代数换算就能看到“一个 SQL 对应一个函数调用”。想要验证这条链路最快的办法是在执行器入口加一个日志钩子。比如你 grep 到了select_executor.cpp就在里面找最外层的执行函数在进函数第一行插入std::cerr [TRACE] execute select, sql sql_text std::endl;sql_text这个名字可能不同你需要看这个函数参数里有没有接收 SQL 字符串的变量。如果没有就在更外层的 dispatch 函数里打印。编译后再跑一次select * from student;服务端终端就会多出一行 trace。这一行日志就是你的“黑匣子数据”能确定解析后的执行入口到底被调起来没有。4.3 想改一个功能如何定位要改的三个文件假设你想给 MiniOB 加一条drop table语句。先别急着写实现按三层分别定位词法层在lex.l里增加drop关键字对应的 token。语法层在parser.y里增加一条语法规则比如DROP TABLE IDENTIFIER并生成对应的语法树节点。执行层在executor目录下新增或扩展一个处理器调用存储层把表文件和元数据文件删掉。具体写法需要参考已有的create_table是怎么写的。一般做法是复制一份create_table_executor.cpp把建表逻辑换成删表逻辑再在解析器的分发函数里加一个case kDropTable。这个过程不用猜结构直接grep -rn create_table src/observer/sql/parser和grep -rn CreateTable src/observer/sql/executor把相关文件打开照着抄。4.4 一个可落地的练习从零加一条 desc 命令我建议新手不要一上来就改聚合函数或 join而是从desc 表名这种“读元数据”的命令入手。步骤是在词法文件lex.l中把desc映射到一个 token比如DESC_TOKEN。在语法文件parser.y中新增规则statement: DESC_TOKEN IDENTIFIER并生成一个描述该语句的节点。在分发函数里找到处理语句类型的 switch/case加一个分支调用表元数据读取接口。在 executor 目录下写一个desc_executor.cpp从TableManager之类的地方取表的列定义拼成文本响应返回给客户端。重新跑 build.sh在客户端执行desc student;看能否输出列名和类型。这个练习的价值在于它逼你走完“词法→语法→分发→执行→存储”的全部环节但不需要你理解复杂算法。做通之后MiniOB 的源码在你眼里就不再是一个黑盒而是一条可以随时插入代码的流水线。5. MiniOB 避坑手册5 个必看的编译运行问题与排查清单这一章是给“按前面步骤做但翻车”的人准备的。我见过太多人卡在某一步之后就放弃了其实大多是环境问题或路径问题。下面这 5 条是我觉得出现频率最高、也最有代表性的坑每条都按“现象 → 原因 → 解决”写清楚。5.1 编译时报 “flex not found” 或 bison 版本过旧现象cmake 配置阶段直接失败提示找不到flex或bison或者报parser.y语法文件无法生成。原因词法/语法分析器是由 flex/bison 在构建时动态生成的这两个工具属于构建链的一部分而不是测试环境自带工具。很多人只装了 g 和 cmake忽略了它们。解决按 2.1 的 apt 命令装齐。注意装完后把build目录删掉再重新 cmake因为失败的 CMake 缓存里可能还记录着旧状态。验证工具版本用flex --version、bison --version如果系统自带的是 2.x 老版本且源码用了 bison 3 的特性建议手动装新版本到/usr/local/bin并把 PATH 调成先找到新版本。5.2 observer 启动即退出报错找不到 observer.ini现象执行./build/bin/observer -f etc/observer.ini后终端马上打印一行错误就退出内容是Failed to open ... observer.ini或类似字样。原因这是典型的“当前工作目录不对”。MiniOB 的配置路径是相对路径如果你的 shell 不在项目根目录它就会到当前目录找etc/observer.ini。此外配置文件里如果写了相对路径的 socket 文件也会因为工作目录错误而被创建到意外位置。解决启动前先cd到项目根目录确认etc/observer.ini存在。同时看配置文件里的unix_socket_path或sock字段把 socket 也指定成项目根目录下的绝对路径或者始终使用同一个固定的相对起点。如果你用 GDB 启动也要注意 GDB 里pwd和在 shell 里不同最好用gdb --args ./build/bin/observer -f $(pwd)/etc/observer.ini。5.3 obclient 启动后连不上报 “connection refused”现象服务端进程还在但客户端一启动就显示无法连接或者输入 SQL 后没有响应随后退出。原因服务端监听的 socket 文件和客户端尝试连接的 socket 文件路径不一致。这种不一致又分两种一是服务端和客户端从不同工作目录启动二是服务端崩溃重启后旧的 socket 文件还残留着客户端连到了残留文件但已经没有进程在监听。解决找到项目根目录下生成的.sock文件删掉后重启 observer再启动 obclient。确认两者的配置来源一致——observer 从observer.ini读 socket 路径obclient 通常也从同一份配置文件读。如果实在排查不出用lsof -U看看当前监听中的 socket 文件路径lsof -U | grep miniob这样可以直接看到服务端到底监听了哪个文件再对比客户端配置里的路径问题一目了然。5.4 make 编译到一半 OOM虚拟机直接卡死现象make -j8跑了一会儿终端报internal compiler error: Killed或者系统完全卡顿。原因-j$(nproc)在宿主机上可能是 8 核 16 线程但在虚拟机里内存只有 1GB、2GB所有编译任务同时展开后内存爆了。C 编译本身是内存大户词法/语法生成后的模板代码更甚。解决降低并行度用make -j1或make -j2。如果还不行关闭一些占用内存的后台进程或者给虚拟机加内存。编译observer这种中型项目2GB 内存基本够用但别开着浏览器和 IDE 一起跑。另外可以换成 Ninja它在调度并行任务时内存表现更好cmake -DCMAKE_BUILD_TYPEDebug -G Ninja .. ninjaNinja 的-j也可以通过NINJA_STATUS或ninja -j2控制。5.5 运行 select 时崩溃或结果数据错乱现象执行select * from student时observer 直接段错误打印Segmentation fault或者能返回但某些行的数据看起来是乱的。原因这类问题大多是内存操作越界。常见位置是析构函数、字符串拷贝、记录长度计算。因为 MiniOB 大量使用裸指针和默认拷贝构造一个 Record 对象被重复释放就会崩。此外类型长度不匹配也会导致读到错乱数据比如建表用char(4)插入abcde没有做长度检查。解决先用 AddressSanitizer 重新编译它能在崩溃时直接告诉你哪个文件哪一行越界mkdir -p build-asan cd build-asan cmake -DCMAKE_BUILD_TYPEDebug -DCMAKE_CXX_FLAGS-fsanitizeaddress -fno-omit-frame-pointer .. make -j2然后用build-asan/bin/observer复现崩溃再把 ASAN 报错栈的顶部几行记录下来。绝大多数问题都能通过这个栈直接定位。如果 ASAN 没报就在执行器加日志二分缩小范围优先检查 Table 的 Record 长度计算和memcpy的size参数。6. 一个调试习惯用 GDB 日志把 MiniOB 变成你的数据库实验沙盒最后分享一个我很受用的调试习惯它能帮你把前面那些表面的运行问题变成“看得见、找得到”的源码问题。核心就两招GDB 抓现场日志定边界。先用 GDB 启动 observer注意把配置参数用--args方式传进去gdb --args ./build/bin/observer -f etc/observer.ini在 GDB 内输入run服务端就在调试器里启动了。另开一个终端执行 obclient随便敲一条让程序崩掉的 SQLGDB 会立刻停在崩溃点。输入bt会得到当前调用栈从下往上数前三帧基本就是问题发生的现场。比如栈顶是memcpy再往下一帧是Table::insert_record就能确定是记录写入时长度不对。GDB 这种交互式调试对运行期崩溃很高效但对“逻辑不对但没崩”的问题日志比 GDB 更顺手。我的习惯是在每一条链路的入口和出口各打一行日志形成一条“政务透明”的执行路径[TRACE] recv sql: select * from student; [TRACE] parsed: select [TRACE] execute begin [TRACE] execute end, rows3打日志的落脚点就是第四章 grep 出来的那些 executor 文件。你不需要一次打很多先用二分法如果怀疑 select 有问题在 parser 出口和 executor 出口各打一条看哪一侧缺日志。缺在 parser问题在语法解析缺在 executor问题在执行或存储。这比盯着代码猜效率高得多。我自己刚拆 MiniOB 时也踩过不少坑最难忘的是晚上两点发现数据写文件后读不出来最后用二分日志定位到页缓存没有刷盘。那次之后我养成了“先跑通、再打日志、后动大改”的习惯后面再改索引、改 Buffer Pool都会先在入口打一条 trace 验证链路。MiniOB 的代码量决定了它有足够深的底层细节但它依然只是一层窗户纸——编译好、跑通 SQL、打几条日志你就能把数据库系统概论里的那些概念全部落到代码上。希望帮到你。本文还有配套的精品资源点击获取