news 2026/10/13 9:53:38

MiniOB数据库系统源码拆解:从编译到SQL执行的调试之旅

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MiniOB数据库系统源码拆解:从编译到SQL执行的调试之旅

简介:基于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/bison

MiniOB 是纯 C++ 工程,代码里用到了 C++11/14 的特性,所以编译器至少要支持 C++11。在 Ubuntu/Debian 系统上,最省事的做法是一次性把构建链和解析器生成工具装齐:

sudo apt update sudo apt install -y build-essential cmake flex bison libjsoncpp-dev
  • build-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 debug

debug参数对应-DCMAKE_BUILD_TYPE=Debug,会带上-g调试信息,后续用 GDB 调试时必须用它。如果脚本没执行权限,先chmod +x build.sh。脚本执行完,可执行文件一般在build/bin或output/bin下,具体看脚本里的OBJ_DIR定义。

如果你手里没有build.sh,或者想自己折腾构建过程,可以直接用 cmake:

mkdir -p build cd build cmake -DCMAKE_BUILD_TYPE=Debug .. make -j$(nproc)
  • -DCMAKE_BUILD_TYPE=Debug指定调试构建,等会儿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 schema,float 的等值比较在执行器里直接做字节比较就会有问题,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 表名这种“读元数据”的命令入手。步骤是:

  1. 在词法文件lex.l中把desc映射到一个 token,比如DESC_TOKEN。
  2. 在语法文件parser.y中新增规则:statement: DESC_TOKEN IDENTIFIER,并生成一个描述该语句的节点。
  3. 在分发函数里找到处理语句类型的 switch/case,加一个分支,调用表元数据读取接口。
  4. 在 executor 目录下写一个desc_executor.cpp,从TableManager之类的地方取表的列定义,拼成文本响应返回给客户端。
  5. 重新跑 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_TYPE=Debug -G Ninja .. ninja

Ninja 的-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_TYPE=Debug -DCMAKE_CXX_FLAGS="-fsanitize=address -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,随便敲一条让程序崩掉的 SQL,GDB 会立刻停在崩溃点。输入bt,会得到当前调用栈,从下往上数,前三帧基本就是问题发生的现场。比如栈顶是memcpy,再往下一帧是Table::insert_record,就能确定是记录写入时长度不对。

GDB 这种交互式调试对运行期崩溃很高效,但对“逻辑不对但没崩”的问题,日志比 GDB 更顺手。我的习惯是在每一条链路的入口和出口各打一行日志,形成一条“政务透明”的执行路径:

[TRACE] recv sql: select * from student; [TRACE] parsed: select [TRACE] execute begin [TRACE] execute end, rows=3

打日志的落脚点就是第四章 grep 出来的那些 executor 文件。你不需要一次打很多,先用二分法:如果怀疑 select 有问题,在 parser 出口和 executor 出口各打一条,看哪一侧缺日志。缺在 parser,问题在语法解析;缺在 executor,问题在执行或存储。这比盯着代码猜效率高得多。

我自己刚拆 MiniOB 时也踩过不少坑,最难忘的是晚上两点发现数据写文件后读不出来,最后用二分日志定位到页缓存没有刷盘。那次之后我养成了“先跑通、再打日志、后动大改”的习惯,后面再改索引、改 Buffer Pool,都会先在入口打一条 trace 验证链路。MiniOB 的代码量决定了它有足够深的底层细节,但它依然只是一层窗户纸——编译好、跑通 SQL、打几条日志,你就能把数据库系统概论里的那些概念全部落到代码上。希望帮到你。

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

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

Vision Transformer图像去雾:全局自注意力原理与PyTorch工程实践

简介&#xff1a;面向图像去雾与视觉Transformer研究者和开发者&#xff0c;提供基于Vision Transformer算法的完整Python实现&#xff0c;含源代码、预训练权重与详细使用说明。压缩包共340个文件&#xff0c;约156MB&#xff0c;其中204个Python脚本负责模型构建、训练参数配…

作者头像 李华
网站建设 2026/10/13 9:51:28

深入解析 etcd Raft 库:核心架构、API 使用与源码实现

容器运行时云原生 【免费下载链接】docker-ce :warning: This repository is deprecated and will be archived (Docker CE itself is NOT deprecated) see the https://github.com/docker/docker-ce/blob/master/README.md :warning: 项目地址&#xff1a; https://gitcode.co…

作者头像 李华
网站建设 2026/10/13 9:50:50

MySQL存储过程与函数实战:核心区别、游标、异常处理与性能优化

1. 为什么存储过程和函数值得单独拎出来讲刚入行那会儿&#xff0c;我对存储过程和函数的理解就停留在“数据库里能写逻辑”这个层面。直到有一次做报表统计&#xff0c;一条复杂查询在应用层拼了七八个SQL&#xff0c;网络往返十几次&#xff0c;响应时间直接飙到三秒多。后来…

作者头像 李华
网站建设 2026/10/13 9:49:57

微客AI助手复盘:客户开口头一句怎么接?开场白与首响话术的设计

客服会话里最值钱的一句话&#xff0c;是商家的头一句回复。这句话之前&#xff0c;客户带着疑问进来&#xff1b;这句话之后&#xff0c;要么话题打开了&#xff0c;要么客户已经悄悄把对话框关掉。我们复盘微客AI助手接管的几万条真实会话时&#xff0c;专门看了首响环节&…

作者头像 李华