Postgresql数据库单个Page最多存储多少行数据
源码src/include/storage/bufpage.h 定义了一个page的结构如下
* +----------------+---------------------------------+
* | PageHeaderData | linp1 linp2 linp3 ... |
* +-----------+----+---------------------------------+
* | ... linpN | |
* +-----------+--------------------------------------+
* | ^ pd_lower |
* | |
* | v pd_upper |
* +-------------+------------------------------------+
* | | tupleN ... |
* +-------------+------------------+-----------------+
* | ... tuple3 tuple2 tuple1 | "special space" |
* +--------------------------------+-----------------+
一、页面整体结构
- 页面头部(Page Header,24 字节)页面头部存储元数据,用于描述页面状态和管理数据
- 行指针表(Line Pointer Array)紧接页面头部之后,存储页面中所有元组的位置信息,每个指针占 4 字节
- 数据区域(Data Section) 存储实际的元组数据,每个元组由 元组头部(Tuple Header 23 个字节) 和 数据内容 组成
- 空闲空间(Free Space) 位于数据区域末尾,用于新元组的插入或元组更新时的扩展。 通过 空闲列表(Free List) 管理可用空间,记录未使用的字节范围。
Page(8KB)结构:
+-------------------+
| 页面头部(24字节) |
+-------------------+
| 行指针表(N×4字节)| 每个指针记录元组的位置和状态
+-------------------+
| 数据区域(可变) | 元组数据(头部+内容),按行指针表顺序存储
+-------------------+
| 空闲空间(可变) | 未使用的字节,由空闲列表管理
+-------------------+
计算最大元组数量
对于默认8K的page,设最大元组数量为 n,行指针表占用空间为 4 * n 字节,元组数据占用空间为 tuple_size * n 字节。
因此可得到不等式: 24+4n+tuplesize×n≤8192
整理该不等式可得:n≤ (8192−24)/(4+tuplesize)
想要n越大,则tuplesize 越小,tuplesize 包括tuple头部信息23字节,n最大值得302。这是纯数学的计算,接下来看看代码中的实际规定。
最大行数的宏定义MaxHeapTuplesPerPage
#define MaxHeapTuplesPerPage \
((int) ((BLCKSZ - SizeOfPageHeaderData) / \
(MAXALIGN(SizeofHeapTupleHeader) + sizeof(ItemIdData))))
/*
* MaxHeapTuplesPerPage is an upper bound on the number of tuples that can
* fit on one heap page. (Note that indexes could have more, because they
* use a smaller tuple header.) We arrive at the divisor because each tuple
* must be maxaligned, and it must have an associated line pointer.
*
* Note: with HOT, there could theoretically be more line pointers (not actual
* tuples) than this on a heap page. However we constrain the number of line
* pointers to this anyway, to avoid excessive line-pointer bloat and not
* require increases in the size of work arrays.
*/
这段代码注释解释了 MaxHeapTuplesPerPage 宏的定义和作用:
1. 这个宏定义了单个堆页面(heap page)可以容纳的元组(tuple)数量的上限值。索引页面可能有更高的上限,因为索引使用更小的元组头部。
2. 计算这个值的依据是:
- 每个元组必须进行MAXALIGN内存对齐
- 每个元组必须有一个关联的行指针(line pointer)
3. 关于HOT(Heap Only Tuple)的特别说明:
- 理论上使用HOT时,一个堆页面上可能有比这个限制更多的行指针(不是实际元组)
- 但为了避免行指针过度膨胀,以及不需要增加工作数组的大小,仍然将这个值作为行指针数量的限制
这个限制确保了PostgreSQL能够有效地管理页面空间,同时保持系统性能。在实际使用中,由于元组大小和页面布局的复杂性,实际可存储的元组数量可能会少于这个上限值。
根据代码里提到的内存对齐,重新计算一次
-
基本参数值:
- 默认块大小
BLCKSZ
= 8192字节 (8KB) - 页面头部大小
SizeOfPageHeaderData
= 24字节 - 堆元组头部大小
SizeofHeapTupleHeader
= 23字节 - 行指针大小
sizeof(ItemIdData)
= 4字节
- 默认块大小
-
计算公式:
MaxHeapTuplesPerPage = (BLCKSZ - SizeOfPageHeaderData) / (MAXALIGN(SizeofHeapTupleHeader) + sizeof(ItemIdData))
-
具体计算步骤: a) 计算页面可用空间:
8192 - 24 = 8168
字节b) 计算单个元组占用空间:
- 堆元组头部对齐后大小:
MAXALIGN(23) = 24
字节 (在64位系统上按8字节对齐) - 加上行指针:
24 + 4 = 28
字节
c) 计算最大元组数量:
8168 / 28 ≈ 291.714
,向下取整得291 - 堆元组头部对齐后大小:
因此,在默认8KB块大小下,不考虑存储数据占用的情况下,每个堆页面理论上最多可以存储291个元组。
实际测试
首先安装pageinspect 插件,能够方便的查看底层page的存储情况。
-- 安装pageinspect扩展
CREATE EXTENSION pageinspect;
-- 查看特定页面的头部信息
SELECT * FROM page_header(get_raw_page('table_name', page_number));
-- 查看页面中的元组信息
SELECT * FROM heap_page_items(get_raw_page('table_name', page_number));
测试表结构只有一列char(1) 数据类型,插入两行数据后查看page使用情况
test=# \d test1
Table "public.test1"
Column | Type | Collation | Nullable | Default
--------+--------------+-----------+----------+---------
var | character(1) | | |
test=# insert into test1 values('A');
INSERT 0 1
test=# insert into test1 values('A');
INSERT 0 1
test=# SELECT * FROM page_header(get_raw_page('test1', 0));
lsn | checksum | flags | lower | upper | special | pagesize | version | prune_xid
-----------+----------+-------+-------+-------+---------+----------+---------+-----------
0/2AB6128 | 0 | 0 | 32 | 8128 | 8192 | 8192 | 4 | 0
(1 row)
test=# SELECT * FROM heap_page_items (get_raw_page('test1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+--------
1 | 8160 | 1 | 26 | 983 | 0 | 0 | (0,1) | 1 | 2050 | 24 | | | \x0541
2 | 8128 | 1 | 26 | 984 | 0 | 0 | (0,2) | 1 | 2050 | 24 | | | \x0541
lp_len 显示的值解析
每一行数据的长度是lp_len
值为26字节的原因如下:
-
元组头部结构
HeapTupleHeaderData
基础大小为23字节,但实际存储时会进行内存对齐:- 在64位系统上会按8字节对齐,所以实际占用24字节
-
对于char(1)字段的存储:
- 1字节用于存储实际字符数据
- 1字节用于null bitmap(即使只有一个字段也需要)
-
计算明细:
- 对齐后的头部:24字节
- null bitmap:1字节
- char(1)数据:1字节
- 总计:24 + 1 + 1 = 26字节
-
额外说明:
t_data
字段显示为\x0541
,其中:05
是长度前缀41
是字符’A’的ASCII码
- 这种存储格式确保了数据对齐和快速访问
每行数据实际占用空间
根据upper计算,初始值是8192-当前值8128=64,一共两行数据,每行实际占用是32个字节,为什么不是lp_len 显示的26字节呢,因为实际存储时会计算内存对齐(在64位系统上会按8字节对齐,长度是8的倍数),所以每行数据实际占用是32字节
真实测试结果每个page能存多少行数据
根据当前的表结构和测试数据,计算结果如下
(8192−24)/(32+4) = 226 行
当插入到226行数据时,upper-lower=32 ,小于行数据32 字节 + 行指针4字节,page已经满了
test=# test=# SELECT * FROM page_header(get_raw_page('test1', 0));
lsn | checksum | flags | lower | upper | special | pagesize | version | prune_xid
-----------+----------+-------+-------+-------+---------+----------+---------+-----------
0/2B13A10 | 0 | 0 | 928 | 960 | 8192 | 8192 | 4 | 0
(1 row)
test=# select count(*) from test1;
count
-------
226
(1 row)
再插入一行数据后,upper和lower已经不变化了,已经到了新的一个page
test=# test=# select count(*) from test1;
count
-------
227
(1 row)
test=# SELECT * FROM page_header(get_raw_page('test1', 0));
lsn | checksum | flags | lower | upper | special | pagesize | version | prune_xid
-----------+----------+-------+-------+-------+---------+----------+---------+-----------
0/2B13A10 | 0 | 0 | 928 | 960 | 8192 | 8192 | 4 | 0
(1 row)
test=# SELECT * FROM page_header(get_raw_page('test1', 1));
lsn | checksum | flags | lower | upper | special | pagesize | version | prune_xid
-----------+----------+-------+-------+-------+---------+----------+---------+-----------
0/2B15780 | 0 | 0 | 28 | 8160 | 8192 | 8192 | 4 | 0
(1 row)
总结
在PostgreSQL数据库中,单个Page能够存储的数据行数与Page size(页面大小)紧密相关。当Page size设定为8K时,从代码层面的设计理论角度来讲,单个Page最多能够存储291行数据。然而,通过实际测试发现,即便是采用最简单的表结构,且每行数据仅占用1个字节的情况下,单个8K大小的Page实际上也只能存储226行数据 。