首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >PostgreSQL 表设计必看!字段对齐规则让你省出 20% 磁盘空间

PostgreSQL 表设计必看!字段对齐规则让你省出 20% 磁盘空间

作者头像
小徐
发布2026-02-28 19:04:17
发布2026-02-28 19:04:17
950
举报
文章被收录于专栏:GreenplumGreenplum

背景

做 PostgreSQL 开发或 DBA 的朋友应该都有过这样的经历,表结构随便建,数据量上来后磁盘嗖嗖涨,明明没存多少数据,空间却告急了。其实很多时候,问题出在字段对齐上,PG 在存储行数据时会按规则做字节对齐,不合理的字段顺序会产生大量空间浪费,而掌握这个规则,只需调整字段顺序,就能轻松省出不少磁盘空间。

PG 的字段对齐核心规则

PG 中每一行数据,在大多数 64 位系统上,由于内存对齐的要求,它实际上会占据 24 字节 的物理空间。 被称为 HeapTupleHeaderDat,这个行头主要用于支持 PostgreSQL 的 MVCC(多版本并发控制) 机制,主要包含:

代码语言:javascript
复制
t_xmin (4 字节): 插入该行的事务 ID。
t_xmax (4 字节): 删除或更新该行的事务 ID(如果没被删除则为 0)。
t_cid / t_xvac (4 字节): 命令 ID 或用于清理(Vacuum)的特殊 ID。
t_ctid (6 字节): 指向该行当前版本或新版本的物理位置(Page/Offset)。
t_infomask2 (2 字节): 存储属性数量及一些元数据标志位。
t_infomask (2 字节): 存储关于元组的状态标志(如是否有 NULL 值、是否有 OID 等)。
t_hoff (1 字节): 记录从行头到实际数据开始处的偏移量。

图片来自于: https://www.interdb.jp/pg/pgsql01/03.html

源代码的描述

代码语言:javascript
复制
typedef struct PageHeaderData
{
    /* XXX LSN是*任何*块的成员,不仅限于页面组织的块 */
    PageXLogRecPtr pd_lsn;        /* LSN:指向此页面上次更改的xlog记录的下一个字节 */
    uint16        pd_checksum;    /* 校验和 */
    uint16        pd_flags;        /* 标志位,见下文 */
    LocationIndex pd_lower;        /* 空闲空间开始的偏移量 */
    LocationIndex pd_upper;        /* 空闲空间结束的偏移量 */
    LocationIndex pd_special;    /* 特殊空间开始的偏移量 */
    uint16        pd_pagesize_version;
    TransactionId pd_prune_xid; /* 最旧可修剪的XID,如果没有则为零 */
    ItemIdData    pd_linp[FLEXIBLE_ARRAY_MEMBER]; /* 行指针数组 */
} PageHeaderData;

例如在以下的查询中,可以通过typalign这个字段则表示对齐所需字节数,c -> 1, i -> 4, d -> 8 ,我们参考以下的结果来设计表的结构。

代码语言:javascript
复制
postgres=# select typname, typalign, typlen from pg_type where typname in ('int4', 'int8', 'bool', 'float4', 'float8', 'bytea', 'text', 'timestamptz', 'serial', 'json', 'int8[]');
   typname   | typalign | typlen
-------------+----------+--------
 bool        | c        |      1
 bytea       | i        |     -1
 int8        | d        |      8
 int4        | i        |      4
 text        | i        |     -1
 json        | i        |     -1
 float4      | i        |      4
 float8      | d        |      8
 timestamptz | d        |      8
(9 rows)

接下来我们创建一张实际的表:

代码语言:javascript
复制
postgres=# create table t1(c char, d float8);
CREATE TABLE
postgres=# insert into t1 values('a',1.1);
INSERT 0 1
postgres=# select pg_column_size(t1.*),pg_column_size(c),pg_column_size(d) from t1;
 pg_column_size | pg_column_size | pg_column_size
----------------+----------------+----------------
             40 |              2 |              8
(1 row)

结果显示整行大小是 40 字节,但实际字段大小只有 2 + 8 = 10 字节,加上 24 字节的HeapTupleHeaderDat行头,理论上 34 字节就够了,多出来的 6 字节去哪了 ?

答案是对齐填充的原因,float8 需要 8 字节对齐,char 字段占 2 字节,必须填充 6 字节才能满足 8 字节的对齐要求,这 6 字节就是纯浪费的空间。计算逻辑是 :24(行头)+ 2(char)+ 6(填充)+ 8(float8)=40 字节。

我们调整一下字段的顺序,把 float8 放前面,char 放后面,创建表 t2:

