暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

手写 SQL 数据库,吊打面试官!

本文是《从零实现 SQL 数据库》课程的加餐文章,描述如何在简历中写这个项目,以及一些可能的面试问题&答案,对课程感兴趣的同学可以查看课程主页: https://roseduan.cn/course/


一、简历上如何写这个项目?

项目名称:sqldb: 极简关系型 SQL 数据库

项目概述:sqldb 是使用 Rust 实现的简易关系型 SQL 数据库,支持常见的表管理、SQL 数据查询与存储等功能,支持 ACID 的事务特性,帮助理解生产级关系型数据库的架构原理和设计细节,以及代码层面的具体实现。

项目架构:主要分为了计算层和存储层,存储层支持基于磁盘和内存的存储引擎,并且在存储之上,使用快照隔离的方法实现了 ACID 事务。计算层按照标准的 SQL 数据库架构实现,主要有 SQL 解析、执行计划和执行器。执行器和存储事务层的读写接口进行交互,实现数据的具体存储和查询。

设计细节:

  • SQL 的词法和语法解析器
  • 执行计划节点构建
  • 执行引擎的执行逻辑
  • 基于内存和磁盘的存储设计
  • ACID 事务的设计
  • 表达式计算
  • 可视化命令行

项目功能:支持了常见的 SQL 语法,包含:

  • 创建和删除表:Create Table、Drop Table 语法,并且支持 Integer、String 等常见数据类型,支持 (Not) Null、Default、Index 等建表约束
  • DML:数据插入 Insert Into、数据删除 Delete From、数据更新 Update,数据查询 Select,select 支持投影、排序、分组、limit/offset、where 过滤、agg 聚集函数、join 子句
  • 事务:支持 Begin、Commit、Rollback 语句
  • 其他语句:比如 Show Tables  查看表结构,Explain 语句输出 SQL 执行计划

二、常见面试问题

1. 请简单介绍一下这个项目

结合上面的项目概述、架构、功能中的描述回答即可。

2. 为什么使用 Rust 来实现?

选择 Rust 主要是因为 Rust 具有出色的性能和内存管理能力。作为系统级编程语言,Rust 提供了安全性和高效性,Rust 的所有权模型和内存管理可以帮助避免传统 C/C++ 编程中的内存泄漏和数据竞争问题。此外,Rust 还具有现代化的工具链,支持高效的编译和静态分析,这对构建高性能数据库是非常重要的。

3. 项目中的存储引擎具体是怎么设计的?

存储引擎主要是有内存和磁盘,对外提供统一的读写接口。

内存使用 BTree,主要是使用了 Rust 标准库中的 BtreeMap。

磁盘存储基于简单的日志追加式的 bitcask 存储模型,bitcask 模型是一个高效、简洁的存储引擎,数据以日志的方式写到磁盘文件中,内存中维护了 key 和对应的索引信息。读取的时候直根据 key 从内存中获取到对应的索引,然后根据索引读取磁盘文件中的数据即可。

4. ACID 的事务具体是怎么实现的?

sqldb 中的事务基于快照隔离实现,主要有以下具体的设计细节:

数据快照:每个事务在开始时会获得一个快照。这意味着事务只会看到在事务开始时已提交的数据,不会看到其他事务的未提交修改。即使其他事务修改了数据,当前事务仍然只看到自己开始时的数据版本。

多版本并发控制(MVCC):每个数据项维护多个版本(即多版本并发控制)。每次数据修改都会创建一个新的版本,事务通过其开始时的快照读取相应的版本,每个版本会关联一个全局唯一的事务版本号。

读写冲突检测:快照隔离会防止并发冲突(如脏读、不可重复读、幻读)。当一个事务修改了数据并提交后,其他事务无法看到其未提交的修改。如果两个事务同时读取相同的数据项并尝试修改它们,会检测到冲突并使其中一个事务回滚,避免不一致的结果。

5. 这种设计是怎么保证 ACID 特性的?

  • A 原子性:通过事务写入数据,只有调用 Commit 之后,才会生效,否则已经写入的中间数据是无效的。要么调用 Rollback 进行回滚,所以一个事务中的数据要么提交,要么回滚,保证了原子性。
  • C 一致性:一致性保证数据库从一个有效的状态转换到另一个有效的状态,在原子性和隔离性都满足的情况下,一致性也能够得到保证。
  • I 隔离性:主要是快照隔离,通过 MVCC 多版本并发控制,以及数据冲突检测机制,能够避免脏读,不可重复读,幻读的问题。是一种介于传统的串行化和可重复读之间的隔离级别。
    • 脏读:事务在开始的时候获取了一个全局快照,只能读到对当前事务可见的数据,其他未提交的事务不能读到,避免了脏读。
    • 不可重复读:通过全局快照,其他事务的修改就算提交了,也不会对当前事务可见,也就是说事务在运行的过程中,始终只能看到其开始时的状态,解决了不可重复读问题。
    • 幻读:事务在读取的时候,如果事务版本号比当面版本号更大,说明是更加新的事务提交的数据,也不可见,解决了幻读的问题。
  • D 持久性:通过磁盘存储引擎,数据写入之后,能够安全的保存到持久化存储中。

