
简介2023 OceanBase数据库大赛初赛资料包源自MiniOB数据库学习项目面向数据库入门者、高校学生及内核技术爱好者以Windows环境下的工程代码为主。压缩包共637个文件大小14.52MB其中以C/C头文件h/cpp为核心提供SQL解析、存储索引等模块的源码实现Markdown文档md梳理了模块设计思路和排错要点PNG图片png保存了关键运行截图与实验结果另附shell、Python脚本、CMake配置等辅助内容方便环境搭建与测试整体目录清晰且便于按模块查阅。目前已有160人学习下载。资料覆盖MiniOB的SQL解析、B树索引、表管理、事务等关键模块代码注释与配套文档紧密呼应能直观展示内存管理、网络通信、磁盘I/O上的简化设计与实现帮助理解数据库内核的运转流程。通过复盘初赛的完整方案与调试记录读者可掌握各模块之间的协作关系提升实际工程编码能力无论备赛冲刺还是深入学习都值得参考。1. 2023 OceanBase数据库大赛初赛.zip一份压缩包一次内核级实战下载 2023 OceanBase数据库大赛初赛.zip 后第一个小时多数人都耗在翻题目说明上真正决定初赛能不能出线的是赛包里那套没写进题面的评测约定。这份压缩包通常装着题目说明、可编译的内核框架、测试脚本和 README参赛者要做的不是交一段独立程序而是在给定框架里实现存储、事务或查询路径中的某一环让黑盒脚本在固定操作序列下认可结果再用更少的时间和内存换取更高排名。下面按拆包、跑通、避坑、优化的顺序把从看懂题到拿到分的关键动作讲清楚适合准备参赛的队伍和想借真题练内核功底的开发者。2. 拆开赛包先干三件事读懂评测逻辑、认清代码框架、确认交付格式解压之后不要急着翻开题目 PDF先把目录树完整列一遍README、脚本、样例输入输出先看。很多翻车不是题目不会做而是没搞懂评测脚本到底拿你的程序做什么。初赛的核心不是“写对答案”而是“让脚本认可你的程序”这两件事在细节上差得很远。先把下面三件事做完再动代码。2.1 评测逻辑先过一致性再比耗时和占用初赛的评测形态各年会有调整但骨架基本是黑盒回放评测程序生成一组固定 key 和操作序列比如 put、get、delete、scan 这类原语按顺序灌进你的引擎跑完后再把引擎的落盘结果或查询结果和标准答案比对。比对的维度通常是结果集合的完备性、数值正确性和顺序约定顺序是很多人栽的地方——某些评测允许无序某些要求严格按 key 序这个信息只会在 README 或评测脚本里出现。评分一般分两层正确性不达标成绩直接不计排名正确性达标后再按耗时、峰值内存和磁盘占用排序。所以“能跑”和“能得分”之间隔着一道正确性闸门而这扇闸门的开关不在你本地在评测脚本里。拿到赛包后我一般按这个顺序确认评测形态先看有没有 eval、test、bench 这类目录里面脚本写得越细越能省你后面几天再看样例输入输出的格式把每个字段对齐最后看有没有环境变量注入比如数据目录、缓存大小、并发线程数。把这三者记下来才是后面调参的依据。评测脚本里常见这样的循环# 示意代码具体以赛包内脚本为准 for c in $(cat case_list.txt); do ./your_engine --data-dir $DATA_DIR $c.in $c.out if ! diff -q $c.out $c.expected /dev/null; then echo case $c mismatch exit 1 fi done这段脚本先跑功能用例用 diff 逐行比对输出任何一个 case 不一致就直接判失败。注意 diff 是顺序敏感的输出行的先后顺序错了同样算错这解释了为什么很多实现“数据都在”却过不了评测。能走到性能评测的 case 会更大脚本会换成独立计时逻辑并把内存上限用资源限制命令卡住。所以确认评测逻辑的第一优先级永远是输出格式字段分隔符、行序、结尾有没有空行都要和样例输出逐字节一致。还要留意脚本在 diff 之前的预处理。有些赛包的脚本会先用 sed、awk 把输出里的时间戳或日志行过滤掉再和期望结果比对如果没看懂这层你会把精力花在修一个评测根本不看的东西上。反过来如果脚本是直接 diff那结果文件里任何多余输出都致命连多余空行都不能放过。判断方法很简单把样例跑一遍把 stdout 存下来和样例文件做一次 hexdump 级别的对比看差异出现在哪里。2.2 代码框架怎么读入口、核心接口、内存管理三张图赛包框架一般是可编译的空壳工程接口给你留好实现留空。读框架不要逐行读先画三张图第一张是入口main 函数在哪、命令行参数怎么解析、数据目录和配置项从哪进来第二张是核心接口也就是你最终要填充的那几个函数它们的签名和注释通常直接定义了数据流的走向第三张是内存管理谁负责分配和释放对象的生命周期谁说了算。很多人在第二张图上花的时间不够导致改了半天发现实现错了层该在存储层做的写到了引擎层性能对不上正确性也悬。找实现入口最快的方式不是翻文档而是搜标记。框架里通常留着 TODO、FIXME、或者直接 return 0 的假实现那几行就是你全部的主战场grep -rn TODO\|FIXME\|NOT_IMPLEMENTED src --include*.cc --include*.h | head -60 # 先定位未实现函数再看它们被哪些调用点引用画出完整调用链把搜索范围限定在 src 目录能避免把第三方依赖的噪音带进来head 限制输出条数方便一次性浏览。定位到未实现函数后不要急着写逻辑先在头文件里看接口注释和输入输出约定再用框架自带的单元测试或样例跑一遍打开日志观察正确调用链长什么样。日志里记录什么、以什么格式记录往往就是后面排查问题的对照物。内存管理这张图尤其要画清楚如果框架让你返回 Slice 而不是 string说明它默认零拷贝如果你自己 new 的对象没在析构里清理大数据集跑到一半就 OOM。看框架自带的测试怎么构造和销毁对象是理解生命周期最快的方式。把这三张图画完你才算真正拿到代码的主动权而不是被框架带着走。2.3 交付格式决定成败目录结构、编译产物、README 里的评分说明最后反复确认交付形式。初赛提交的不只是源码它是“一套能在评测机上自洽构建并运行的东西”。常见交付物包括源码目录、构建脚本、可执行文件或可执行文件名约定、以及一份简短的启动说明。评测机一般不会手工敲命令它会按约定调用构建脚本再用固定方式启动你的程序目录名或路径变了就整个失败。交付物常见要求提交前检查点源码目录保持赛包原有结构不额外套一层目录相对路径构建不依赖当前用户目录构建脚本一键执行支持无交互构建在干净环境下跑一次确认无网络依赖可执行文件/产物名与评测约定完全一致大小写敏感检查评测脚本里调用的文件名和路径README写明启动参数、数据目录、特殊前提参数与评测脚本注入的环境变量一致这张表列的项每年都有人丢分尤其文件名大小写和路径前缀这两个看起来不起眼的点。输出规范的坑更隐蔽评测脚本可能要求结果写到 stdout也可能要求写到你指定的数据目录下的某个文件如果它比较的字段里包含时间戳或随机数而你的实现里混进了这些内容diff 必然失败。这类问题在本地造数时看不出来只有对照样例逐字节比对才能发现。配置项方面常见的注入方式是通过环境变量或命令行参数设置数据目录、缓存上限、线程数、日志级别。建议把评测脚本里设置过的每个变量都抄下来形成一张参数表参数/环境变量典型取值影响数据目录评测机上的临时目录影响所有落盘路径代码里不能写死缓存上限几百 MB 级别超限会被 OOM直接 kill日志级别warn/errordebug 日志拖慢性能、刷爆空间线程数可能固定为 1并发不能成为正确性前提把参数表和启动代码对齐后再开始动手实现。这半小时的读包时间是整个初赛里最划算的投入。3. 从解压到跑通编译、启动、造数与验证的最小链路框架读明白之后下一步是让整条链路在本地真实跑起来。这里有三个关卡要依次过编译不过、启动失败、结果不对。每一关都有固定的排查套路按顺序走避免在玄学问题里耗掉一整天。先说明下面的命令都是示意可执行文件名、参数名、输入格式以你手里的赛包为准我讲的是路径和方法。3.1 编译环节先锁编译器版本再处理第三方依赖编译环节的翻车点在“本地能过、评测机过不了”。赛包通常会给出建议的编译方式和构建方式但你自己机器上的默认工具链可能更新也可能更老。锁版本的方法是用构建脚本里出现的编译器命令作为基准不给默认版本发挥的空间。比如构建系统里写的是 cmake就明确指定生成器和 Release 配置不要依赖本机的默认 Debug 选项。mkdir -p build cd build cmake .. -DCMAKE_BUILD_TYPERelease -DCMAKE_CXX_STANDARD17 \ -DCMAKE_CXX_FLAGS-O2 -fno-omit-frame-pointer make -j$(nproc) cd .. ./build/your_engine --help /dev/null 21 echo build ok这里把标准固定到 C17O2 是性能和排错的平衡点fno-omit-frame-pointer 让后面性能画像时调用栈更完整代价是极小的体积开销值得带上。debug 和 release 的差异比很多人以为的大断言、容器检查、日志行为都会改变所以从第一天起就用 Release 调优省得提交前一晚换构建方式。第三方依赖是编译环节最大的变量如果赛包自带源码就静态编进去如果需要链接系统库先确认评测机的约定版本和你本机一致。编译成功后马上验证可执行文件能响应 --help 或等价参数这一步能在十秒内区分“框架本身有问题”和“你的环境有问题”。3.2 启动与初始化数据目录、资源上限、日志级别编译通过只是第一步启动参数决定你能不能在评测机的资源限制里活下来。常见做法是让程序支持命令行参数覆盖默认配置而不是改代码。最值得先定死的是数据目录、缓存上限和日志级别。缓存上限尤其重要评测机通常会卡内存你的实现如果无限缓存到大数据集必然被 kill。./build/your_engine \ --data-dir ./data \ --cache-mb 512 \ --log-level warn \ --threads 1>#!/usr/bin/env bash set -euo pipefail ENGINE./build/your_engine rm -rf ./data mkdir -p ./data python3 - EOF import random random.seed(42) keys [f{random.randint(0, 999999):08d} for _ in range(10000)] with open(case.in, w) as f: for k in keys: f.write(fput {k} {v*64}\n) for k in random.sample(keys, 5000): f.write(fget {k}\n) EOF $ENGINE --data-dir ./data --cache-mb 512 --log-level warn case.in case.out wc -l case.out随机种子固定保证每次造出来的输入完全一致key 用 8 位定长数字序关系明确put 和 get 的写法只是示意具体原语名必须和赛包里的命令格式一致。set -euo pipefail 让脚本在引擎崩溃或管道中途失败时立刻退出而不是带着错误继续跑。最后 wc -l 对齐输出行数是最快的一层正确性检查行数不对后面 diff 都不用做。行数对上了再校验内容完备性。把结果解析成集合并与期望比对先把“数据缺了”和“顺序不对”两类问题分开# 示意排序后对比内容集合只关心有没有缺数据 sort case.out | uniq out_sorted.txt sort expected.txt | uniq exp_sorted.txt diff -q out_sorted.txt exp_sorted.txt这一步通过后再回来处理顺序问题顺序问题的病根后面专门讲。最小验证脚本到这里就算闭环了造数、回放、行数检查、集合检查每一步都有明确输出失败时能立刻定位到是哪一层出了问题。3.4 性能观测时间、内存、IO 分开记录功能通了就要把性能指标量化下来否则后面优化全凭感觉。我不用 shell 内置的 time它的输出字段太少也分不清用户态和内核态用 /usr/bin/time 把所有指标打到单独文件里跑完一次性看/usr/bin/time -v \ ./build/your_engine --data-dir ./data --cache-mb 512 --log-level warn \ case.in case.out 2 time.log grep -E Elapsed|Maximum resident|User time|System time time.log-v 参数会输出用户态时间、系统态时间、峰值驻留内存、文件系统输入输出量。真正该盯的是峰值内存评测机的上限是固定的你的缓存配置和数据结构决定它会不会触发 OOM。记下这组数字作为基线。后面每次改结构都跑同一份输入对比这三项指标优化才有方向而不是“感觉快了一点”。3.5 在受限环境里自检模拟评测机的资源限制最后一步模拟评测机的压力。你本地机器内存大但评测机不一定本地跑 3 秒的成绩在受限环境里可能变成 30 秒甚至被 kill。用资源限制命令模拟一下比自己猜靠谱得多。ulimit -v 2097152 # 限制虚拟内存约 2GB ulimit -t 120 # 限制 CPU 时间 120 秒 ./build/your_engine --data-dir ./data --cache-mb 512 --log-level warn case.in case.out echo exit$?ulimit -v 限制虚拟内存超出会直接收到内存分配失败或被内核 killulimit -t 限制 CPU 时间超时会收到 SIGKILL。exit 非 0 就是触到限制需要回头调缓存策略或减少无谓分配。这一步虽然不能完全复刻评测机但它能提前暴露最致命的两类问题内存峰值超限、特定输入下死循环。注意这是在当前 shell 里临时生效不会影响系统其他进程适合写进自检脚本每次打包提交前跑一遍。4. 初赛避坑5 个浪费半天以上的真实翻车点这五条是初赛里出现频率最高的翻车点每一条都真实存在且都导致过“白干半天”或“白干一整天”。按现象、原因、解决三步写你对照自查就能省下时间。重点不是记住答案而是学会从现象反推原因的方法。4.1 本地编译通过评测机上一行报错现象本地 Release 构建一切正常提交后评测反馈编译失败日志里只有几行 error。 原因绝大多数是编译器版本差异比如本地编译器默认开了新特性或者 C 标准没锁少数是第三方库路径写死成了绝对路径评测机上没有这个路径。 解决锁标准、静态化、干净环境自检。构建时把 -std 明确写在 CMake 或 Makefile 里不要依赖隐式默认cmake .. -DCMAKE_CXX_STANDARD17 -DCMAKE_CXX_STANDARD_REQUIREDONCMAKE_CXX_STANDARD_REQUIRED 为 ON 时编译器不支持该标准会直接失败而不是悄悄回退到低版本。这比“加一行注释提醒自己”可靠。第三方库能静态链接就静态链接确保构建不依赖评测机上的系统库版本实在要动态链接就把动态库一并打进提交包并在 README 里写明放哪个目录。4.2 峰值内存超限跑到一半被 kill现象小数据量一切正常换成大数据集程序跑到一半消失输出文件只写了一半。 原因缓存没有上限或者每次查询都把全量数据加载进内存另一种隐蔽情况是删除操作只标记不清理太久没做空间回收数据文件里攒了大量失效块。 解决给缓存和数据块都设上限达到阈值就刷盘或淘汰删除要真正释放可复用空间。自检时按 3.5 的 ulimit 方法压一遍再用 /usr/bin/time -v 看峰值内存落在哪个操作阶段。如果峰值集中在启动阶段说明加载全量索引如果集中在写入阶段说明写缓存无界。定位后分别治理不要上来就改内存分配器那是最后一个选项。4.3 结果内容全对diff 就是不通过现象人工看输出每条记录都在数值也对diff 一下第一行就 mismatch。 原因顺序问题。实现里用了哈希表或并发结构迭代顺序和样例不一致或者同一个 key 的多次操作记录的是最后写入还是第一次写入语义没对齐赛包约定。 解决先确认顺序语义再统一输出顺序。最稳妥的做法是不依赖任何容器内部顺序按 key 排序输出或按输入操作序号记录位置# 先确认期望文件是否本身有序只排序实际输出不要动期望文件 sort -n out_actual.txt | diff -q - expected_sorted.txt注意如果样例本身就是无序的排序后比对反而会误判所以先查 README 里到底有没有顺序约定。顺序类问题的病根在写入路径get 返回时的查链顺序、scan 的遍历起点都要和框架注释对齐。这类 bug 在单线程下可能不出现一开并发就冒出来所以排查时把线程数降回 1逐条回放输入定位到第一条 mismatch 的位置。4.4 debug 日志刷爆磁盘评测跑到一半空间不足现象明明结果正确评测反馈运行异常放大日志才发现几十 GB 的日志文件把磁盘写满。 原因日志级别开在 debug而每条操作都打一行功能测试量小没暴露到了性能评测的数据量就爆了。更隐蔽的是日志目录和数据目录在同一块盘上日志写满后数据文件也写不进去最终表现是数据损坏而不是单纯的慢。 解决日志级别常驻 warn只有定位问题时才临时开 debug定位完立刻改回。程序内部再加一层保险日志文件超过阈值就轮转或清空。# 日志单独放一个目录和数据目录隔离级别明确写 warn ./build/your_engine --data-dir ./data --log-dir ./logs --log-level warn 2 engine.err把日志路径固定下来还有个好处评测失败时你能直接从日志里定位到最后一个成功操作而不用在海量输出里捞。日志和数据目录分离这条看起来是小事关键时刻能救回一整个性能测试。4.5 打包后缺文件评测机上目录结构和本地不一样现象本地跑得完美评测环境里报“找不到文件”或“段错误”检查发现构建产物、资源文件或配置没进提交包。 原因构建产物被 .gitignore 排除了软链接在打包后失效脚本文件没有执行权限评测机调用时直接 Permission denied。 解决打包前跑一遍自检脚本从一个干净的临时目录重新构建并运行最小 case。软链接能不用就不用直接复制文件脚本统一加执行权限再提交chmod x run.sh build.sh tar czf submit.tar.gz $(ls -A) # 提交前解压到临时目录按 README 的命令完整跑一遍chmod 之后再用 tar 打包确保权限位进了压缩包。肉眼检查压缩包列表看有没有长度异常的路径或者指向本机路径的软链这类问题在评测机上会原样放大。养成打包后解压验证的习惯比任何检查清单都可靠。5. 从跑通到拿分用固定基准与性能画像驱动优化功能和稳定都过关之后排名比的是单位时间能处理多少操作、峰值内存能压到多低。这时候不要凭感觉改代码而是先建立固定基准再做性能画像最后只选高性价比的点动手。先造一份贴近评测分布的基准数据key 用固定宽度数字数量至少百万级put 与 get 的混合比从赛包样例推断比如 73随机种子固定。后续每次优化都跑同一份输入才能对比前后指标。跑之前用上一章 /usr/bin/time 的方法记录用户态耗时、系统态耗时、峰值内存存成基线文件。然后做画像不要猜瓶颈在哪# 需要带调试符号的构建所以前面加了 -fno-omit-frame-pointer perf record -g ./build/your_engine --data-dir ./data --cache-mb 512 case_big.in perf report --stdio | head -40perf 需要一个带调试符号的构建否则报告里全是地址没有函数名。看报告里占比最高的那个函数大概率就是热点。如果热点集中在字符串处理上把 std::string 换成框架自带的 Slice/StringRef能省掉大量拷贝如果热点在单条写路径的重复落盘考虑批量 flush把多次小写入合并成一次大写入。这两个方向在初赛里往往是最先见效的比花一晚上调锁粒度划算得多。提交前再检查三件事构建脚本在干净目录可执行、输出格式和样例逐字节一致、日志级别保持 warn。然后跑一遍完整自检把时间记录留给下一次迭代。我最疼的一次教训是功能全部正确性能测试名列前茅最后因为日志级别忘关大数据集上多花了三分之一时间排名掉了一大截。从那以后我养成了提交前先 grep 代码里有没有 debug 输出的习惯自检脚本第一条就是检查日志级别。竞赛的价值恰恰在这编译器版本、资源限制、输出约定每一项都会在评测机上诚实地还给你。把我上面这些习惯直接拿来用你能少走不少弯路希望帮到你。本文还有配套的精品资源点击获取