代码语言:javascript
复制
postgres=# create table t2(d float8, c char);
CREATE TABLE
postgres=# insert into t2 values(1.1,'a');
INSERT 0 1
postgres=# select pg_column_size(t2.*),pg_column_size(c),pg_column_size(d) from t2;
 pg_column_size | pg_column_size | pg_column_size
----------------+----------------+----------------
             34 |              2 |              8
(1 row)

结果整行大小直接降到34 字节,没有任何填充空间,完美利用了磁盘,计算逻辑:24(行头)+ 8(float8)+2(char)= 34 字节。

仅仅是调换了两个字段的顺序,每一行就节省了 6 字节,千万级数据量的表,这个差距会被无限放大!

那为什么将占用字节数更大的字段(比如float8)放在表结构的前面,能减少字节填充、节省存储空间,这其实是数据库存储中数据对齐(Data Alignment) 和填充(Padding) 机制的核心问题。

复杂类型的对齐和开销

除了基础类型,我们日常用的 bytea、text、数组、json 这些复杂类型,对齐规则有特殊点,而且部分类型有额外的 header 开销,设计时一定要注意,避免踩坑。

bytea & text

这两个类型都是1 字节对齐,几乎不会产生填充空间,而且 header 开销很小,仅 1 个字节,存储单个字符时,实际占用仅 2 字节(1 字节 header+1 字节数据)

代码语言:javascript
复制
postgres=# create table t_bytea(b bool, ba bytea);
CREATE TABLE
postgres=# insert into t_bytea values(false,'\xFF');
INSERT 0 1
postgres=# select pg_column_size(t_bytea.*) from t_bytea;
 pg_column_size
----------------
             27
(1 row)

在以上的内容中可以看出 text 类型和 bytea 完全一致,无需额外考虑填充。

数组类型(text []、int8 [])

数组类型同样是1 字节对齐,但坑在header 开销极大,哪怕只存一个元素,也需要 20 多字节的 header,这是固定开销,和元素多少无关。

代码语言:javascript
复制
postgres=# create table t_int8_arr(b bool, arr int8[]);
CREATE TABLE
postgres=# insert into t_int8_arr values(false,ARRAY[1]);
INSERT 0 1
postgres=# select pg_column_size(t_int8_arr.*) from t_int8_arr;
 pg_column_size
----------------
             54
(1 row)

而且数组元素越多,整体占用空间会线性增加,设计表时如果数组元素少,建议考虑拆成普通字段,减少 header 浪费。

json

json 类型是1 字节对齐,和 bytea、text 一样,header 开销很小,存储简单 json 时仅占 3-6 字节,是比数组更省空间的选择,适合存储轻量结构化数据。

代码语言:javascript
复制
postgres=# create table t_json(b bool, j json);
CREATE TABLE
postgres=# insert into t_json values(false,'{}'::json);
INSERT 0 1
postgres=# select pg_column_size(t_json.*) from t_json;
 pg_column_size
----------------
             28
(1 row)

掌握了对齐规则和特殊类型的特点,设计 PG 表结构时,只需遵循这 3 个原则,就能从根源上减少空间浪费,

大对齐字段放前面

把 d 类型(8 字节,如 int8、float8、timestamptz)放在最前面,接着放 i 类型(4 字节,如 int4、float4),最后放 c 类型(1 字节,如 bool、char),彻底避免大对齐字段的填充空间。

可变长字段放最后,bool 字段见缝插针

bytea、text、json 这些可变长类型,本身 1 字节对齐,放在最后不会产生填充;而 bool 字段(仅 1 字节)可以插在其他字段的间隙里,比如某个字段占用 25 字节(1 字节对齐),后面可以直接加 bool 字段,完美利用空间,不产生任何填充。

慎用数组类型,优先选 json / 普通字段

如果需要存储少量结构化数据,优先用 json(轻量 header),而非数组(超大 header);如果数组元素固定且少,直接拆成普通字段,比数组更省空间。

PostgreSQL 的字段对齐规则,看似是个小细节,却是表设计的关键,很多人忽略它,导致磁盘空间被大量浪费,数据量越大,问题越严重。

其实优化方法很简单,就是按对齐字节数从大到小排列字段,可变长字段放最后,bool 字段见缝插针,只需这一步,就能让你的表节省 20% 甚至更多的磁盘空间,同时还能提升数据读取效率。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-02-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 河马coding 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • PG 的字段对齐核心规则
  • 复杂类型的对齐和开销
    • bytea & text
    • 数组类型(text []、int8 [])
    • json
      • 大对齐字段放前面
      • 可变长字段放最后,bool 字段见缝插针
      • 慎用数组类型,优先选 json / 普通字段
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档