注: 文中讲述的原理是推理和探讨 , 和现实中的实现不一定完全相同 。
数据库呢 , 主要分为 5 大部分 ,
1 Sql 分析器
2 查询(更新)计划器
3 数据存储检索
4 优化策略
5 事务(Transaction)
第一个部分 Sql 分析器 呢 , 涉及到 编译原理 语法分析 的 知识 和 关系运算 的 知识 , 但这并不难 , 我写了一个项目 SelectDataTable , 可以解析简单的 Sql 语句 , 通过 Sql 语句在 DataTable 中查询数据 , 可以参考 : https://www.cnblogs.com/KSongKing/p/9455216.html
第二个部分 查询(更新)计划器 , 这个部分就是把 Sql 解析的结果 转换为 数据存储检索的 指令 。
第三个部分 数据存储检索 , 就是 数据如何在磁盘上存储和检索 。 我们来详细谈一下这个部分 。
数据 在 磁盘上 存储检索 的 基础 , 是 数据块(Data Block) , 就是说 , 把要存储的数据分成一个一个的 数据块 。 比如 , 我们可以定义数据块的大小是 4K 。
那么 , 在数据库里,数据是以 表 和 表记录 的 形式存在的 , 那么就把表记录放到 数据块 里 存储 。 当然 一笔表记录 的 大小 不能超过 数据块的大小 。
那么如何 检索 呢 ? 将 数据块 从 磁盘读取到 内存 , 在内存里进行检索 。
如何 更新 呢 ? 如果 数据 所在的 数据块 已经在 内存 里 , 就先对 内存里的数据块更新, 在 适当的时候 再批量更新到 磁盘 上 。 如果 数据 不在内存里 , 需要直接更新磁盘 。 从这里可以看出来 , 更新 可能 频繁 写磁盘 , 需要 频繁移动 磁头 , 在 固态硬盘 的 时代 , 这个问题可能会改善很多 。 另外也可以看出来 , 如果 内存 足够大 , 那么可以把 大量的数据 加载到 内存 里 在内存里 查询 更新 , 在适当的时候才 批量 写入 磁盘 , 这样处理速度可以加快 。 换句话说 , 内存 的 充分 对于 数据库 效率 很重要 。 实际的经验中 , 看到的情况大致也是这样 。 ^ ^ 有充分的内存 , 数据库 可以把 整张表的资料 和 索引 都 加载到 内存 , 这样 查询 和 更新 的 速度 是很快的 。 而 经验中 也经常会有这样的经验 : 第一次查询的时候会比较慢 , 后面就快了 。 实际上 就跟 数据库 加载 数据 到 内存 的 这个 原理有关 。
但上面说的有一点也不对 。 如果 数据 已经在 内存 里 , 那么更新了 内存 里的数据后 , 应立即更新 磁盘 上的数据 。 不然如果 服务器 突然断电 , 数据就丢失了 。 对于 客户端 来说 , 执行 insert update delete 成功后 , 就意味着 数据 已经 持久化 。
数据库 通常 会把 数据 存放在一个 文件 里 。 比如 Sql Server 。 通过 FileStream 的 Position 属性 , 我们可以 指定位置 写入 和 读取 数据块 , 以及 指定位置 直接更新 数据块 里的 数据 。 这样 , 文件就可以看作一块 地址空间 , 就像 内存 一样 , 可以像 管理 内存 一样 管理 。 当然 , 这是从 地址 这个角度来看是这样 。 从 硬件属性 来看 , 还是要考虑 磁盘 的 机械读写 的 特性 , 顺序读写 的 效率 比 随机读写 好 , 所以 据说 B Tree 索引 就是 顺序存储 索引 的 , 而 B Tree 是使用最广泛的 索引 了 吧 !
但 总的来说 , 固态硬盘 的 出现 , 会使这些问题 改善 很多 。
第四个部分 , 优化策略 主要是 临时索引 和 并行计算 等 。 临时索引 是 很有用的 , 它可以使 数据库 变得 “傻瓜化” , 不需要刻意的去设计和建立索引 , 就可以获得高效的查询性能 。 另外 , 完全依靠人工设计和建立索引也是很大的工作量 , 同时 , 固定的索引会在每次更新表时都要更新索引 , 同时索引会一直占用存储空间 , 所以 临时索引 还让 数据库 的 使用 轻松 灵活 了 。
另外就是 并行计算 , 并行计算 看起来 很诱人,很美好 , 但是仔细想想好像不是那么回事 。 数据库 通常处于 并发的场景下 。 在 高并发 下, 每个 CPU 核 都会处理 n 个 请求 , 如果还要把每个请求的查询任务分成若干个任务并行执行 , 好像意义不大 。
第五个部分 , 事务 是 数据库 的 重头戏 。 事务 通过 事务日志(Transaction Log) 实现 。 当一个事务开始时 , 首先会在事务日志中记录该事务已开始 , 并且只有在事务日志中记录日志成功 , 才会开始下一步的操作 。 对于事务来讲 , 为了保证 数据完整性 , 或者说 ACID , 需要这样严谨的进行 。 可以说是 “环环相扣” 。 接下来就开始执行更新操作 , 每一个更新操作 , 会 分为 3 个 步骤 : 1 在事务日志中记录 Begin(包括 要执行什么样的 操作 的 信息) , 2 执行更新操作 , 3 在事务日志中记录 End 。 事务完成后 , 会再记录整个事务 End 。 只有到这一步 , 整个事务才算结束 , 更新才彻底生效 。 正常情况下 , 如果需要回滚 , 可以根据 事务日志 来 回滚 , 这容易理解 , 就不详细描述了 。 在异常情况下 , 比如 服务器 突然断电 , 在这样的情况下 , 要如何处理 , 才能使 数据 正确呢 ? 数据库 在 重新启动 时 , 会检查 事务日志 , 会发现 未完成的 事务日志(没有记录 End 的) , 数据库 会 对 未完成 的 事务 进行 回滚 。
事务 另外一个方面就是 锁(Lock) 。 在 事务 开始时 , 会锁定表 , 这意味着 从现在起 , 不允许对表开始新的操作 , 同时 要求 在当前所有对表的操作(包括 select) 结束后 , 才会开始本次事务的 操作 。 那要怎么才能确定当前对表的操作 都结束了呢 ? 这大概还是需要通过 锁 。 普通的 insert update delete select 也需要获得锁 , 这个 锁 应该是 行级锁 。 insert update delete 应该是 独占锁 , select 可以是 共享锁 。
基本上就这些 。
按照这个原理 , 可以写一个 数据库 。 呵呵呵呵