假设元组的 c 属性是一个 VARCHAR 类型并且保存的值很大,那么元组内在 c 的位置会保存一个指针,它会指向存储在溢出页中的 varchar 数据。溢出页可能一页存不下,不止一页大小,所以会是一个页链表。这在不同的系统中有不同的叫法:
- postgres 称它为 toast,如果大于2KB,溢出页就会出现
- MySQL:大于页大小的一半就会出现溢出页
- SQL Server:大于页大小才会出现溢出页
除了溢出页还有另一种方式即存储为外部文件
某些 DBMS 允许你将这种大值存储到外部的文件中,以 BLOB 的方式处理这个数据,例如:
- Oracle: BFILE 数据类型
- Microsoft:FILESTREAM 数据类型
我们一般不不适合存储进数据库的大数据放入外部文件存储,例如视频、图片等等。我们只是存储了一个指向数据的指针,实际指向的数据位于文件系统的其他地方,我们可以在需要的时候引用它。
但是这样就丧失了 DBMS 对其的管理特性,例如不能保证外部是否有修改或者删除这个外部文件,不能保证事务性修改等等。
对于非不适合放入数据库存储的那种大数据,比如大 JSON 等等,Jim Gray 曾经有一篇论文研究,评估数据大小对于是直接在溢出页面内存储blob好,还是在外部存储blob更好的影响,这是一篇 15 年前的论文,这篇论文得出的结论是,他们得出的结论是256KB的大小在外部存储更有利,现在这个数字可能变得更大了。但是我们要记住,如果它存储在DBMS中,我们每次都要把这些巨大的对象通过很多页写入和从磁盘中读取,这是我们要考虑的权衡。
3. 系统目录(System Catalog)
我们接下来要讲的是系统级别的目录,DBMS 需要在内部保存所有关于数据库的元数据以便于他能需要知道如何编码和解码存储在代表元组的字节中的数据。一般存储关于表,列,索引,视图的结构信息,诸如此类的结构信息。DBMS 通常还存储有关用户和权限的信息,就像访问权限,即用户应该能够查看或修改哪些数据。最后,DBMS 还存储了大量的内部统计数据,比如不同值的数量,或者连接基数,或者数据范围之类的,这些是构建查询计划,查询执行中非常重要的。
大部分的 DBMS 都将数据库存储为目录类型的结构,前面我们说过,在这个系统目录中也会存储关于表,列,索引,视图的结构信息,这些结构信息也像普通的表一样存储。那么现在就有了鸡生蛋蛋生鸡的问题,我们需要这些结构信息解析读取表数据,但是这些信息也以表的形式存储。所以一般的设计是它们有这些特殊的元数据对象包装器,系统可以用来直接编码和解码存储在系统目录中的值。
用户可以查询 DBMS 的这个内部目录,它通常存储在这个 INFORMATION_SCHEMA 中,以获取关于数据库的信息以及各种统计信息等等。这被 ANSI 标准定义为只读视图的集合,在它标准化之前,这曾经很混乱,每个系统都有自己的方式来暴露这些元数据。现在有了这个标准,大家都可以通过访问 INFORMATION_SCHEMA 来访问这些信息。不过不同的系统还是暴露了其他的一些等价的快捷方式命令访问这些信息,比如:
这是列出某个数据库中所有表的命令:
- SQL-92 标准中是:
select * from information_schema.tables
- Postgres 中是:
\d
- MySQL 中是:
show tables
- sqlite 中是:
.tables
这是查看某个表的详细信息的命令:
- SQL-92 标准中是:
select * from information_schema.tables where table_name = 'student'
- Postgres 中是:
\d student
- MySQL 中是:
DESCRIBE student
- sqlite 中是:
.schema student
4. 数据库工作负载类型(Database Workload)
我们主要有三种不同类型的数据库工作负载:
- 第一个是在线事务处理,简称 OLTP(Online Transaction Processing):这意味着你有很多快速短小的运行操作,即每次只读取或更新一小部分数据。例如你的银行账户场景,比如你想要得到你银行账户的余额,你只是读取一个值,或者如果你想做一笔交易,比如存款,这是一笔相当短的交易。还有就是像亚马逊这样的网上商店,你浏览不同的产品把它们加到你的购物车结账付运费,你访问的数据量相对于亚马逊提供的所有产品来说是相对较小的,但是如果你思考下世界上有很多同时购物的人,这就积少成多了,即各个事务访问或修改的数据量积少成多了。
- 第二个是在线分析处理,简称 OLAP(Online Analytical Processing):这些有点像分析查询,读取大量的数据,扫描表的大部分,会产生聚合,有很多表之间的连接,通常用于决策支持或商业智能。同样是亚马逊的例子,比如亚马逊想知道在过去的一个月里,CMU 学生购买最多的五个商品是什么。这种查询需要扫描一个大的样本,而不仅仅是更新单个或读取单个记录。
- 最后一个是最近越来越流行的混合事务和分析处理,简称 HTAP(Hybrid Transaction and Analytical Processing):目标是能够把 OLTP 和 OLAP 放在同一个数据库实例上,事务处理和分析同时运行,这样能够得到关于交易数据更快或更实时的见解。
这个坐标图可能更直观些,X 轴是从写多读少到读多写少,Y 轴是请求复杂度,从简答到复杂。OLTP 的工作负载更多的是写多一些并且比较简单的请求,OLAP 的工作负载更多的是读多一些并且比较复杂的请求,HTAP 介于两者之间。
在实际使用中,一般公司会建立 OLAP 与 OLTP 独立的环境:因此,在一端你通常会有多个 OLTP 数据筒仓,这里做所有的在线业务请求;另一端非常大的 OLAP 数据仓库,你要在数据仓库转储所有的数据筒仓的数据以供分析。我们需要从数据筒仓到数据仓库的数据传输,主要通过这个 ETL(Extract Transform Load,提取、转换、加载)过程:我们从这些不同的数据筒仓中提取所有的数据,这些数据格式可能与我们最终需要的数据格式有差异,所以我们需要转换这些数据,并且对数据做一些处理,比如合并,删除重复等等,最后加载到数据仓库中。还有一些数据分析的结果需要从数据仓库传回数据筒仓中,例如一些产品推荐信息,在你访问商品网页时为你推荐的产品。HTAP 的思想就是让这些事务工作与查询工作一起并发执行,并省略很多中间的同步操作。
为什么区分不同类型的工作负载很重要?回顾一下关系模型,它为我们对数据进行不同操作提供了一定的规则和要求,但它并没有告诉我们在物理上我们需要如何存储数据。我们需要根据我们的业务即工作负载的类型,来决定我们的数据如何存储。我们前面主要讲的主要是基于行的存储,即某一个元组的所有属性的数据都紧密的保存在一起。但是这种设计并不适用所有的场景,我们来看一个维基百科的例子: