数据库的路上

Postgresql数据库单个Page最多存储多少行数据

源码src/include/storage/bufpage.h 定义了一个page的结构如下

 * +----------------+---------------------------------+
 * | PageHeaderData | linp1 linp2 linp3 ...           |
 * +-----------+----+---------------------------------+
 * | ... linpN |									  |
 * +-----------+--------------------------------------+
 * |		   ^ pd_lower							  |
 * |												  |
 * |			 v pd_upper							  |
 * +-------------+------------------------------------+
 * |			 | tupleN ...                         |
 * +-------------+------------------+-----------------+
 * |	   ... tuple3 tuple2 tuple1 | "special space" |
 * +--------------------------------+-----------------+

一、页面整体结构

  1. 页面头部(Page Header,24 字节)页面头部存储元数据,用于描述页面状态和管理数据
  2. 行指针表(Line Pointer Array)紧接页面头部之后,存储页面中所有元组的位置信息,每个指针占 4 字节
  3. 数据区域(Data Section) 存储实际的元组数据,每个元组由 元组头部(Tuple Header 23 个字节) 和 数据内容 组成
  4. 空闲空间(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能够有效地管理页面空间,同时保持系统性能。在实际使用中,由于元组大小和页面布局的复杂性,实际可存储的元组数量可能会少于这个上限值。
 

根据代码里提到的内存对齐,重新计算一次

  1. 基本参数值

    • 默认块大小 BLCKSZ = 8192字节 (8KB)
    • 页面头部大小 SizeOfPageHeaderData = 24字节
    • 堆元组头部大小 SizeofHeapTupleHeader = 23字节
    • 行指针大小 sizeof(ItemIdData) = 4字节
  2. 计算公式

    MaxHeapTuplesPerPage = (BLCKSZ - SizeOfPageHeaderData) / 
                          (MAXALIGN(SizeofHeapTupleHeader) + sizeof(ItemIdData))
    
  3. 具体计算步骤: 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字节的原因如下:

  1. 元组头部结构HeapTupleHeaderData基础大小为23字节,但实际存储时会进行内存对齐:

    • 在64位系统上会按8字节对齐,所以实际占用24字节
  2. 对于char(1)字段的存储:

    • 1字节用于存储实际字符数据
    • 1字节用于null bitmap(即使只有一个字段也需要)
  3. 计算明细:

    • 对齐后的头部:24字节
    • null bitmap:1字节
    • char(1)数据:1字节
    • 总计:24 + 1 + 1 = 26字节
  4. 额外说明:

    • 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行数据 。