news 2026/10/9 11:50:16

拆解MiniOB:几千行C++数据库内核的编译、执行与事务实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
拆解MiniOB:几千行C++数据库内核的编译、执行与事务实践

简介:基于C++实现的MiniOB数据库系统,是由OceanBase与华中科技大学联合推出的数据库入门实践项目,主要面向零基础学生,帮助其快速理解数据库内核各模块及其关联,并能设计出高效SQL。资源包内共三百六十三个文件,以一百一十九个头文件与一百零七个C++源文件为核心,覆盖词法语法分析、B+树索引、磁盘缓冲池、表管理、事务日志等关键模块;另有五十八个png示意图辅助理解架构,十九个md文档说明实现细节,压缩包整体约3.17MB,便于系统阅读与工程对照。通过该项目可掌握INI配置解析、IO操作、互斥锁管理、日志系统、内存池管理、SEDA框架、记录与索引管理等一系列基础功能,理解数据库从SQL解析到存储执行的整体链路。目前已有七十四人学习下载,适合作为高校数据库课程设计、考研复习或自学的入门参考,具有较高的学习价值。

1. 一个几千行的 C++ 数据库内核,为什么值得你拆开看

一条select * from t where id = 1;背后要经历词法解析、语法树构建、执行计划生成、算子调度、存储页读取、事务可见性判断这么一整条链路。MiniOB 恰恰就是把这个链路压缩到只要几千行 C++ 就能跑通的教学内核:它不追求高并发,不追求分布式,甚至只支持 MySQL 的一个很小方言子集,但数据库内核该有的骨架——解析器、执行器、缓冲池、表、事务、日志——一个不少。我见过太多人背了一堆缓冲池 LRU 策略、MVCC 快照理论,下到代码层却不知道哪个文件对应哪个概念。MiniOB 就是用来治好这种"理论懂、代码不懂"的。这篇笔记面向正在学数据库实现、要交课程设计、或者想补内核底子的工程师,目标是让你拿到源码包之后,从编译启动到读通执行链路,再绕开我踩过的几个重复坑。

2. 先跑起来再看源码:MiniOB 的编译启动与最小验证

拿到源码包之后,我建议的顺序永远是先编译、先跑通、再往里读。直接翻开源码往往会被目录结构劝退,因为一个教学数据库的目录也是按模块拆的,而模块之间的调用关系远比目录复杂。

2.1 拿到源码包后,先按这三块建立目录心智模型

MiniOB 的源码目录大体上可以分成三层去看。第一层是src/observer下面的主程序与模块,这一层承载了数据库实体的完整流程:SQL 解析、计划生成、执行器、存储引擎、事务管理。第二层是依赖目录,主要存放构建时需要的外部组件和公共库。第三层是测试相关目录,里面放着大量 SQL 用例和跑批脚本,这部分恰恰是最有用的学习材料。

读源码时不要从第一个文件看到最后一个文件,那样会迷路。我一般会从主程序切入,找到接收客户端请求的入口,然后顺着一次insert命令往下追两层,先把整体调用链画出来:网络接收 → 命令分发 → 词法语法解析 → 语法树 → 执行器 → 存储接口。这个调用链就是整个 MiniOB 的骨架。你不需要一上来就懂每一处细节,先建立"命令是从哪进去的、数据是从哪出来的"这个框架。

我在读第一遍时做了一张非常粗糙的映射表,放在手边反复对照。这张表不需要精确到类名,只需要记住大方向:SQL 相关代码在sql目录里,表与页相关的存储逻辑在storage目录里,事务与隔离在transaction目录里,日志与配置在common目录附近。后面的章节里,每一步都会落回这张表。

2.2 编译依赖与构建参数:Debug 比 Release 重要

编译 MiniOB 不是难点,但有两个小细节值得先说明。第一个是依赖工具齐全。MiniOB 的解析器采用词法语法分析器生成,所以构建机器上需要装好 flex 和 bison。缺了它们,编译会在构建 parser 相关代码时失败,而且报错往往比较隐晦。第二个是把构建类型设置为 Debug。教学数据库的代码里大量使用断言和调试日志,Debug 构建能把它们全部激活,后续排查问题会省很多力气。

