数据库页Page详解


数据库通常使用固定大小的页来存储数据。表、集合、行、列、索引、序列、文档等最终以页中的字节结束。这样存储引擎就可以从负责数据格式和 API 的数据库前端中分离出来。此外,当一切都是页时,这使得读取、写入或缓存数据变得更加容易。
下面是 SQL Server 页布局的示例。

在这篇文章中,我探讨了数据库页的概念,它是如何读取和写入磁盘的,它们是如何存储在磁盘上的,最后我通过一个 Postgres 中的页布局示例。

页池
数据库以页为单位读写。当您从表中读取一行时,数据库会找到该行所在的页,并标识该页在磁盘上的文件和偏移量。然后数据库要求操作系统从文件中读取页长度的特定偏移量。操作系统检查其文件系统缓存,如果所需数据不存在,则操作系统发出读取并将页拉入内存以供数据库使用。

数据库分配一个内存池,通常称为共享池或缓冲池。从磁盘读取的页放在缓冲池中。一旦页在缓冲池中,我们不仅可以访问请求的行,还可以访问页中的其他行,具体取决于行的宽度。这使得读取更加高效,尤其是那些由索引范围扫描产生的读取。行越小,单个页中容纳的行越多,单个 I/O 给我们带来的收益就越大。

写入也是如此,当用户更新一行时,数据库找到该行所在的页,将页拉入缓冲池并更新内存中的行并将更改的日志条目(通常称为 WAL)持久化到磁盘。该页可以保留在内存中,因此它可以在最终刷新回磁盘之前接收更多写入,从而最大限度地减少 I/O 的数量。删除和插入工作相同,但实现可能不同。

页内容
您在页中存储的内容取决于您:
行存储数据库将行及其所有属性一个接一个地写入页中,以便更好地处理 OLTP 工作负载,尤其是写入工作负载。

列存储数据库逐列写入页中的行,这样运行汇总的 OLAP 工作负载字段越少效率越高。单页读取将包含来自一列的值,使 SUM 等聚合函数更加有效。我制作了一个视频来比较基于行和基于列的存储引擎,

基于文档的数据库压缩文档并将它们存储在页中,就像行存储和基于图形的数据库保持页中的连接性这样页读取对于遍历图形是有效的,这也可以针对深度与广度与搜索进行调整。

无论您是存储行、列、文档还是图表,目标都是将您的项目打包在页中,以便页读取有效。该页应为您提供尽可能多的有用信息,以帮助您减轻客户端的工作量。如果您发现自己阅读了很多页来做一些微不足道的工作,请考虑重新考虑您的数据建模。这是一篇完全不同的文章,数据建模,被低估了。

小页与大页
小页的读写速度更快,尤其是当页大小接近媒体块大小时,但是与有用数据相比,页标头元数据的开销成本可能会很高。另一方面,较大的大小可以最大限度地减少元数据开销和页拆分,但会以更高的冷读写为代价。

当然,您离磁盘/SSD 越近,这就会变得非常复杂。存储行业的杰出人士正在研究 NVMe 中的 Zoned 和键值存储命名空间等技术,以优化主机和介质之间的读/写。我什至不打算在这里解释它,因为坦率地说,我仍然在这些水域中试探。

Postgres 默认页大小为8KB,MySQL InnoDB 为16KB,MongoDB WiredTiger 为32KB,SQL Server 为8KB,Oracle 也是8KB。数据库默认值适用于大多数情况,但了解这些默认值并准备好为您的用例配置它很重要。

页如何存储在磁盘上
我们可以通过多种方式将页存储到磁盘或从磁盘检索页。一种方法是将每个表或集合的文件作为固定大小页的数组。第 0 页,然后是第 1 页,然后是第 2 页。要从磁盘读取内容,我们需要信息、文件名、偏移量和长度,在这种设计中,我们拥有所有这三者!
要读取页 x,我们从表中知道文件名,得到它的偏移量是 X *Page_Size,以字节为单位的长度是页大小。


示例读取表测试,假设页大小为 8KB,要读取第 2 页到第 10 页,我们读取表测试所在的文件,偏移量为 16484 (2*8192) 为 65536 字节 ((10–2)*8192)。

但这只是一种方式,数据库的优点在于每个数据库的实现都是不同的。

Postgres 页布局
在数据库中,我想探讨一下 PostgreSQL 是如何存储页的,并提供我对某些选择的批评。在 Postgres 中,默认页大小为 8KB,如下所示。

让我尝试浏览每个部分:
页眉 — 24 字节
页必须有元数据来描述页中的内容,包括可用的可用空间。这是一个 24 字节的固定标头。

ItemIds — 每个 4 字节
这是一个项目指针数组(不是项目或元组本身。每个 itemId 都是一个 4 字节的 offset:length 指针,它指向项目所在页的偏移量以及项目的大小。

事实上,这个指针的存在允许 HOT 优化(Heap only tuple),当 postgres 中的一行发生更新时,会生成一个新的元组,如果该元组恰好与旧元组放在同一页中,则HOT 优化将旧的项目 id 指针更改为指向新的元组。这样索引和其他数据结构仍然可以指向旧的元组 ID。很强大。

尽管一项批评是项目指针占用的大小,每个指针为 4 个字节,但如果我可以存储 1000 个项目,则一半的页 (4KB) 会浪费在标题上。
我使用项目、元组和行但有区别,行是用户看到的,元组是页中行的物理实例,项目是元组。同一行可以有 10 个元组,一个活动元组和 7 个留给旧事务(MVCC 原因)读取和 2 个不再需要的死元组。

项目——可变长度
这是项目本身一个接一个地存在于页中的地方。

特殊 — 可变长度
本节仅适用于B+Tree索引叶页,其中每个页都链接到上一页和前一页。有关页指针的信息存储在这里。

概括
数据库中的数据以页结尾,无论是索引、序列还是表行。这使得数据库更容易处理页,而不管页本身有什么。它自己的页有一个标题和数据,并作为文件的一部分存储在磁盘上。每个数据库对页的外观以及它在磁盘上的物理存储方式都有不同的实现,但最后,概念是相同的。