PostgreSQL tuple alignment padding (行,字段对齐) - 对齐规则,以及如何选择字段顺序

本文涉及的产品
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
简介: postgresql 数据库 字段选择

背景
PostgreSQL tuple内部有ALIGN机制,因此字段顺序选择实际上也是有讲究的,选择不好,可能因为ALIGN导致占用空间放大。

src/backend/access/common/heaptuple.c

  • Before Postgres 8.3 varlenas always had a 4-byte length header, and
  • therefore always needed 4-byte alignment (at least). This wasted space
  • for short varlenas, for example CHAR(1) took 5 bytes and could need up to
  • 3 additional padding bytes for alignment.
  • Now, a short varlena (up to 126 data bytes) is reduced to a 1-byte header
  • and we don't align it. To hide this from datatype-specific functions that
  • don't want to deal with it, such a datum is considered "toasted" and will
  • be expanded back to the normal 4-byte-header format by pg_detoast_datum.
  • (In performance-critical code paths we can use pg_detoast_datum_packed
  • and the appropriate access macros to avoid that overhead.) Note that this
  • conversion is performed directly in heap_form_tuple, without invoking
  • tuptoaster.c.
    https://www.postgresql.org/docs/devel/static/catalog-pg-type.html

对齐规则
tuple对齐规则

src/include/access/tupmacs.h

/*

  • att_align_datum aligns the given offset as needed for a datum of alignment
  • requirement attalign and typlen attlen. attdatum is the Datum variable
  • we intend to pack into a tuple (it's only accessed if we are dealing with
  • a varlena type). Note that this assumes the Datum will be stored as-is;
  • callers that are intending to convert non-short varlena datums to short
  • format have to account for that themselves.
    */

define att_align_datum(cur_offset, attalign, attlen, attdatum) \

(

    ((attlen) == -1 && VARATT_IS_SHORT(DatumGetPointer(attdatum))) ? \  
    (uintptr_t) (cur_offset) : \  
    att_align_nominal(cur_offset, attalign) \  

)

/*

  • att_align_pointer performs the same calculation as att_align_datum,
  • but is used when walking a tuple. attptr is the current actual data
  • pointer; when accessing a varlena field we have to "peek" to see if we
  • are looking at a pad byte or the first byte of a 1-byte-header datum.
  • (A zero byte must be either a pad byte, or the first byte of a correctly
  • aligned 4-byte length word; in either case we can align safely. A non-zero
  • byte must be either a 1-byte length word, or the first byte of a correctly
  • aligned 4-byte length word; in either case we need not align.)
  • Note: some callers pass a "char *" pointer for cur_offset. This is
  • a bit of a hack but should work all right as long as uintptr_t is the
  • correct width.
    */

define att_align_pointer(cur_offset, attalign, attlen, attptr) \

(

    ((attlen) == -1 && VARATT_NOT_PAD_BYTE(attptr)) ? \  
    (uintptr_t) (cur_offset) : \  
    att_align_nominal(cur_offset, attalign) \  

)

/*

  • att_align_nominal aligns the given offset as needed for a datum of alignment
  • requirement attalign, ignoring any consideration of packed varlena datums.
  • There are three main use cases for using this macro directly:
    • we know that the att in question is not varlena (attlen != -1);
  • in this case it is cheaper than the above macros and just as good.
    • we need to estimate alignment padding cost abstractly, ie without
  • reference to a real tuple. We must assume the worst case that
  • all varlenas are aligned.
    • within arrays, we unconditionally align varlenas (XXX this should be
  • revisited, probably).
  • The attalign cases are tested in what is hopefully something like their
  • frequency of occurrence.
    */

define att_align_nominal(cur_offset, attalign) \

(

    ((attalign) == 'i') ? INTALIGN(cur_offset) : \  
     (((attalign) == 'c') ? (uintptr_t) (cur_offset) : \  
      (((attalign) == 'd') ? DOUBLEALIGN(cur_offset) : \  
       ( \  
                    AssertMacro((attalign) == 's'), \  
                    SHORTALIGN(cur_offset) \  
       ))) \  

)
通过pg_attribute系统表,查看对齐情况
https://www.postgresql.org/docs/devel/static/catalog-pg-type.html

typalign is the alignment required when storing a value of this type. It applies to storage on disk as well as most representations of the value inside PostgreSQL. When multiple values are stored consecutively, such as in the representation of a complete row on disk, padding is inserted before a datum of this type so that it begins on the specified boundary. The alignment reference is the beginning of the first datum in the sequence.

Possible values are:

c = char alignment, i.e., no alignment needed.