# 在源码根目录下创建构建目录,避免污染源码目录 mkdir -p build && cd build # Debug 构建保证断言和符号信息完整 cmake .. -DCMAKE_BUILD_TYPE=Debug # -j 后面的数字按 CPU 核数调整,4 核机器用 -j4 即可 make -j4

构建完成后我建议先看一眼生成文件的体积,如果observer这个可执行文件出来了,说明主程序编译通过。如果中途失败,先看报错的第一行而不是最后一行,尤其是 flex 或 bison 相关的报错,那通常意味着工具没装或者 PATH 里找不到它们。CMake 的配置项除了构建类型之外,还有一些开关可以控制是否启用某些模块,具体选项名称每个版本略有差异,建议直接看 CMakeLists.txt 里的option声明。

这块最容易被忽略的是构建目录的污染问题。如果你直接在源码根目录下执行cmake .,生成的临时文件会和源码混在一起,后面 git 切分支或者重新配置时会遇到各种奇怪现象。我见过某同学因为在源码目录里反复切换构建模式,最后 parser 生成的文件一直不更新,导致语法改动死活不生效,这就是典型的构建目录没隔离。

2.3 一条建表语句跑通全链路:验证环境的第一步

编译成功之后,下一步是启动服务并用客户端连上去。MiniOB 的启动方式并不复杂,核心是用-f参数指定一个配置文件,端口、日志级别、缓冲池大小都在这个配置文件里维护。不同版本的配置项名可能有差异,我一般会先打开配置文件看一眼,再决定要不要改端口。

# 在构建目录里启动 observer,-f 指向配置文件 cd build ./observer -f ../etc/observer.ini # 另开一个终端,用 mysql 协议客户端连接 # 端口以配置文件里的配置为准,常见版本默认是 6789 mysql -h 127.0.0.1 -P 6789 -u root

连接上之后,先不要跑任何复杂的 SQL,用最小三步验证环境:建表、插入、查询。如果这三步能顺利跑通,说明从语法解析到存储落盘、再到执行器返回结果,整条链路都是通的。

create table student(id int, name char(20)); insert into student values(1, 'zhangsan'); select * from student;

这里有个细节需要注意:MiniOB 支持通过begin显式开启事务。如果不写begin,单条语句会被当做一个独立事务自动提交,所以你直接执行上面的 insert 再 select 是能看到数据的。一旦你用begin开启了事务,后续的 insert 就必须commit之后才对其他会话可见,这个行为我们在避坑章还会遇到。

验证环境这一步建议顺手做一件事:把上面的 SQL 保存成一个.sql文件,用客户端重定向执行,例如mysql -h 127.0.0.1 -P 6789 -u root < test.sql。这比在交互终端里一条条敲更接近真实测试场景,后面做回归测试时也要依赖这个习惯。

3. SQL 从文本到执行计划:MiniOB 解析与执行链路的三个核心站

数据库内核里最让人头晕的往往不是存储,而是 SQL 怎么变成机器能执行的动作。MiniOB 把这套流程压缩得很短,但每一站都是完整的。理解这条路径,后面所有二次开发才有抓手。

3.1 词法语法层:文件与生成产物分别是什么

MiniOB 的词法分析和语法分析是分开的两个阶段,分别由 flex 和 bison 负责。词法分析器把客户端发来的 SQL 文本切成一串 token,每个 token 带着类型和位置信息;语法分析器再根据文法规则把这些 token 组织成一棵语法树。这棵树随后被转换成一种统一的结构,里面记录了命令类型和该类型对应的附加参数。

例如create table student(id int, name char(20));这条语句,语法树会记录这是一个建表命令,附带表的 schema 信息:两个字段,一个叫id,类型是 int,另一个叫name,类型是 char,宽度是 20。后续的 insert、select 语句,都会生成类似的语法树节点。

理解这一站时,我建议你找到 parser 相关的目录,分别打开词法文件和语法文件,不要被大段规则吓到。你只需要关注一条规则:create_table_statement是怎么被定义的,它的产生式右侧引用了哪些 token。看完后你会明白,MiniOB 的 SQL 方言为什么比 MySQL 窄很多——因为它只定义了有限条语法规则,凡是没定义的写法一律解析失败。

