• 一个 .Net 开源 数据库 项目 SqlNet


    大家好 , 我发起了一个 .Net 开源 数据库 项目 SqlNet 。

    项目计划 是 用 C# 写一个 关系数据库 。

    可以先参考我之前写的 2 篇文章 :    

    谈谈数据库原理    https://www.cnblogs.com/KSongKing/p/9492315.html

    论数据库 B Tree 索引在固态硬盘上的离散存储    https://www.cnblogs.com/KSongKing/p/9501686.html

    根据 上面说的 , SqlNet 中的 表数据 和 索引数据 的 存储 打算使用 链式存储(离散存储) 。

    但这样的做法是 存在一些 风险 或者说 可能的问题 的 。

    因为 现有的     文件流  驱动程序  硬件控制指令  硬件控制电路     都是 基于 顺序读写 的 模式 来 设计 的 , 所以用 顺序读写 架构下的 指令 来 实现 随机读写 , 效率应该会有所降低 。 简单的 , 我们可以这样看 , 在现有的架构下 , 需要 2 个步骤来完成 1 次 随机读写 : 1 设定流位置(Position) , 2 读写 。 而真正的 随机读写 应该是像 读写内存 一样 在一个 指令里 指定 地址 + 数据 , 1 个指令就完成对指定地址的读写 。

    这中间可能还有很多细节 , 不过这个要测试起来应该很麻烦 , 而且自己测试不一定准确 , 所以懒得测了 , 就按照这个设计开始 吧 。  ^ ^

    我们可以参考这篇文章 :http://ssd.zol.com.cn/608/6082318.html  

    这篇文章是 固态硬盘 性能测试报告 , 包括了 连续读 连续写 随机读 随机写 , 其中所说的 “Intel 600P” 的 连续读可以达到 1800M/s ,连续写可以达到 500M/s , 随机读接近 480M/s , 随机写接近 400M/s 。  所以 。

    文中的 随机读 和 随机写 是 指 “4K随机读” 和 “4K随机写” , 就是说是以 4K 为基本单位 进行 随机读写 , 而我们的 数据库 的 随机读写 的 单位在 insert 的时候是 行 。一个栏位比较多的行 , 数据量可能是 1K , 如果 栏位比较多 , 栏位里的内容也比较长(比如 字符串比较长) , 那么也很容易达到 4K  。 所以 4K 作为读写的 最小单位 进行的测试 基本上 跟 我们的 数据库 的 使用场景 也差不多 。 当然 update 和 delete 的 写入 数据量 会 比较小 , update 只写入更i新的栏位数据 , delete 只修改 行 的 上一行 的 Next 指针 , 以及将 本行 标识为 已删除 。

    不过这些我想不是问题 , 理论上, 这些问题在未来都可以解决 。 未来出现 专门用于 固态硬盘 随机读写的    文件流  驱动程序  硬件控制指令  硬件控制电路    就可以了 。

    仔细再一想 , 固态硬盘 做为 外部设备 , 先设置 读写位置(Position) , 再批量读写 , 这个也是合理的 。

    这好像有点绕 , 哈哈哈哈 。

    总之 , 这么说 , 现有的   文件流  驱动程序  硬件控制指令  硬件控制电路   如果对 SqlNet 的支持 还不是 最优 , 那么 , 随着技术的发展 , 是可以得到优化的 。

    要 采用 链式存储 , 就需要 实现 一个 内存堆 分配 的 机制 。 将 数据文件(Data File)看作一个 地址空间 , 在这个地址空间上实现一个 堆 机制 。

    堆机制 可以自己设计 , 不过 先研究一下已有的实现原理 , 比如  C# ,  Java ,  C++  的 。

    采用了 链式存储 , 就不需要使用传统的  数据块(Data Block)的存储方式了 , 当然相对的 , 需要实现一个 堆机制 。

    但是 仔细再一想 , 固态硬盘 是一个 外部设备 , 每一笔资料都要单独读取 , 这个性能消耗 应该会比 连续批量读取 大很多 。

    所以 , 我觉得还是要采用 传统的 Data Block 的方式 。

    实际上 , Data Block 本身就是 线性表 和 链表 两者 的结合 。 Data Block 是一个 线性表 , 多个 Data Block 之间通过 链表 的 方式 连接起来 。

    所以 , 从这里可以看到 , Data Block 的大小(Size)是一个 关键 。 Size 太大 , 则可能浪费过多的磁盘空间 , 同时 insert 时需要向后移动的 行数 也会很多 。

    Size 太小 , 则读取的效率会降低 , 最坏的情况就是 退化 成一个 纯粹的 链表 , 比如 每个 Data Block 只包含 一行 。 这样就又恢复到 “链式存储” 了 。  ^^

    什么情况下 , 每个 Data Block 只包含 一行 呢 ? 比如我们设定每个 Data Block 的大小是 4M , 如果 1行资料 的大小 接近 4M , 那么 , 这个 Data Block 就只能包含 1 行的 资料 。

    所以 , 从这里可以看出 , Data Block 的 Size 需要根据 Table (Schema) 来决定 。 不同的 Table , Data Block Size  是不一样的 。 或者说 , Data Block 应该叫做 “Table Block” 。

    我们在创建 Table 时会指定 Table Schema , 包括 有哪些列 , 列的数据类型 , 根据这些我们可以计算出 一行 所需的最大空间 , 我们设定 , 1 个 Table Block 包含 1024 行 , 那么 , 加入 1 行所需的最大空间是 4K , 那么 , 这张 Table 的 Table Block Size 就应该是 4K * 1024 = 4M 。

    这种做法会造成 存储空间 的浪费 , 因为 比如 字符串类型的数据的长度 是 不定的 , 在 传统的数据库 中有 char , varchar , nchar , nvarchar 等 4 种类型 表示字符串 。

    对于 变长字符串 , 如果要兼顾到   读取  查找  插入  更新    的 效率 的话 , 情况可能比较复杂 。

    不过我们可以先实现简单的实现 , 比如 , 我们可以先只支持 定长的 char 类型 。  

    但 , 这样根据 Table Schema 来决定 Table Block Size 的 做法 也有问题 。 在 行 size 很大时 , 会产生一些问题 。 什么时候 行 size 很大呢 ? 比如 列很多 , 或者 列 size 很大 , 都可能导致 行 size 很大 。 假设 行 size 是 1 M , 根据上面的设定 , 1 个 Table Block 应该有 1024 行 , 1 个 Table Block 的大小就是 1M * 1024 = 1G 。

    1 G 的 Table Block 看起来是挺大的 , 这会导致什么问题呢 ?  

    在 insert 的时候 , 如果没有 聚集索引 , 新增一行 就是将 新行 添加为表的 最后一行 。 如果有聚集索引(比如 主键) , 会将 新行 根据 索引排序 插入到 指定的位置 。 而 插入 会导致 这个 Table Block 内在 这个新行 之后 的 所有 行 都 向后移动 (参考 线性表 的 插入操作)。  

    1 G 的 Table Block 需要向后移动的 数据量 是 很大的 , 如果 新行 插入的位置是比较靠近 Table Block 的 开始位置 , 那么 需要向后移动的 数据 可能 接近 1 G 。  

    还有在 update 的时候 , 对于 长度可变 的 列 , 比如 varchar 或者 nvarchar 的 列 , 新值 如果比 旧值 的 长度 更长 , 同样 会向后移动数据 。 需要移动 本行 的 update 的 列 之后 所有列 的数据 , 以及 本行之后 所有行 的 数据 。 

    同上 , 对于 1 G 的 Table Block , 如果 update 的位置靠近 Table Block 的 开始位置 , 那么 需要向后移动的 数据 可能 接近 1 G 。

    所以 , 我们还是回到 固定大小 的 Table Block , 或者说 Data Block 。  ^^

    对于 固定大小的 Data Block , 首先 1 行的长度不允许超过 Data Block Size 。 那么 , 回到上面提出过的问题 , 当 行 size 比较大时 , 可能 1 个 Data Block 只包含 1 行 , 此时 , 存储结构 将 “退化” 为 一个 链表 。 但仔细一想 , 这并没有关系 , 不管 1 个 Data Block 里包含 几行 , insert 和 update 时 需要移动的 数据 最多接近 Data Block Size 。 假设 Data Block Size 是 1 M , 那么需要移动的数据 最多接近 1 M 。

    对于 读取效率 , 每次读取的 数据 就是 1 个 Data Block , 即 1 M 。

    综上 , 存储结构 的 设计 就 清楚了 , 而 在这个 存储结构 里 , Data Block Size 是一个 关键参数 。

    我想 我们可以设定 Data Block Size 为 1 M 。

    下面 , 我们先来解决 第 1 个 问题 , 索引 。

    为什么 索引 是 第一个 问题呢 ? 索引是高效查询的基础 , 如果 表 有聚集索引(比如 主键) , 那么 聚集索引 的 存储 就是 表数据 的 存储 。 而 主键 是 广泛使用的 , 甚至可以说是 必需 的 (见 三大范式) , 根据主键查询也是广泛使用的 , 所以 索引 是 第 1 个问题 , 可以说是 数据库 的 基础 。 解决了 索引 的 存储检索 问题 , 也就解决了 数据库 的 存储检索 问题 。

    我理解的 B Tree 索引 :

    实际上 , B Tree 索引 所代表的查询原理是一种 普遍的 索引原理 , 为什么叫 “B Tree” , 就不知道了 。  ^^

    B Tree 索引 是一个 树形结构 , 但为了能够从 外部存储器(磁盘)高效的 读取 , 我们需要将 B Tree 索引 顺序的排列起来 , 存放到 Data Block 里 。

    顺序排列 起来 存放到 Data Block 里的 B Tree 索引 如下 :

     

    一个 Data Block 存放满了 , 就存到下一个 Data Block 里, 上文说过 , Data Block 之间通过 链表 的 方式连接起来 。 或者说 , 一张表的数据 , 或者索引 , 就是一个 Data Block 作为元素组成的 链表 。

    B Tree 索引 的 效率如何呢 ? 可以看到 , 上面图中的 B Tree 索引的每个节点(索引项)有 4 个 子节点 , 这大概叫做 “4 阶索引” 。 4 阶索引 的 检索流程 如下 :

    假如要检索的内容是一个中文字符 , 按 Unicode 存储的话 占 2 个字节(Byte) , 对于 4 阶索引来说 , 每次检索 2 位(bit) , 2 位 代表了 4 种情况 : 00 , 01 , 10 , 11 。

    2 个字节包含了 16 位 , 那么就要检索 16/2 = 8 次 , (每一次检索就是检索一个 B Tree 节点(索引项))  。

    如果要检索的内容是一串字符 , 字符的长度是 64 个字节(Byte) , 相当于是 32 个中文 , 那么检索次数是 (64 * 8) / 2 = 64 * 4 = 256 次 。

    所以 B Tree 索引的 时间复杂度 和 行数 无关 , 和 检索内容 的 长度 有关 。 具体的说, B Tree 索引的时间 复杂度 是 O(length * 8 / 2) , length 是 检索内容 的 长度(Byte 数) 。

    每次检索(检索一个 索引项)需要判断 4 种情况 : 00 , 01 , 10 , 11 , 如果 每次检索话费的时间是 4ns (4 纳秒) , 那么查找 32 个中文的字符串的时间就是 256 * 4ns = 1024ns  约等于 1 微秒 。

    以此类推 , 查找长度为 320 个中文字符 的 字符串 的 时间是 1 微秒 * 10 = 10 微秒 。

    查找长度为 3200 个中文字符 的 字符串 的 时间是 1 微秒 * 100 = 100 微秒 。

    Sql Server 中的 nvarchar 类型 长度 可达 4000 , 就是说可以存储 4000 个 中文字符 。 这个可以作为参考 。

    假设我们的数据库中某列 的 长度 平均 是 100 个中文字符 , 用于查找该列的内容 也是 按 平均 100 个中文字符计算 , 按照上面的估算 , 可以估算按照索引查找该列的时间约是 3.4 微秒 , 假设按 4 微秒 算 , 那么 每秒查询次数(QPS)可以达到 25万次 / 秒 , 呵呵呵 , 实际能不能达到这个效果 , 就不知道了 。 需要测试 。

    从这里 , 我们再次体会到 , 测试 是 一个 专业 , 是 和 开发 不可分割 的 一部分 , 和 开发 一起组成 软件生产力 。 测试 是 DevOps 的 主干力量 。

    等 , 我好像是 第二次 讲上面这句话了 。  -_-      第一次是 在 《Socket-Vs-WebSocket-TestTool》 这篇文章里 :  https://www.cnblogs.com/KSongKing/p/9455439.html

    上面的 估算 是 针对 一个 CPU 核 的 , 如果 CPU 有 多个 核 , 比如 4 核 , 那么 QPS 可以达到 25万 * 4 = 100万次 / 秒 , 如果是 8 核 , 可以达到 25万 * 8 = 200万次 / 秒 。

    B Tree 在某些场合会显得比较 “白痴” 。 比如 只有 一行数据 , 要检索的 列 的 长度比较长 , 比如 4000个中文字符 , 检索内容(查询条件) 也是 4000个中文字符 的 字符串 , 根据上面的 推算 , 以 4000个字符 的 字符串 作为查询条件 的 检索 会 花费比较长的时间 。 而如果是循环遍历比较字符串的话 , 只需循环 1 次 , 比较 1 次 字符串 就可以得出结果了 。 对于 索引 而言 , 4000 个 中文字符 需要 检索 4000 * 2 * 8 / 2 = 3.2万 个 索引项 。 天 !   

    看起来 索引 跑了个 马拉松 , 而 循环遍历 字符串 只跑了 400米 。

    但仔细一想 , 字符串 比较 的 时间花费 跟 字符串 长度 也有关系 , 对于 ASCII 码 的话 , 每个字节(Byte)作一次比较 , 循环比较直到最后一个字符(如果中间有字符不同则可结束循环 返回 false) , 对于 Unicode 的话 , 每 2 个字节作一次比较 , 可以理解是 1 次 Int16 整数的比较 , 但也要循环比较 4000 次 。 

    而从这又联想到 , 对于 大字符串 的比较有没有更优化的算法 ? 我们会想到计算 Hash , 可以计算 2 个字符串的 Hash 值 进行比较 , 若相同则表示 字符串 相同 。 但 Hash 计算相当于是对 大整数 的 计算 , 具体的算法上可能也是会按 Byte 来计算 , 或者按 Int64(64位整数) 来计算 , 即对于 大字符串 , 每次取 8 个 字节(Byte) 来进行整数运算 , 以此来计算 Hash 。 但即使每次取 8 个字节 来计算 , 也要循环计算 1000 次 。

    所以 。 然后 。

    上述 的 效率对比问题 在    行数较少    检索内容长度较长    的 时候都存在 。

    索引 , 或者说 B Tree 索引 , 应该是广泛的应用于 数据存储管理 的 各种场合 。 比如 操作系统 的 文件系统 。

    这一点 , 我们会在 《浅谈操作系统原理》  https://www.cnblogs.com/KSongKing/p/9495999.html    一文中 探讨 , 当然 , 现在这篇文章里还没有具体内容 。 嘿嘿嘿 。

    到这里 , 看起来 , 问题差不多解决了 。 但 , 还有一个问题 , 就是 排序规则 。

    为了让数据按照人们习惯的排序方式排序 , 索引也需要 按照 人们习惯的排序方式 排序 , 实际上 , 索引 的排序规则 , 本身就是 检索规则 。

    所以 排序规则 是 索引 的 重要 组成部分 。

    比如 , 我们的中文习惯按照 音序 排序 , 就像 新华字典 那样 。

    那么 , 要实现 索引 的 排序 和 按 排序规则 检索 , 要怎么办呢 ?              

    要实现 排序规则 , 需要给 字符 编一个 排序编码 , 就像 字符编码(比如 Unicode)那样 。

    和 Unicode 一样 , 排序编码 也是 2 个 字节 , 编码是 按照 音序 来 , 比如 “啊” 字 大概是 “0000 0000 0000 0001”  吧  !

    不过 上面假设是 只包含 中文 的 情况 , 如果把 字母 和 特殊字符 包括进来 , 那 字母 和 特殊字符 应该会排在 汉字 前面 。  

    那要怎么知道 这个字符 的 排序编码是 多少呢 ? 需要一张 Unicode 和 排序编码 的 对照表 。 我们把这个 对照表 称为 排序编码表 。

    这样 根据 字符 的 Unicode 可以查找到 对应的 排序编码 。

    排序编码表 也是 一个 B Tree 索引 。 这样可以 快速查找 。

    根据 Unicode 查找 排序编码 , Unicode 的长度是 2 个 字节 , 所以查找的 时间花费 是 8 * 2 / 2 = 8 , 即 O(8) 。

    所以还是 很快的 。

    在有 排序编码 的情况下 , 索引 实际上是 根据 排序编码 建立 , 检索 也是根据 排序编码 检索 , 也就是说 , 索引项 里存的 2 位(bit) 数据都是 排序编码 的 bit 。

    字符的 Unicode 只有在 索引 最终指向的 数据项 才会 保存 。

    有了 索引 之后 , 就可以开始写 数据库引擎 了 , 索引 是 数据库 的 基础 。 也是 最基本单元 。

    首先 , 我们可以用 索引 来 建立 数据库 的 元数据 引擎 。

    元数据 , 就是 有多少张表 , 每张表 有哪些列 , 列的数据类型 , 表的 起始 Data Block , 表有哪些索引 , 索引的 起始 Data Block    等等 。

    数据库 要运作 , 首先要能 高效 的 管理 和 查询 元数据 。 这是基础 。 在这个基础上 , 才能进行 表 和 数据 的 存储管理 。

    接下来 , 我们要对 insert update 导致 数据移动 的问题进行一些 优化 。

    上文不止一次的提到 , 在 insert 和 update 可变长类型(如 varchar , nvarchar) 时 会 导致 数据移动 , 我们再来 Review 一下 :

    1 insert 会导致 Data Block 中 插入的数据 之后的数据全部要向后移动 。

    2 update 可变长类型 如果 新值 比 旧值 长 , 会导致 Data Block 中 旧值 之后的数据全部要向后移动 。

    3 update 可变长类型 如果 新值 比 旧值 短 , 会导致 Data Block 中 旧值 之后的数据全部要向前移动 。

    上面的 3 种 情况 相当于是 线性表 的 插入 删除 操作 。

    这些情况 对 性能的影响是挺大的 。 所以需要作一些改良 。 可以引入一些 “链式存储”(链表) 的 特性 , 来弥补这部分不足 。

    比如 insert 一笔资料 的时候 , 具体的举例 , 比如 , 有一个 Data Block , 我们称之为 Data Block 1 , 里面存了 2 行 , A 行 和 C 行 。 现要在 A 行 和 C 行之间插入 B 行 , 由于 A 行 C 行 的数据 是 顺序 连续 的 排列的 , 所以如果将 B 行数据 插入在 A行 和 C 行之间 , 就会需要 C 行数据 向后移动 , 如果采用 链表 的 方式 , 新建一个 Data Block (称之为 Data Block 2) , 将 B 行写入 Data Block 2, 让 A 行的 Next 指针指向 Data Block 2 中的 B 行 , 同时让 B 行的 Next 指针指向 Data Block 1 中的 C 行 。 这样就可以了 。这里的 指针 包含 2 个 字段 , 一个是 Data Block 的 位置 , 另一个是 数据在 Data Block 中的 起始位置 。 这里的 “位置” 是指 文件流 里的 “位置” 这个概念(如 C# 中的 FileStream.Position 属性) 。 Data Block 的 位置是指 Data Block 在 数据文件(Data File) 中的 起始位置 , 数据在 Data Block 中的位置 指 数据的起始位置 相对于 Data Block 起始位置 的 位置 。

    对于 可变长类型, 比如 varchar , nvarchar , 应采用 指针 的方式存储 , 即 可变长类型 的 值 不保存在 行 中 , 而是 独立存储 , 行 通过 指针指向 值 。 在 update 时 , 如果 新值 的长度大于 旧值 , 而 旧值 后面又连续存储了其它数据 , 则应 新申请一块空间 来存储 新值 , 并修改 行 内该列的指针 , 使指针指向 新值 的 位置 。 新申请的空间 可能在 同一个 Data Block 里 , 也可能在一个 已有的 Data Block 的 空闲空间(Free Space) 里 , 也可能会在一个 新的 Data Block 里 。

    如果 包含指针的数据 和 指针指向的数据 在 同一个 Data Block , 那么 指针 里的 Data Block 位置字段 可以为 -1 , 表示 在 同一个 Data Block 。 所谓 “包含指针的数据” 是指 比如 行 ; “指针指向的数据” 比如 行 的 下一行 , 或者 行 的 可变长类型列的 值 。

    显然 , 这样会造成一些 空闲空间(Free Space) , 或者 “碎片”  。

    看起来我们需要引入一些管理 空闲空间(Free Space) 的机制 。 可以用一个 空闲空间表(Free Space List)来管理 Free Space 。Free Space List 是一个 线性表 , 长度设为 10 , 就是说 , 最多只保存 10 个 Free Space 。 当有超过 10 个 的 Free Space 产生时 , 如果 新的 Free Space 的大小 小于 Free Space List 中当前最小的 Free Space , 则不会添加到 Free Space List, 如果大于 , 则会 移除 当前最小的 Free Space , 将新的 Free Space 添加进 Free Space List 。

    在 insert update 需要写入数据的时候 , 就到 Free Space List 里 查找 大小足够 的 Free Space , 若找不到大小足够的 Free Space , 则 申请一个新的 Data Block 。

    Free Space List 的每一个表项描述一个 Free Space , 表项应包含 3 个字段 , 1 Free Space 所在的 Data Block 的 起始位置 , 2 Free Space 的 起始位置 , 3 Free Space 的 结束位置 。 Free Space 的 起始位置 和 结束位置 是 相对于 Data Block 起始位置 的 相对位置 。

    大量 delete 数据的时候 会产生很多 Free Space , 如果只保存 10 个 Free Space , 会造成大量存储空间浪费 。 算了 , 还是 全部 Free Space 都保存吧 , 有多少保存多少 。 而且也不要 线性表 了 , 还是用 链表 来作为 Free Space List 。 这就跟 内存堆 一样了 。 关于 内存堆 , 可以 参考 我写的另一篇文章 《漫谈 C++ 的 内存堆 实现原理》  https://www.cnblogs.com/KSongKing/p/9527561.html  。

    如果 整个 Data Block 都空闲出来了 , 就直接归还 数据库引擎 , 不需要再保存到 Free Space List 。

    对于 “碎片” , 可以通过 Job 的 方式 定期 或 不定期 整理 。

    现在 , 在技术上 , 我们还需要实现一个 系统 , 或者说 机制 , 或者说 库 , 或者说 模块 , 来实现将 数据文件(Data File)里的 Data Block 读取到内存里并构成 对象图(对象树) 以及 将 更新过的数据写入 数据文件 对应的 Data Block 的 对应的位置 , 或者 将 新创建的 Data Block 写入 数据文件 。 所谓 对象图(对象树) , 就是 上述的 行与行 , 行与可变长类型列的值 之间的 链表关系(指针关系) 。

    好了 , 有了上述的这些 , 可以开始写数据库了 。

    好的, 我们进一步来讨论一下具体的做法 。

    我们需要一个 DataManager 类 和 一个 DataBaseManager 类 。

    DataManager 负责 底层 的 数据存取 。 DataBaseManager 负责 关系数据 的 管理(表 索引   ……) 。

    DataBaseManager 会 调用 DataManager 。

    DataManager 要实现的, 是一个 类似 内存映像 或者 虚拟内存 的 一个机制 。 将 内存 和 数据文件(Data File) , 映射成一个 虚拟的存储空间 。 我将这个机制, 称为 “虚拟存储” 。

    这样, DataBaseManager 就可以不需要考虑 数据在 内存 和 数据文件 中存储的 细节 而 只需 关注 关系数据 的 管理 即可 。

    DataManager 要实现 内存映像 或者 虚拟内存, 可以这样做, 首先, 定义一个 数据存储的单元, 类似 虚拟内存 里的 页, 我们可以叫做 Data Block 。 上文中也定义了 Data Block, 不过现在的 Data Block 和 上文的 Data Block 意义不一样。 上文的 Data Block 是 关系数据层面的, 比如 一张表的 数据 会 存在多个 Data Block 中, 以及 一行资料 最大长度 不能超过 一个 Data Block 的大小 等等 。 这些对于现在的 Data Block 来讲, 都不存在了 。

    所以, 这就是 大的 架构 。

    结论: SqlNet 是 基于 离散存储 的 新一代 数据库 。 离散存储 可以解决    大数据量大并发频繁 insert 索引排序(移动)造成的瓶颈      的问题 。

    SqlNet 的 离散存储 基于 虚拟存储 和 堆 。

    有关于    堆  ,   我在  《漫谈 C++ 的 内存堆 实现原理》   https://www.cnblogs.com/KSongKing/p/9527561.html       一文中 作了 探讨 。

    基于 离散存储 的 数据库 诞生的 土壤 是 硬件的发展, 以 固态硬盘 和 大容量内存 为代表 。

    还有另一个因素 是 关系数据库 的 发展到了 新的 突破 的 时候了 。

    或者可以这么说, 离散存储 使得 关系数据库 向 分布式并行计算 的 方向发展 更加 可行 和 有效 。

  • 相关阅读:
    F. Journey
    D. Divide
    C. Counting Pair
    A. A Big Dinner
    E
    D -Sale
    第十三课 历史记录画笔工具
    第十二课 文字工具
    第十一课 模糊工具、海绵工具、仿制图章工具
    第十课 切片工具 修复画笔工具 修补工具 颜色替换工具
  • 原文地址:https://www.cnblogs.com/chinasoft/p/14797910.html
Copyright © 2020-2023  润新知