我们有有一个 useracct 表,也就是维基百科的用户,它包含 userId 和 userName;然后有 pages 表,存储了维基百科数据;然后有 revisions 表,它说明哪个用户对哪个页面进行了哪些编辑或修订。同时,userId 指向 useracct 表,pageId 指向 pages 表,其中 pages 表的 revId 指向 revisions 表。
对于维基百科 OLTP 业务场景举几个例子,这些场景都只会修改或者查询表中很少的数据:
- 查询某一个维基百科词条,这样就是查询 pages 以及 revisions 表。
- 比如可能是用户每次登陆的时候更新用户记录
- 获取用户上次登录时更新的词条数据
- 修改词条,即修改 pages 表以及添加一个新的记录到 revisions 表中。这些是运行时间很短的简单操作,只在数据库中读取或写入一些值。
对于维基百科 OLAP 业务场景的一个例子是查看上个月来自于 .gov 的用户不同登陆次数,这种就会扫描表中的大部分数据。
我们引入存储模型的概念,第一种是基于行的存储模型,这就是所谓的n元存储模型(N-ary Storage Model),在一个页中存储基于行的数据,所有东西都像一个字节数组,所有东西都是连续存储的。这种格式对于 OLTP 业务请求更加友好,因为查询倾向于操作单个记录或者行这个行的所有数据是存储在一起的,如果不考虑溢出页的话就都在一页,也就是大部分请求每个都只会操作一页。
使用前面维基百科的 OLTP 例子,例如用户登录需要查询单个用户,这个请求会走索引(索引在后面的课堂中会讲到,在第七讲),索引会告诉我们去哪个页的哪个槽去获取这个用户元组的位置,读取槽获取到用户元组位与页中的位置,然后就能读取到这个用户元组的所有信息。同时注册新用户需要插入记录,这个插入也只会放在一页上,并且用户所有值都在一起。
但是这种存储不太适合 OLAP 的场景,还是用前面提到的维基百科的例子,查看上个月来自于 .gov 的用户不同登陆次数,这个查询不能走索引,我们需要遍历这个表的所有页,过滤 hostname 是.gov的以及 lastlogin 是上个月的,然后统计 lastlogin 字段,也就是我们其实只需要 hostname 和 lastlogin 这两个字段的值,但是实际我们却加载并解析了整个元组的所有属性值。这些带来了首先是磁盘 I/O 的浪费,以及对于解析整个元组数据带来的额外的内存与 CPU 消耗。
我们总结下 n 元存储模型的优缺点:
- 优点:
- 元组的增删改查很快
- 适合需要查询整个元组数据的查询
- 缺点:
- 很不适合要扫描表中大部分数据,并且查询的只是元组属性的子集的场景
第二种是基于列的存储模型,这就是所谓的分解存储模型或 DSM(Decomposition Storage Model),即将一个元组的单一属性的值于一个页面中连续存储,而不是连续地存储单个元组的所有不同属性值。我们将提取所有的元组这个列值并将他们连续存储,这也是"列存储"这个名字的来源。这对于有很多只读查询的 OLAP 工作负载非常理想,一般这种查询需要分析大部分行的某些属性值,如果我们把同一属性的值放在一起,我们就不用扫描查询中用不到的属性,并且同一属性的值在一起这样对于某个属性运行聚合函数窗口函数就会效率更高。
我们回到前面提到的维基百科的 OLAP 例子,查看上个月来自于 .gov 的用户不同登陆次数,这个查询我们只需要hostname和lastLogin,我们不需要表格中的任何其他属性,所以我们现在就可以找到对应于这两个列的页,减少了要扫描的数据消耗。
但是对于那种需要返回元组所有属性的请求,比如要查询某一个元组的所有属性,需要查询每个属性的所在的页,然后汇总返回。那么如何从每个属性所在的页找到这个元组对应的数据呢?
可以有两种方式:
- 固定长度的偏移量:因为每个列值都是相同的类型,对于固定长度字段,我们直接通过长度乘以个数就能得到对应数据的位置偏移。但是如果对于可变长度的字段,例如可变长度的字符串,可以通过一些方式转换成固定长度的字段,例如将字符串填充拉长到特定的长度,或者进行编码使用长度的整数代码替换字符串,这个在之后的课程会详细讨论。这种方式虽然占用空间小实现简单,但是对于变长字段实现比较麻烦并且会有空间浪费等问题。
- 另一种选择是存储元组的id直接嵌入到列中:一般这些列还是通过某种排序规则排序的,我们可以通过二分查找来找到对应 id 的数据。
我们总结下 DSM 存储模型的优缺点:
- 优点:
- 减少了I/O浪费,因为只读取查询所需的列中的数据
- 对于实际存储的列,您可以得到更好的查询处理和压缩(后面课程还会详细讨论这个,同一种类型的数据放在一起更好压缩,例如我们存储的是日期,那么我们不用每一个值都存储日期,而是第一个存储日期,之后的存储与第一个日期的相对日期)
- 缺点:
- 如果你想去重建一个单独的元组的所有数据,那么就比较慢
- 要做插入更新之类的事情要困难得多,因为你现在需要在多列多页中添加值而不是一次写完
DSM 系统并不是新的设计,它们已经存在了一段时间,第一个是在20世纪70年代发布的 Cantor,它实际上不像DBMS,而是像一个文件系统。在20世纪80年代,有了第一个关于DSM存储的理论基础或提议。在20世纪90年代,有一种产品叫做SybaseIQ,它就像Sybase这个行存储的内存加速器,可惜并不流行。他们所做的是将数据以列存储形式在内存中,以加速某些类型的查询。在21世纪初到中期,这三个系统开始流行,Vertica, VectorWise 和 MonetDB,他们是第一个受欢迎的,商业上成功的列存储,为很多目前很常见的列存储技术铺平了道路。从 2010 以后,基本所有人都会用到基于 DSM 的系统了。
总结一下,虽然课程开始的时候一直在说DBMS是由这些独立的部分组成的技术栈,但其实并不是完全独立的,比如这里,为目标工作负载选择正确的存储模型非常重要,对于 OLTP 你想要行存储,OLAP 需要列存储。