6. SQL 语句是怎么解析并执行的?

SQL 解析:首先使用词法分析器(Lexer)和语法分析器(Parser)将 SQL 查询转换成一个抽象语法树(AST)。词法分析器将 SQL 语句分割成各个语法单元(例如关键字、标识符 Token、运算符),语法分析器 Parser 则检查 SQL 语句是否符合 SQL 语法规范,并生成抽象语法树 AST。

执行计划生成:根据 AST 构建执行计划。执行计划定义了如何访问和处理数据库中的数据。数据的增删改都对应了具体的执行节点,例如 Select 会决定使用索引还是全表扫描,或者如何使用 JOIN、Group By 等操作。

执行器:执行器根据执行计划访问数据库中的数据,执行相应的操作。执行器会与存储引擎交互,从内存或磁盘中读取数据,并根据操作修改数据。

7. SQL 解析时,如何处理不同类型的 SQL 语法?

SQL 解析主要在词法分析和语法分析阶段完成,在 Lexer 中将 SQL 拆分为不同各个语法单元,例如关键字、Token、运算法等。在 Parser 阶段会根据预定义的语法规则进行解析,如果不符合预期的话,则直接报出对应的语法错误。如果是一个有效的 SQL 语句则生成对应的抽象语法树。

8. 如何保证数据的崩溃恢复?

sqldb 中实现了基于磁盘的存储引擎,数据写入的时候会首先写入到磁盘存储引擎中,这能够保证持久性。如果在写入的过程中发生了异常,由于事务原子性机制的保证,中间状态的数据并不会生效,因此能够有效的保证数据库恢复到正确的状态。

9. 如何处理表达式计算?

sqldb 中使用了基于运算符优先级的算法 Precedence Climbing
 来进行表达式计算,它本质上是一种递归下降解析表达式的方法,通过递归地处理运算符和操作数来解析表达式,并根据运算符的优先级和结合性来确定表达式的计算顺序。

这种算法的核心思想是利用运算符的优先级进行“爬升”(Climbing),以决定表达式的结构和计算顺序。

在 sqldb 当中,由于运算符众多,支持了几种最常用的运算符加减乘除。

  • 如果面试官要求代码演示,可以参考:https://github.com/rosedblabs/rust-practice/tree/main/expr-eval

10. 如何支持 SQL 语法中的聚合函数和 Join 操作?

聚合函数(如 SUM、AVG、COUNT)在执行引擎中通过遍历结果集并进行相应的计算来实现。在执行查询时,执行引擎会根据执行计划确定是否需要对结果集进行分组,并计算聚合结果。

对于 JOIN 操作,执行引擎会根据查询类型选择合适的连接算法(如 Nested Loop Join 或 Hash Join)来完成数据表的连接操作。

11. 建表时既然支持 Index,那么索引是如何实现的?

sqldb 中的索引实现比较简单,借助 KV 存储引擎的底层结构,实现了类似哈希索引的功能。在实现上,主要是针对索引列,单独存储了一个 Key/Value 对,key 就是表名+索引列数据,value 是主键 id。这样在通过索引列进行查询的时候,能够快速定位到具体的数据,并且取出其中的主键 id 信息,然后再根据主键 id 获取数据。

并且在数据进行增删改的时候,如果索引列也进行了更新,那么也需要对索引的数据进行维护。

12. 如果要将这个数据库扩展成一个生产级别的数据库,你会考虑哪些方面?

要将这个数据库扩展成生产级别的数据库,可以从以下几个方面进行改进:

  1. 完善 SQL 语法:增加更多样化的 SQL 语法,支持更加复杂的查询场景
  2. 性能优化:增加查询缓存、优化索引结构、增加并行查询处理等,以提高性能。
  3. 高可用性和容错:实现数据的自动备份、故障恢复和数据复制机制,确保高可用性。
  4. 安全性:增加访问控制、加密、审计日志等功能,确保数据安全性。
  5. 易用性:完善命令行工具,提供图形化界面,增强用户友好性。

文章转载自roseduan写字的地方,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论