这一站最容易踩的认知误区是分不清"解析"和"校验"。语法分析只负责把文本变成结构,字段类型存在不存在、表名是否存在,这一站完全不管。MiniOB 在语法树生成之后还要经过语义绑定,才把字段名映射到实际的表列。把这两个阶段分开,后续读执行器代码时会轻松很多。

3.2 语句到算子的映射:以 select 为例的物理计划

语法树只是逻辑描述,真正执行时要把它转化成物理算子组成的执行计划。MiniOB 的物理算子在设计上非常朴素,但它遵循了一个经典接口:open、next、close。open 负责初始化,next 每次产出一行数据,close 负责收尾。整个执行就是驱动一棵算子树,从最底层的扫描算子开始,一层层向上拿数据。

-- 用这条 SQL 走通整个执行链路 select s.name, c.score from student s, course_score c where s.id = c.student_id and c.score > 60;

这条 SQL 在 MiniOB 里会被拆成几个核心算子:底层是学生表和成绩表的扫描算子,往上是一个条件过滤,再往上是连接算子,最后是投影算子。如果你在日志里打开了执行计划打印,会看到算子之间的层级关系。物理算子的排列顺序和 SQL 书写顺序并不完全一致,连接条件、过滤下推这些优化会改变算子树的形态。MiniOB 的优化器远没有商业数据库那么激进,但基本的下推和重排逻辑是有的。

我读物理计划时有个习惯:先把常用的算子列成一张小表,标出它们各自的职责和输入输出。这样在算子树里看到一个节点,就能立刻知道它在做什么。

算子职责输入输出
表扫描读取表中全部记录表与谓词条件满足条件的行
索引扫描通过索引定位记录索引与键值命中的行
嵌套循环连接对两个输入做笛卡尔积过滤左右两个算子连接后的行
投影只保留指定列子算子投影后的行
过滤按谓词条件筛选子算子满足条件的行

这张表不需要背,但对照它读物理计划生成代码,你会发现 MiniOB 的每个算子都有明确的职责边界,没有跨层越界的行为,这正是教学系统最难得的优点。

3.3 执行器调度:为什么每条 SQL 都绕不开那个分发函数

物理计划生成之后,执行阶段需要有一个统一的入口,根据命令类型把计划交给对应的执行逻辑。MiniOB 在这里使用了一个分发函数,它会从语法树里取出命令类型,然后 switch 到对应的执行分支。这个函数是所有 SQL 的必经之路,也是我们做二次开发时最容易挂勾子的地方。

// 使用 open/next/close 接口示意物理算子骨架 class Operator { public: virtual int open() = 0; virtual int next() = 0; // 返回 0 表示还有下一行 virtual int close() = 0; };

MiniOB 的算子抽象与这个骨架一致,类名和具体方法名略有差异,但调用模式基本相同。理解了这个骨架,你就能明白为什么数据库执行器这么容易做单算子测试:每个算子都是独立的,给它一个 mock 输入,调用 open 和 next,就能验证输出是否符合预期。

读这章代码时有一个小技巧:先在分发函数里打断点,然后执行一条最简单的 select,观察调用栈。栈上能看到从网络收包、命令处理、语法解析、物理计划生成到算子执行的完整路径。这个调用栈就是你要建立的全链路图,比任何文档都准确。

分发函数这一站也暴露了 MiniOB 的一个特点:它很多地方的处理是顺序直白的,优化空间留给读者自己动手。比如某些版本里简单查询和带条件查询走了几乎相同的路径,索引扫描是否启用完全取决于优化器有没有判断出可用的索引。这正好给了我们在最后一章做实验的切入点。

4. 翻开存储与事务:表、缓冲池和 MVCC 的取舍

解析和执行器只是数据库的脸面,真正的数据要落在磁盘上。MiniOB 的存储层相对精简,但它把表、页、槽位、缓冲池、事务这些核心概念都串起来了,读清楚这一层才算摸到数据库的地基。

4.1 表即文件:记录、页和槽位的关系

MiniOB 中的一张表,本质上对应一个数据文件。文件内部按页划分,每一页是读写的最小单位。记录存放在页内的槽位上,页头记录槽位数量和空闲空间的位置,空闲区从页尾向前生长,槽位数组从页头向后生长。删除记录时通常不是物理擦除,而是把对应槽位标记为空,这样后续插入可以复用位置。