s = short alignment (2 bytes on most machines).

i = int alignment (4 bytes on most machines).

d = double alignment (8 bytes on many machines, but by no means all).
表示在这个字段前面必须已经以以上大小对齐。

例如,

c表示,这个字段的前面已占用空间(包括24字节的tuple head)必须1字节的倍数。如果不是,则前面一个字段末尾必须有padding,使得符合这个对齐条件。

s表示,这个字段前面已占用空间(包括24字节的tuple head)必须是2字节的倍数。如果不是,则前面一个字段末尾必须有padding,使得符合这个对齐条件。

i表示,这个字段前面已占用空间(包括24字节的tuple head)必须是4字节的倍数。如果不是,则前面一个字段末尾必须有padding,使得符合这个对齐条件。

d表示,这个字段前面已占用空间(包括24字节的tuple head)必须是8字节的倍数。如果不是,则前面一个字段末尾必须有padding,使得符合这个对齐条件。

例如
1、

1,4,8
会变成

1,3(padding),4,8
2、

1,1,8
会变成

1,1,6(padding),8
3、

1,1,1,8
会变成

1,1,1,5,8
例子
1、空行

postgres=# select pg_column_size(row());

pg_column_size

         24  

(1 row)
2、PADDING

postgres=# select pg_column_size(row(char 'a', char 'b', int4 '1'));

pg_column_size

         32  

(1 row)

postgres=# select pg_column_size(row(char 'a', char 'b', int8 '1'));

pg_column_size

         40  

(1 row)
相关文档举例
https://blog.2ndquadrant.com/on-rocks-and-sand/

1、

SELECT pg_column_size(ROW()) AS empty,

   pg_column_size(ROW(0::SMALLINT)) AS byte2,  
   pg_column_size(ROW(0::BIGINT)) AS byte8,  
   pg_column_size(ROW(0::SMALLINT, 0::BIGINT)) AS byte16;  -- 24,2,6(padding),8   

2、包含padding的表

CREATE TABLE user_order (
is_shipped BOOLEAN NOT NULL DEFAULT FALSE,
user_id BIGINT NOT NULL,
order_total NUMERIC NOT NULL,
order_dt TIMESTAMPTZ NOT NULL,
order_type SMALLINT NOT NULL,
ship_dt TIMESTAMPTZ,
item_ct INT NOT NULL,
ship_cost NUMERIC,
receive_dt TIMESTAMPTZ,
tracking_cd TEXT,
id BIGSERIAL PRIMARY KEY NOT NULL
);
查看这个表的对齐规则

SELECT a.attname, t.typname, t.typalign, t.typlen
FROM pg_class c
JOIN pg_attribute a ON (a.attrelid = c.oid)
JOIN pg_type t ON (t.oid = a.atttypid)
WHERE c.relname = 'user_order'
AND a.attnum >= 0
ORDER BY a.attnum;

attname typname typalign typlen
is_shipped bool c 1
user_id int8 d 8
order_total NUMERIC i -1
order_dt timestamptz d 8
order_type int2 s 2
ship_dt timestamptz d 8
item_ct int4 i 4
ship_cost NUMERIC i -1
receive_dt timestamptz d 8
tracking_cd text i -1
id int8 d 8

3、插入测试数据

135MB

INSERT INTO user_order (

is_shipped, user_id, order_total, order_dt, order_type,  
ship_dt, item_ct, ship_cost, receive_dt, tracking_cd  

)
SELECT TRUE, 1000, 500.00, now() - INTERVAL '7 days',

   3, now() - INTERVAL '5 days', 10, 4.99,  
   now() - INTERVAL '3 days', 'X5901324123479RROIENSTBKCV4'  

FROM generate_series(1, 1000000);

SELECT pg_relation_size('user_order') AS size_bytes,

   pg_size_pretty(pg_relation_size('user_order')) AS size_pretty;  
size_bytes size_pretty
141246464 135 MB

4、调整字段顺序,可以把padding消除掉,基于前面说的规则即可。

SELECT pg_column_size(ROW()) AS empty_row,

   pg_column_size(ROW(0::NUMERIC)) AS no_val,  
   pg_column_size(ROW(1::NUMERIC)) AS no_dec,  
   pg_column_size(ROW(9.9::NUMERIC)) AS with_dec,  
   pg_column_size(ROW(1::INT2, 1::NUMERIC)) AS col2,  
   pg_column_size(ROW(1::INT4, 1::NUMERIC)) AS col4,  
   pg_column_size(ROW(1::NUMERIC, 1::INT4)) AS round8;  

