前言
postgresql 保存数据的基本单位是 page,一个 page 里包含多条数据。postgresql 同磁盘的读写单位也是 page,一个 page 对应于磁盘的一个 block。block 的格式和 page 是相同的,本篇文章详细得介绍了 page 的数据存储格式和相关的增删改查操作。
内存结构
page 可以简单划分为四块区域:
- page 头部区域,描述整个 page 的情况,比如空闲空间,校检值等
- 数据指针区域,数据指针用来描述实际数据的存储信息
- 数据区域,用来存储实际数据
- 特殊区域,用来存储一些特殊数据
其中数据指针区域和数据区域是空间共享的,数据指针区域的区间是从上面开始的,向下扩展。而数据区域的空间方向是相反的,从下面开始的,向上扩展。
每条数据存储在 page 里,都对应一个数据指针和一个数据,数据指针记录了实际存储数据的位置。这种共享机制能够充分的利用空间,无论每条数据的是否过大或过小,都能几乎填满整个 page。
Page 头部
page 头部由结构体PageHeaderData
来表示,
|
|
当新添加一条数据到 page 里,需要快速判断是否有空闲空间。page 的 pd_flags 记录了 page 是否有空闲空间,它的标记位如下:
|
|
注意到 pd_linp 成员,它是一个 ItemIdData 数组,这里需要把它看成一个指针。它并不属于头部,从计算头部的长度就可以看出
|
|
整个结构如下图所示:
数据指针
ItemData 结构表示数据的指针,它描述了数据的位置和状态
|
|
lp_flags 只有2位,它有四种状态值:
|
|
插入数据
PageAddItemExtended
函数负责插入数据,定义如下
|
|
如下图所示,原来的 page 有三条数据 data0、data2、data3,而 data 1 数据已经被删除,所以数据指针 ItemIdData 1 的位置是空闲的,现在要插入 data4 数据。
中间的图片展示了两种插入情况,没有指定数据指针的位置,和指定了数据指针位置为第2个(也就是原有的 ItemData 1 空闲位置)并且指定了覆盖选项。
后面的图片展示了指定数据指针的位置第三个(也就是为原有 ItemData2 的位置)。
插入数据的原理总结如下:
- 如果没有指定数据指针的位置,那么会尽量使用空闲位置的,通过检查
PD_HAS_FREE_LINES
标记位,就可以判断page 是否有空闲数据指针。如果有空闲位置,那么就从头开始遍历,直到找到一个空闲位置。如果没有前面空闲位置,只能使用 pd_lower 指向的位置。 - 如果指定了数据指针位置,并且设置了覆盖选项,那么首先会去检查该位置的指针是否已经被使用,如果没有被使用则直接修改指针属性和插入数据,否则就会报错。
- 如果指定了数据指针位置,但没有指定覆盖,那么不管此位置是否空闲,都需要将后面的指针后移一位。
删除数据
PageIndexTupleDelete
负责删除指定位置的数据,删除数据后,会将需要将空闲的数据指针和数据进行压缩合并。
|
|
下图展示了数据删除的过程,这里需要删除数据 data 2:
首先将数据指针删除,然后将后面的数据向上移动,填满空缺位。
然后将实际的存储数据删除,将后面的数据向下移动,填补空缺位。
最后还需要更新数据指针的 offset 属性,因为其对应的数据存储位置已经发生了改变。
修改数据
|
|
如果原有数据的大小和新数据相同,那么直接修改对应的数据指针和实际的数据。
如果不一致,需要先将数据进行删除,然后将删除的空间进行压缩合并,并且更新所有数据指针的 offset 属性。最后才完成添加数据。