// 示意页头结构,具体字段以源码里的 PAGE_SIZE 与页内布局为准 struct PageHeader { uint32_t slot_count; // 当前页内的记录槽位数量 uint32_t free_begin; // 空闲区起始偏移,从页头向页尾方向增长 uint32_t free_end; // 空闲区结束偏移,从页尾向页头方向收缩 };

上面的结构不是 MiniOB 的精确源码,而是常见的页内组织方式。页内的空闲区是双向逼近的,插入记录时从free_end向free_begin方向压,分配槽位时从free_begin向free_end方向推。两个方向相遇说明页面满了。理解了这个布局,你就能明白为什么 delete 操作不会立刻让文件变小:它只是释放了页内槽位,页本身还在文件里占着位置。

表与页的关系理清之后,再看表结构就顺了。MiniOB 的表由 schema 描述,schema 里记录字段名、字段类型、字段长度、是否可空等信息。插入记录时,执行器按 schema 将各字段编码到一个连续的字节区域里,再按上述页内布局写入。这一层代码没有太多玄学,主要是在围绕偏移量计算做处理,读的时候准备一支笔在纸上画页的布局会更清楚。

这里有个值得留意的边界:char 类型的字段长度是定义时就固定的,写入字符串不足定义长度时通常会用空白补齐。这意味着where name = 'abc'这种比较可能和你预想的不一样,因为存储里实际是补满空格后的值。

4.2 缓冲池:一个周期性落盘的内存层

MiniOB 的存储层不会每次读写都直接操作磁盘,而是经过一层缓冲池。缓冲池按页管理内存,读页时先看目标页是否已在内存,不在则从磁盘调入。页面被修改后会标记为脏页,等待落盘。落盘策略可能是直接写回,也可能是延迟批量写,这取决于实现版本的配置。

缓冲池的容量通常是可配置的,配置值直接影响内存占用和磁盘 IO 次数。如果缓冲池太小,表扫描会频繁换页,整体性能呈断崖式下降;如果开得过大,又可能挤占系统内存,导致编译环境和测试环境一起卡顿。我一般先把配置调到一个适中值跑通功能,再做性能实验时一次性加大,这样才能对比出效果。

读缓冲池代码时,重点观察两点:页面淘汰策略怎么选 victim 页面,以及脏页落盘的时机。MiniOB 的教学实现通常是策略简单、直白,但正因为简单才适合用来对照教科书里的 LRU、Clock 算法。看懂了基础的实现,再去读商业数据库的缓冲池代码,认知落差会小很多。

4.3 MiniOB 的 MVCC 到底做了什么,没做什么

事务模块是 MiniOB 里改动空间比较大的部分。它支持 begin、commit、rollback,这是最基本的会话控制能力。在并发可见性上,MiniOB 引入了 MVCC 概念,让不同事务去读同一个记录时,通过版本信息判断哪些行对当前事务可见。写操作和读操作之间不会互相阻塞,这是它接近真实数据库的地方。

# 终端 A begin; insert into student values(2, 'lisi'); # 此时先不 commit,切到终端 B 观察 # 终端 B select * from student; # 如果 MVCC 生效,应该看不到终端 A 未提交的数据

如果上面这段操作你执行后,终端 B 能立刻看到数据,说明当前版本的隔离级别设置得比较宽松,或者你连接的两个会话实际上共享了同一个事务上下文。MiniOB 各版本的 MVCC 实现差异比较大,有的按事务快照判断可见性,有的在存储层维护了额外的版本链。读代码时把重点放在"事务号怎么生成、读操作怎么判断可见性"这两个问题上,而不是纠结具体类名。

同时也要清楚 MiniOB 没做什么。它没有商业数据库那套复杂的锁矩阵,没有意向锁和间隙锁,对死锁的处理通常很有限。这意味着你拿它做高并发压测,很多在商业数据库上不会出现的问题都会暴露出来。这恰恰是教学系统的价值所在——先在小实现里撞见问题,再去理解大系统里为什么要付出那么大的代价做锁管理。

5. 运行与扩展 MiniOB 的避坑记录:现象、原因、解法

无论你是跑测试还是改代码,MiniOB 都会用各种奇怪的现象教你做人。下面这五条是我反复见到的坑,也是我每次搭 MiniOB 环境时一定会提前说明的注意事项。

