磁盘上的数据布局与 RAM 中的数据表示完全一致。页面及其元组按原样读入缓冲区缓存中,无需任何转换。这就是为什么数据文件在不同平台之间不兼容的原因。
- 不兼容的原因之一 是字节序 。例如,x86 架构是小端序 ,z/Architecture 是大端序 ,而 ARM 的字节序是可配置的。
- 另一个原因是按照机器字边界进行数据对齐,这也是许多架构所要求的。例如,在 32 位 x86 系统中,整数 (integer 类型,占用四个字节) 按四字节字边界对齐,就像双精度浮点数 (double precision 类型,占用八个字节)。但在 64 位系统中,双精度数按八字节字边界对齐。
数据对齐使得元组的大小取决于表中字段的顺序。这种影响通常可以忽略不计,但在某些情况下,它会导致大小显著增加。
使用pageinspect 扩展中的 heap_page_items 函数来显示关于指针和元组的一些细节。
sql
=> CREATE TABLE padding(
b1 boolean,
i1 integer,
b2 boolean,
i2 integer
);
=> INSERT INTO padding VALUES (true,1,false,2);
=> SELECT lp_len FROM heap_page_items(get_raw_page('padding', 0));
lp_len
−−−−−−−−
40
(1 row)
#这一行的大小是 40 个字节,其行头占 24 个字节,integer 类型的列占用 4 个字节,每个 boolean 类型的列占用 1 个字节。总共 34 个字节,因此浪费了 6 个字节用于整数列的四字节对齐。
#如果我们重建表,空间将被更有效地利用:
=> DROP TABLE padding;
=> CREATE TABLE padding(
i1 integer,
i2 integer,
b1 boolean,
b2 boolean
);
=> INSERT INTO padding VALUES (1,2,true,false);
=> SELECT lp_len FROM heap_page_items(get_raw_page('padding', 0));
lp_len
−−−−−−−−
34
(1 row)
在 PostgreSQL 中,表通常被称为堆 (heap)。这是另一个晦涩的术语,暗示了元组的空间分配和动态内存分配之间的相似性。确实可以看到某种类比,但表由完全不同的算法管理。与有序索引相比,我们可以将这个术语理解为"一切都堆积成堆"。
关键概念:字节对齐
- 4 字节对齐:数据必须存储在能被 4 整除的内存地址上
- 填充字节:为满足对齐要求而插入的空字节
第一种表结构(低效)
ini
CREATE TABLE padding(
b1 boolean, -- 1 字节
i1 integer, -- 4 字节,需要 4 字节对齐
b2 boolean, -- 1 字节
i2 integer -- 4 字节,需要 4 字节对齐
);
内存布局分析:
偏移量: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
内容: [行头................................] [b1] [pad] [pad] [pad] [i1...........]
偏移量: 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
内容: [b2] [pad] [pad] [pad] [i2...........] [未使用的空间...................]
总计: 24(行头) + 1(b1) + 3(填充) + 4(i1) + 1(b2) + 3(填充) + 4(i2) = 40 字节
浪费了6个字节
第二种表结构(高效)
sql
CREATE TABLE padding(
i1 integer, -- 4 字节,4 字节对齐
i2 integer, -- 4 字节,4 字节对齐
b1 boolean, -- 1 字节
b2 boolean -- 1 字节
);
内存布局分析:
偏移量: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
内容: [行头................................] [i1...........] [i2...........]
偏移量: 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
内容: [b1] [b2] [未使用的空间................................................]
总计: 24(行头) + 4(i1) + 4(i2) + 1(b1) + 1(b2) = 34 字节
PostgreSQL 在存储表行时,会根据每个数据类型的对齐要求插入填充字节以满足对齐规则。整数类型(integer)需要 4 字节对齐,因此如果布尔类型(boolean,1 字节)列放在整数列之前或中间,会产生填充字节浪费空间。
- 原始表结构:布尔类型列散布在整数列中间,导致每个整数列前都可能插入填充字节,使行长度增加至 40 字节。
- 重新排序字段:先放所有整数类型,再放布尔类型,避免了中间填充,数据紧凑排列,使行长度减少到 34 字节。
因此,合理设计列的顺序,以满足对齐要求,可以显著减少行占用的存储空间,提高存储效率和 IO 性能。