SELECT pg_column_size(ROW()) AS empty_row,

   pg_column_size(ROW(''::TEXT)) AS no_text,  
   pg_column_size(ROW('a'::TEXT)) AS min_text,  
   pg_column_size(ROW(1::INT4, 'a'::TEXT)) AS two_col,  
   pg_column_size(ROW('a'::TEXT, 1::INT4)) AS round4;  

SELECT pg_column_size(ROW()) AS empty_row,

   pg_column_size(ROW(1::SMALLINT)) AS int2,  
   pg_column_size(ROW(1::INT)) AS int4,  
   pg_column_size(ROW(1::BIGINT)) AS int8,  
   pg_column_size(ROW(1::SMALLINT, 1::BIGINT)) AS padded,  
   pg_column_size(ROW(1::INT, 1::INT, 1::BIGINT)) AS not_padded;  

5、消除tuple padding的优化,字段顺序如下

5.1、定长字段(从大到小)

5.2、变长字段

DROP TABLE user_order;

CREATE TABLE user_order (
id BIGSERIAL PRIMARY KEY NOT NULL,
user_id BIGINT NOT NULL,
order_dt TIMESTAMPTZ NOT NULL,
ship_dt TIMESTAMPTZ,
receive_dt TIMESTAMPTZ,
item_ct INT NOT NULL,
order_type SMALLINT NOT NULL,
is_shipped BOOLEAN NOT NULL DEFAULT FALSE,
tracking_cd TEXT,
order_total NUMERIC NOT NULL,
ship_cost NUMERIC
);

INSERT INTO user_order (

is_shipped, user_id, order_total, order_dt, order_type,  
ship_dt, item_ct, ship_cost, receive_dt, tracking_cd  

)
SELECT TRUE, 1000, 500.00, now() - INTERVAL '7 days',

   3, now() - INTERVAL '5 days', 10, 4.99,  
   now() - INTERVAL '3 days', 'X5901324123479RROIENSTBKCV4'  

FROM generate_series(1, 1000000);

postgres=# dt+ user_order

                   List of relations  
Schema Name Type Owner Size Description
public user_order table postgres 112 MB

(1 row)
6、优化后的padding情况,可以看到已经消除了padding,空间降低到112MB。

SELECT a.attname, t.typname, t.typalign, t.typlen
FROM pg_class c
JOIN pg_attribute a ON (a.attrelid = c.oid)
JOIN pg_type t ON (t.oid = a.atttypid)
WHERE c.relname = 'user_order'
AND a.attnum >= 0
ORDER BY t.typlen DESC;

attname typname typalign typlen
id int8 d 8
user_id int8 d 8
order_dt timestamptz d 8
ship_dt timestamptz d 8
receive_dt timestamptz d 8
item_ct int4 i 4
order_type int2 s 2
is_shipped bool c 1
tracking_cd text i -1
ship_cost NUMERIC i -1
order_total NUMERIC i -1

小结
消除tuple PADDING, 字段顺序规则:

1、定长字段(从大到小)

2、变长字段

本文的例子简单的说明了padding引入的TUPLE变大的情况,使用以上规则调整字段顺序后,空间占用下降了10%左右。

参考
src/include/access/tupmacs.h

src/backend/access/common/heaptuple.c

https://blog.2ndquadrant.com/on-rocks-and-sand/

《Greenplum 优化CASE - 对齐JOIN字段类型,使用数组代替字符串,降低字符串处理开销,列存降低扫描开销》

《PostgreSQL 10.0 preview 性能增强 - pg_xact align(cacheline对齐)》

《未对齐(alignment)造成SSD 写放大一例》

https://yq.aliyun.com/articles/237

https://www.pgcon.org/2012/schedule/attachments/258_212_Internals%20Of%20PostgreSQL%20Wal.pdf

https://www.postgresql.org/docs/devel/static/runtime-config-wal.html#RUNTIME-CONFIG-WAL-SETTINGS