5.1 现象:insert 成功但另一边查不到

客户端执行 insert 返回了成功,看起来没有报错,但切到另一个会话去 select,记录就是不在。这个问题的原因是事务隔离和可见性策略在起作用。MiniOB 里如果显式执行了begin,后续操作都处在一个事务里,只有commit之后数据才对其他会话可见。

解决方法是先确认当前会话是否已经开启了事务,再确认提交动作是否真的成功。排查时可以故意在多个 session 里交替执行查询,观察结果差异。另一个隐蔽原因是:你在测试脚本里误把begin和insert放在同一个事务里,但这个事务一直没有 commit,脚本结束连接断开,事务回滚,所以数据丢了。

5.2 现象:delete 后文件大小不变

删掉了表里的全部记录,但查看数据文件,体积几乎没变。原因我们在存储章节已经提过:delete 只是释放页内槽位,页面本身还在数据文件里,文件不会自动收缩。从查询功能上看数据确实没了,但从磁盘占用上看文件依然庞大。

解决方法是理解这种设计在真实数据库里也常见,商业数据库同样要靠重建表或碎片整理来回收空间。如果你想强制收缩,可以创建一个同结构的新表,把残留数据导入后重建,或者直接 drop 掉旧表重建。执行批量删除后再做表扫描,性能变差也和页内碎片有关,这是正常的。

5.3 现象:日志刷屏却找不到关键报错

MiniOB 的日志输出比较直白,有时一条普通查询会在终端打出大量信息,真正出错时,错误信息被淹没在刷屏日志里。原因通常是日志级别配置过低,所有调试信息都直接输出。

解决办法是先调整配置文件里的日志级别,把级别调高,让调试信息关掉,只保留 warning 和 error。定位问题阶段需要更多上下文时,再调低级别并配合 grep 过滤关键字。我调试时会用./observer -f ../etc/observer.ini 2>&1 | grep -i error这种管道方式,先把终端噪音排除掉。

5.4 现象:用 MySQL 客户端习惯连上后执行报错

很多同学一连接成功就开始敲create database xxx,结果直接报语法错误。原因是 MiniOB 只支持很小一个 SQL 方言子集,它不支持数据库级对象概念,也没有use database操作。整个实例就是一个大库,表直接在里面创建。

解决方法很简单:抛开 MySQL 的习惯,先看测试用例里的 SQL 写法,以 MiniOB 的方言为准。它支持的建表、插入、查询、索引等语句和标准 SQL 很像,但细节差异不少,比如某些注释语法、多行插入方式都不一定支持。遇到语法报错时不要硬改 SQL,去 parser 文件里找对应规则。

5.5 现象:调试时看到一堆奇怪的类名和编号后缀

打开核心头文件,发现很多符号带着模板和内部编号后缀,gdb 里打断点时找不到预期函数名。这个问题的根因是模板展开和编译器生成符号的命名规则,并不是源码真的混乱。

解决办法是别盯着符号名去理解逻辑,用调用栈倒推函数所在文件。gdb 里先在主程序入口或命令分发函数下断点,执行一条 SQL,用bt看完整调用栈,再逐个list跳到源码行。尤其是在 Debug 构建下,符号信息完整,这个方法比翻文件列表高效得多。不要试图在启动阶段就打断点去追踪整个启动流程,那会绕进大量底层初始化细节。

6. 把 MiniOB 当实验田:三个我建议你立刻试的进阶玩法

读通源码和改过源码是完全不同的能力。前面五章讲的都是地基,最后给三个我反复调试后觉得性价比最高的实验方向,每个都能让你对 MiniOB 的理解上一个台阶。

第一个玩法是给算子加打点钩子。在执行器的 open、next、close 三处埋上耗时统计和行数统计,然后跑同样的 SQL,你就能看到一张算子级耗时分布。你会发现很多时间消耗在表扫描和逐行过滤上,这个结果会直观地告诉你为什么索引那么重要。常见做法是在执行器外层包一层带计时的包装算子,不改动原有执行逻辑。

# 回归对拍:同一组 SQL,在两个构建版本上跑,对比输出 baseline/build/observer -f etc/observer.ini & mysql -h 127.0.0.1 -P 6789 -u root < test/sql/basic.sql > out_baseline.txt new_build/observer -f etc/observer.ini & mysql -h 127.0.0.1 -P 6789 -u root < test/sql/basic.sql > out_new.txt diff out_baseline.txt out_new.txt

