数据库通常使用固定大小的页面来存储数据。表、集合、行、列、索引、序列、文档等最终都以字节形式存在页面中。这样,存储引擎就可以与负责数据格式和 API 的数据库前端分离开来。此外,当一切都是页面时,更容易读取、写入或缓存数据。
下面是 SQL Server 页面布局的示例。
在本文中,我将探讨数据库页面的概念,介绍它们如何从磁盘读取和写入,以及它们如何在磁盘上存储。最后,我将通过一个示例来介绍 PostgreSQL 中的页面布局。
页面池
数据库以页面为单位进行读写。当你从表中读取一行时,数据库会找到包含该行的页面,并确定页面在磁盘上的文件和偏移位置。然后,数据库请求操作系统从文件的特定偏移位置开始读取页面的长度。操作系统检查其文件系统缓存,如果所需数据不在缓存中,操作系统会发起读取操作,并将页面加载到内存中供数据库使用。
数据库分配一个内存池,通常称为共享缓冲池或页缓冲池。从磁盘读取的页面被放置在缓冲池中。一旦页面位于缓冲池中,我们不仅可以访问请求的行,还可以访问页面中的其他行,具体取决于行的宽度。这使得读取操作非常高效,尤其是由索引范围扫描引起的读取。行越小,一页中容纳的行数就越多,单个 I/O 操作的效益就越大。
写入操作也是类似的过程。当用户更新一行时,数据库会找到包含该行的页面,将页面加载到缓冲池中,在内存中更新行,并创建一条变更日志记录(通常称为 WAL),将其持久化到磁盘。页面可以保留在内存中,以便在最终刷新回磁盘之前接收更多的写入操作,从而减少 I/O 次数。删除和插入操作的工作原理相同,但实现方式可能有所不同。
页面内容
你可以自行决定在页面中存储什么数据。行存储的数据库将行及其所有属性依次打包在页面中,以提高 OLTP 工作负载的性能,特别是写入工作负载。
列存储的数据库将行按列存储在页面中,适用于运行汇总、字段较少的 OLAP 工作负载。单个页面读取将会包含来自一个列的值,使得像 SUM 这样的聚合函数更加有效。
基于文档的数据库会压缩文档并将其存储在页面中,类似于行存储,而基于图的数据库则将连接性持久化在页面中,使得遍历图时页面读取更加高效,这也可以针对深度、广度和搜索进行调优。
无论你存储的是行、列、文档还是图,目标都是将数据项紧密地打包在页面中,以使页面读取更加高效。页面应该提供尽可能多有用的信息,以帮助处理客户端的工作负载。如果你发现自己需要读取许多页面来完成微小的工作,考虑重新思考你的数据建模。数据建模是一个完全不同的话题,常常被低估。
小页面 vs 大页面
小页面的读取和写入速度更快,特别是如果页面大小接近媒体块大小,然而页面头元数据的开销与有用数据相比可能会变得较高。另一方面,较大的页面大小可以减少元数据开销和页面拆分,但会增加冷读取和写入的代价。
当然,一旦接近磁盘/SSD,情况就变得非常复杂。存储行业的伟大头脑正在致力于优化主机和介质之间的读写性能,例如 Zoned 和 NVMe 中的键值存储命名空间等技术。在这里,我不打算尝试解释这些技术,因为坦率地说,我对这些领域还只是略知一二。
Postgres 默认页面大小为 8KB,MySQL InnoDB 为 16KB,MongoDB WiredTiger 为 32KB,SQL Server 为 8KB,Oracle 也为 8KB。数据库的默认设置适用于大多数情况,但重要的是要了解这些默认值,并准备为你的使用场景进行配置。
页面在磁盘上的存储方式
有许多方法可以将页面存储到磁盘并从磁盘中检索页面。一种方法是将每个表或集合作为固定大小页面的数组存储为一个文件。页面0后面是页面1,再后面是页面2。要从磁盘读取数据,我们需要的是文件名、偏移量和长度信息,在这种设计中我们都有!
要读取第x页,我们从表中获取文件名,偏移量为 X * Page_Size,页面的长度以字节为单位就是页面大小。
以读取名为"test"的表为例,假设页面大小为8KB,要读取页面2到10,我们读取存放表"test"的文件,偏移量为16484(2 * 8192),读取65536字节((10-2) * 8192)。
但这只是其中一种方式,数据库的美妙之处在于每个数据库实现都是不同的。
Postgres 页面布局
在众多数据库中,我想探索一下 PostgreSQL 是如何存储页面的,并对其中某些选择提出我的批评意见。在 Postgres 中,默认的页面大小为8KB,以下是它的布局。
让我尝试解释每个部分:
页面头部 — 24字节
页面必须包含用于描述页面内容的元数据,包括可用的空闲空间。这是一个固定的24字节头部。
ItemIds — 每个4字节
这是一个项指针(不是项或元组本身)的数组。每个ItemID是一个4字节的偏移量:长度指针,它指向页面上项的偏移量和大小。
事实上,这些指针的存在使得可以进行HOT优化(仅堆元组)。当在Postgres中更新行时,会生成一个新的元组。如果新的元组恰好适合在同一页面中作为旧元组,HOT优化将更改旧的项指针,使其指向新的元组。这样,索引和其他数据结构仍然可以指向旧的元组标识。非常强大。
尽管一种批评是项指针占用的大小,每个占用4字节,如果我可以存储1000个项,那么半个页面(4KB)就浪费在头部上了。
我使用项(item)、元组(tuple)和行(row),它们有所不同。行是用户看到的内容,元组是页面中行的物理实例,项指的就是元组。同一行可以有10个元组,一个是活动元组,7个用于供旧事务(出于MVCC原因)读取,还有2个无用的死元组。
项(Items) — 可变长度
这是项本身在页面上连续存储的地方。
特殊区域(Special) — 可变长度
此部分仅适用于B+树索引的叶子页面,其中每个页面链接到前一个页面和后一个页面。页面指针的信息存储在这里。
以下是引用元组的示例。
总结
数据库中的数据以页面为单位存储,无论是索引、序列还是表中的行。这使得数据库可以更轻松地处理页面,而不论页面本身包含什么内容。页面本身具有页头和数据,并作为文件的一部分存储在磁盘上。每个数据库对页面的外观和物理存储方式都有不同的实现,但最终的概念是相同的。
如果你喜欢我的文章,点赞,关注,转发!