https://www.postgresql.org/docs/devel/static/wal-reliability.html
转自阿里云德哥

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
目录
相关文章
|
关系型数据库 PostgreSQL
PostgreSQL tuple alignment padding (行,字段对齐) - 对齐规则,以及如何选择字段顺序
标签 PostgreSQL , 对齐 , 变长 , 行 , tuple , row , alignment , padding 背景 PostgreSQL tuple内部有ALIGN机制,因此字段顺序选择实际上也是有讲究的,选择不好,可能因为ALIGN导致占用空间放大。
961 0
|
26天前
|
关系型数据库 分布式数据库 数据库
成都晨云信息技术完成阿里云PolarDB数据库产品生态集成认证
近日,成都晨云信息技术有限责任公司(以下简称晨云信息)与阿里云PolarDB PostgreSQL版数据库产品展开产品集成认证。测试结果表明,晨云信息旗下晨云-站群管理系统(V1.0)与阿里云以下产品:开源云原生数据库PolarDB PostgreSQL版(V11),完全满足产品兼容认证要求,兼容性良好,系统运行稳定。
|
1月前
|
关系型数据库 分布式数据库 数据库
PolarDB常见问题之数据库不能自己减少节点如何解决
PolarDB是阿里云推出的下一代关系型数据库,具有高性能、高可用性和弹性伸缩能力,适用于大规模数据处理场景。本汇总囊括了PolarDB使用中用户可能遭遇的一系列常见问题及解答,旨在为数据库管理员和开发者提供全面的问题指导,确保数据库平稳运行和优化使用体验。
|
1月前
|
缓存 关系型数据库 分布式数据库
PolarDB常见问题之数据库cpu突然飙高如何解决
PolarDB是阿里云推出的下一代关系型数据库,具有高性能、高可用性和弹性伸缩能力,适用于大规模数据处理场景。本汇总囊括了PolarDB使用中用户可能遭遇的一系列常见问题及解答,旨在为数据库管理员和开发者提供全面的问题指导,确保数据库平稳运行和优化使用体验。
|
2月前
|
关系型数据库 分布式数据库 数据库
阿里云PolarDB登顶2024中国数据库流行榜:技术实力与开发者影响力
近日,阿里云旗下的自研云原生数据库PolarDB在2024年中国数据库流行度排行榜中夺冠,并刷新了榜单总分纪录,这一成就引起了技术圈的广泛关注。这一成就源于PolarDB在数据库技术上的突破与创新,以及对开发者和用户的实际需求的深入了解体会。那么本文就来分享一下关于数据库流行度排行榜的影响力以及对数据库选型的影响,讨论PolarDB登顶的关键因素,以及PolarDB“三层分离”新版本对开发者使用数据库的影响。
74 3
阿里云PolarDB登顶2024中国数据库流行榜:技术实力与开发者影响力
|
1月前
|
关系型数据库 分布式数据库 数据库
PolarDB PostgreSQL版:Oracle兼容的高性能数据库
PolarDB PostgreSQL版是一款高性能的数据库,具有与Oracle兼容的特性。它采用了分布式架构,可以轻松处理大量的数据,同时还支持多种数据类型和函数,具有高可用性和可扩展性。它还提供了丰富的管理工具和性能优化功能,为企业提供了可靠的数据存储和处理解决方案。PolarDB PostgreSQL版在数据库领域具有很高的竞争力,可以满足各种企业的需求。
|
1天前
|
关系型数据库 OLAP 分布式数据库
「杭州*康恩贝」4月26日PolarDB开源数据库沙龙,开启报名!
4月26日周五,PolarDB开源社区联合康恩贝将共同举办开源数据库技术沙龙,本次沙龙我们邀请了众多数据库领域的专家,期待大家的参与!
|
11天前
|
运维 关系型数据库 分布式数据库
「合肥 * 讯飞」4 月 19 日 PolarDB 开源数据库沙龙,报名中!
4月19日周五,PolarDB开源社区联合科大讯飞共同举办开源数据库技术沙龙,本次沙龙我们邀请了众多数据库领域的专家,期待大家的参与!
「合肥 * 讯飞」4 月 19 日 PolarDB 开源数据库沙龙,报名中!
|
1月前
|
存储 关系型数据库 分布式数据库
PolarDB常见问题之PolarDB突然有大量服务连不上数据库如何解决
PolarDB是阿里云推出的下一代关系型数据库,具有高性能、高可用性和弹性伸缩能力,适用于大规模数据处理场景。本汇总囊括了PolarDB使用中用户可能遭遇的一系列常见问题及解答,旨在为数据库管理员和开发者提供全面的问题指导,确保数据库平稳运行和优化使用体验。
|
1月前
|
存储 关系型数据库 MySQL
TiDB与MySQL、PostgreSQL等数据库的比较分析
【2月更文挑战第25天】本文将对TiDB、MySQL和PostgreSQL等数据库进行详细的比较分析,探讨它们各自的优势和劣势。TiDB作为一款分布式关系型数据库,在扩展性、并发性能等方面表现突出;MySQL以其易用性和成熟性受到广泛应用;PostgreSQL则在数据完整性、扩展性等方面具有优势。通过对比这些数据库的特点和适用场景,帮助企业更好地选择适合自己业务需求的数据库系统。