一、天天用数据库,你却连它怎么找数据都不知道?
每个程序员的职业生涯,都离不开数据库。用户信息、支付记录、产品状态,我们把最核心、最关键的数据,毫无保留地交给它保管,默认它永远可靠、永远高效。
可一旦被问起“数据库到底是怎么找到某一行数据的”,绝大多数人都只会含糊其辞,提一嘴“B树”就匆匆带过,连自己都不信这个答案。我们依赖它、信任它,却对它的底层逻辑一无所知,就像天天用手机,却不知道屏幕背后的芯片如何工作。
这种“知其然不知其所以然”的尴尬, Prem Chandak 也深有体会。但他没有像普通人一样不了了之,而是做了一件惊动技术圈的事:用Rust语言,不依赖任何第三方库、不走任何捷径,从零亲手打造了一个数据库,只为揭开它的“神秘面纱”。
他的尝试,不仅打破了“数据库是魔法”的固有认知,更给所有程序员上了生动一课——那些我们觉得高深莫测的技术,拆解开来,其实每一步都有迹可循。可问题来了:从零造数据库,到底难不难?普通人能通过这种方式,真正搞懂数据库底层吗?
二、核心拆解:从零造数据库,5层架构+Rust代码,一步都不跳
Prem Chandak 的实践证明,数据库没有任何“魔法”,本质是一套层层递进的工程架构。他用Rust打造的简易数据库,完美还原了主流数据库的核心流程,从SQL输入到结果输出,每一层都有明确的分工,缺一不可。下面我们一步步拆解,连核心代码都完整同步,普通人也能跟着理解、尝试。
关键技术补充:Rust与数据库底层的适配性
本次用到的核心技术是Rust语言,它是一款开源、免费的系统级编程语言,在GitHub上拥有超过88万星标,凭借“内存安全、无垃圾回收、高性能”三大优势,成为打造底层系统(包括数据库)的首选语言。与C/C++相比,Rust的所有权模型能避免内存泄漏、数据竞争等底层bug,这也是Prem Chandak 选择它的核心原因——数据库底层容不得一丝马虎,而Rust能从编译阶段就规避大部分致命错误。
第一层:存储引擎——数据库的“硬盘管家”,只认“页面”不认“行”
存储引擎是数据库的基础,负责管理数据在硬盘上的存储方式,也是打破我们固有认知的第一步。很多人以为,执行“SELECT * FROM users WHERE id = 42”时,数据库会逐行扫描找到目标数据,但事实并非如此。
数据库的核心逻辑是:不读单个行,只读“页面”。页面是固定大小的字节块(通常是4KB或8KB),因为硬盘I/O操作成本很高,读取8KB的一块数据,和读取1字节的数据,成本几乎一样——批量读取,才能提升效率。
下面是Prem Chandak 用Rust实现的极简页面代码,通俗易懂,可直接复制运行:
const PAGE_SIZE: usize = 4096;struct Page { data: [u8; PAGE_SIZE],}impl Page { // 新建一个空页面 fn new() -> Self { Page { data: [0u8; PAGE_SIZE] } } // 向页面写入数据 fn write(&mut self, offset: usize, bytes: &[u8]) { self.data[offset..offset + bytes.len()].copy_from_slice(bytes); } // 从页面读取数据 fn read(&self, offset: usize, len: usize) -> &[u8] { &self.data[offset..offset + len] }}除了页面,存储引擎还有一个关键组件——缓冲池。它是内存中的一块缓存,用来存放常用的页面。当需要读取某个页面时,先检查缓冲池,如果存在就直接读取;如果不存在,再从硬盘加载,并替换掉缓冲池中不常用的页面(用LRU算法)。
Rust的所有权模型在这里发挥了巨大作用:它不允许同时出现两个对同一页面的可变引用,避免了多线程操作时的数据混乱,这也是很多C语言写的数据库容易出现生产崩溃的核心原因——而Rust能在编译时就发现这类问题。
第二层:索引——告别全表扫描,1000万行数据也能秒查
如果没有索引,数据库查找数据只能靠“全表扫描”:逐页检查每一行数据,匹配目标条件。这种方式在数据量小时(比如1000行)没问题,但一旦数据量达到1000万行,效率会低到无法接受——相当于在一本没有目录的厚书里找一个字。
主流数据库的解决方案,都是B树索引。它的核心优势是“平衡树结构”,能让查找操作的时间复杂度从O(n)降到O(log n),也就是说,1000万行数据,只需23次比较就能找到目标,而不是逐行扫描1000万次。
B树的结构非常清晰,类似多级目录:
[ 50 ] / \ [ 25 ] [ 75 ] / \ / \ [10,20] [30,40] [60,70] [80,90]
每个节点存放多个键值,树的结构会自动保持平衡。下面是Prem Chandak 实现的极简B树节点代码,核心是“搜索”功能:
struct Node { keys: Vec, // 节点中的键值 children: Vec>, // 子节点 is_leaf: bool, // 是否为叶子节点}impl Node { // 搜索指定键值,返回其位置(索引) fn search(&self, key: i64) -> Option { // 如果是叶子节点,直接查找键值位置 if self.is_leaf { return self.keys.iter().position(|&k| k == key); } // 非叶子节点,找到子节点的索引,递归搜索 let idx = self.keys.partition_point(|&k| k < key); self.children[idx].search(key) }} 这里有个关键难点:B树的“分裂与合并”。当一个节点的键值装满时,需要将其分裂成两个节点,并把中间的键值向上传递;当节点键值过少时,需要和相邻节点合并。这部分逻辑非常复杂,也是Prem Chandak 花了大量时间调试、写测试的部分——一旦逻辑出错,整个B树就会数据 corruption,无法正常工作。
第三层:查询解析器——把SQL字符串,变成机器能懂的“指令”
我们写的SQL语句(比如“SELECT name FROM users WHERE age > 30”),机器是无法直接理解的。查询解析器的作用,就是将SQL字符串转换成机器能处理的“抽象语法树(AST)”,分为两个步骤:词法分析(分词)和语法分析(生成AST)。
第一步:词法分析(Lexer/Tokenizer)——将SQL字符串拆分成一个个“token”(标记),比如:
SELECT → Token::Select name → Token::Ident("name") FROM → Token::From users → Token::Ident("users") WHERE → Token::Where age → Token::Ident("age") > → Token::Gt 30 → Token::Number(30)
第二步:语法分析(Parser)——将这些token组合成AST,也就是数据库的“内部语言”。Prem Chandak 用Rust定义了AST的核心结构,如下:
// 表达式(比如age > 30、name等)enum Expr { Column(String), // 列名(如age、name) Literal(Value), // 常量(如30) BinaryOp { // 二元运算(如>、=、<) left: Box, op: Op, right: Box, },}// SELECT语句的结构struct SelectStmt { cols: Vec, // 要查询的列(如[name]) table: String, // 要查询的表(如users) filter: Option, // 过滤条件(如age > 30)} AST是数据库后续处理的基础,查询优化器、执行引擎,都是基于AST工作,而不是直接处理原始SQL字符串。
第四层:查询执行引擎——按计划执行,把AST变成结果
AST告诉数据库“要做什么”,而查询执行引擎告诉数据库“怎么做”。Prem Chandak 采用了主流数据库(如PostgreSQL)的“火山模型”——每个执行节点都是一个迭代器,上层节点从下层节点“拉取”数据,逐步处理,最终输出结果。
以“SELECT name FROM users WHERE age > 30”为例,执行流程如下:
TableScan("users") → 扫描users表的所有数据 | Filter(age > 30) → 过滤出age大于30的行 | Project(name) → 只保留name列
下面是Rust实现的核心执行器代码,核心是“next”方法,用于逐行获取数据:

// 执行器 trait,所有执行节点都要实现这个 traittrait Executor { fn next(&mut self) -> Option;}// 过滤执行节点:筛选符合条件的行struct Filter { src: E, // 下层执行节点(如TableScan) pred: Expr, // 过滤条件(如age > 30)}impl Executor for Filter { fn next(&mut self) -> Option { loop { // 从下层节点获取一行数据 let row = self.src.next()?; // 验证是否符合过滤条件,符合则返回 if eval(&self.pred, &row) { return Some(row); } } }}
这种设计的优势是“可组合”——可以根据不同的SQL语句,组合出不同的执行流程,比如增加排序、分组等节点,灵活且高效。
第五层:完整流程——从SQL到结果,一步不差
把上面所有层组合起来,就是一个简易数据库的完整工作流程,没有任何魔法,每一步都清晰可见:
"SELECT name FROM users WHERE age > 30" | [ 词法分析 + 语法分析 ] | [ AST抽象语法树 ] | [ 查询优化器(选择索引/全表扫描) ] | [ 执行引擎:扫描 → 过滤 → 投影 ] | [ 缓冲池 + B树索引 ] | [ 硬盘上的页面数据 ] | [ 最终查询结果 ]
三、辩证分析:从零造数据库,是无用功还是真修行?
Prem Chandak 的尝试,收获了大量技术圈的认可,很多程序员表示“看完瞬间懂了数据库底层”,但也有不少人质疑:“有现成的PostgreSQL、MySQL,从零造数据库根本没有实际意义,纯属浪费时间”。
不可否认,从实用性来看,个人从零打造的数据库,无论是性能、稳定性,还是功能丰富度,都远不及主流数据库,更不可能用于生产环境——毕竟主流数据库经过了十几年的迭代、亿万用户的验证,背后是几十上百人的研发团队。 Prem Chandak 自己也承认,他打造的只是一个“玩具级”数据库,目的不是替代主流数据库,而是理解其底层逻辑。
但换个角度想,这种“无用功”,恰恰是程序员成长的关键。很多时候,我们陷入“只会用,不会懂”的困境,不是因为技术太难,而是因为我们懒得去拆解、去实践。就像很多人天天用框架,却不知道框架底层如何实现,一旦遇到底层bug,就只能束手无策。
更重要的是,从零造数据库的过程,能倒逼我们掌握核心技术:Rust的内存管理、B树的实现逻辑、SQL的解析流程、执行引擎的设计思路——这些知识,不是靠看文档、看教程就能真正掌握的,只有亲手实践,才能融会贯通。那么问题来了,对于普通程序员来说,到底有没有必要花时间,去从零实现一个数据库?
四、现实意义:搞懂底层,才是程序员的“核心竞争力”
Prem Chandak 的实践,给所有程序员提了一个醒:在这个“框架泛滥”的时代,懂底层,才能走得更远。他的数据库虽然简单,但带来的收获,远比我们想象的更多,这也是其背后的现实意义。
首先,解决实际工作中的“疑难杂症”。很多程序员工作中会遇到“查询变慢”“数据异常”等问题,此时如果不懂底层,只能靠猜、靠查Stack Overflow,效率低下且不一定能解决问题。但如果懂数据库的底层逻辑,就能快速定位问题:是没有命中索引?是查询计划不合理?还是存储引擎出了问题? Prem Chandak 自己也说,做完这个项目后,他再遇到数据库相关的问题,思路会清晰很多。
其次,提升自身的技术壁垒。现在的程序员,会用框架、会写SQL的人一抓一大把,但能懂底层、能从零实现核心组件的人,却寥寥无几。从零造数据库的过程,本质是锻炼自己的逻辑思维、编码能力和问题解决能力——这些能力,无论在哪个技术领域,都是核心竞争力,也是区别于“普通程序员”和“高级程序员”的关键。
最后,打破“技术恐惧”。很多人觉得数据库、操作系统等底层技术“高深莫测”,不敢去触碰。但Prem Chandak 的实践证明,只要一步步拆解、一点点实践,再难的技术,也能被掌握。他的数据库,没有用任何高深的黑科技,都是最基础的代码和逻辑,普通人只要愿意花时间,也能实现。
还有一个关键数据,能直观体现底层优化的价值:Prem Chandak 对自己的玩具数据库做了测试,读取100万行数据,全表扫描需要约4200毫秒,而用B树索引查找,只需要约18毫秒——差距高达230倍。这个数据,也让我们真正理解了“索引”的意义,而不是只停留在“索引能加快查询”的表面认知。
五、互动话题:你觉得,程序员有必要从零造一个数据库吗?
看完Prem Chandak 的实践,相信很多程序员都有自己的想法。有人觉得,这是最有效的学习方式,能快速吃透数据库底层,提升自己;也有人觉得,纯粹是浪费时间,不如把精力放在主流数据库的优化和使用上,毕竟工作中用不到自己造的数据库。
其实,没有绝对的“对”与“错”,关键在于你的学习目标。如果你想提升底层能力、打破技术瓶颈,那么从零造一个简单的数据库,绝对是值得的;如果你只是想做好日常开发,熟练使用主流数据库,那么专注于应用层面,也无可厚非。
不妨在评论区留下你的观点:你觉得程序员有必要从零造一个数据库吗?你平时工作中,有没有遇到过因为不懂底层而解决不了的问题?你最想亲手实现的底层组件是什么?