第二个玩法是建立随机 SQL 对拍回归集。准备一批覆盖建表、插入、删除、查询、索引的 SQL,在修改前和修改后各跑一遍,用 diff 对比输出。MiniOB 的改动很容易影响查询结果,比如改了页内布局后记录顺序变化、字段补齐规则变化,这些不通过回归根本发现不了。我一般会把 SQL 按场景拆成多个文件,每个场景一个文件,哪个场景挂了立刻就能定位到模块。

第三个玩法是回到 gdb,断点下在事务可见性判断的路口。分别在两个会话里开启事务,在一个会话未提交时,到另一个会话的查询路径上观察可见性判断结果。这是理解 MVCC 最快的方式,比单纯读代码有效得多——你会在断点处亲眼看到"这条记录为什么对当前事务不可见"的每一步判断。

我做这些实验掉过的最大教训是:不要一次性改很多地方。每次只改一个点,跑对拍,通过后再动下一个。MiniOB 代码规模小,牵一发动全身,几个改动叠加之后,出问题你根本分不清是哪一处引入的。保持小步验证这个习惯,后面做索引、做算子优化都会顺很多。希望帮到你。

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

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

多智能体情感分析在教育评价系统中的应用与架构实践

简介&#xff1a;这是一套面向在线教育平台评价场景的多智能体情感分析系统&#xff0c;基于CrewAI框架实现多个专业化角色协同分析学习者评论&#xff0c;并输出课程质量评估结果。系统内置登录鉴权、任务选择、数据文件上传、情感分析、结果对话与历史记录查看等模块&#xf…

作者头像 李华
网站建设 2026/10/9 11:48:07

JSFiddle嵌入失败原因与三种合规解决方案

1. 为什么直接复制 JSFiddle 的“分享链接”永远嵌不进你的页面你肯定试过&#xff1a;在 JSFiddle 上写好一个炫酷的轮播图、一个实时验证的表单&#xff0c;或者一段带动画的 SVG 图标&#xff0c;点开右上角的Share→ 复制那个https://jsfiddle.net/xxxxx/链接&#xff0c;然…

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

client、offset、style 三大 DOM 属性详解:坐标系、读写规则与选型指南

1. 三个属性到底在操作什么client、offset、style这三个词放在一起&#xff0c;几乎每个写过前端的人都在面试题或者实际项目里撞见过。它们看起来都是“获取某个值”&#xff0c;但背后的坐标系、参照物、可读写性完全不同。我见过太多人写拖拽组件时把offsetX和clientX混着用…

作者头像 李华
网站建设 2026/10/9 11:45:39

蓝桥杯既约分数题解:从暴力枚举到欧拉函数线性筛优化

1. 从一道填空题看"既约分数"的暴力枚举边界蓝桥杯2020年初赛有一道填空题&#xff0c;题目编号1509&#xff0c;问的是在1到2020的范围内&#xff0c;有多少对互质的整数(i, j)&#xff0c;也就是分子分母最大公约数为1的分数有多少个。这道题看起来简单到令人发指—…

作者头像 李华
网站建设 2026/10/9 11:44:00

Bonmin混合整数非线性规划:从源码编译到MINLP求解实战

简介&#xff1a;Bonmin-master 是面向运筹优化、工程计算与科研开发者的开源混合整数非线性规划求解库源码包&#xff0c;适合需要处理整数约束与非线性函数耦合问题的中高级用户。Bonmin 基于 LP/NLP 的分支定界算法&#xff0c;将问题逐步分支并估计上下界&#xff0c;以缩小…

作者头像 李华
网站建设 2026/10/9 11:43:24

VC6.0下用ODBC访问Access数据库:从环境配置到避坑指南

简介&#xff1a;基于VC6.0与MFC的ODBC访问Access数据库示例工程&#xff0c;内含一个学生信息管理系统&#xff0c;适合C初学者或需要掌握数据库编程的开发者&#xff0c;用于学习通过ODBC统一接口连接Access并执行增删改查的完整流程。压缩包共278个文件&#xff0c;以C头文件…

作者头像 李华