PostgreSQL变长存储

PostgreSQL使用TOAST机制来处理超过页面大小的变长数据,包括压缩和行外存储。存储策略包括plain、main、external和extended,由用户在创建表时选择。数据类型通过指针的第一个字节识别,如1B头和4B头,并有特定的struct对应不同存储形式。TOAST表的oid可以通过查询pg_class获得,且与普通表通过relkind区分。
摘要由CSDN通过智能技术生成

PostgreSQL变长存储

目的就是存储变长的attribute,FormData_pg_attribute中标志位:attbyval = false,相应的attlen = -1;

  • 怎么存:短的存在结构体中(结构体大小固定),长的存在到toast表中;
  • 怎样算长:“并非所有变长的数据类型才会触发TOAST机制,只有属性超过BLCKSZ/4字节数据时,TOAST机制才会触发”

PostgreSQL使用固定的页面尺寸(通常是8KB),并且不允许元组跨越多个额页面。因此不可能直接存储非常大的域值。为了克服这个限制,大的域值会被压缩并/或分解成多个物理行。这些处理对用户都是透明的,只是在大部分的后端代码上有一些小的影响。这个技术的昵称是TOAST(或者“切片面包之后的最好的东西”)。TOAST 机制也被用来提升内存中大型数据值的处理。


  • 存储策略:这个attribute怎么存需要约束和选择,标志在FormData_pg_attribute的attstorage中:
  1. plain:避免压缩或行外存储
  2. main:允许压缩,不允许行外存储
  3. external:允许行外存储,不允许压缩
  4. extended:允许压缩和行外存储

这些存储策略是由用户在创建表时通过SQL选择使用的,也可以在以后进行修改,对应:

	/*----------
	 * attstorage 可能的取值如下:
	 *		'p': 即上面的PLAIN,避免压缩或线外存储
	 *		'e': 即上面的EXTERNAL,允许线外存储,但不允许压缩。
	 *		'm': 即上面的MAIN,允许压缩,但不允许线外存储。
	 *		'x': 即上面的EXTENDED,允许压缩和线外存储,也是默认策略。
	 *----------
	 */
	char		attstorage;

PS: 查询 pg_class 表,其 reltoastrelid 就是 toast 表的 oid,toast 表的表名后缀为普通表的 oid,举个例子:

普通表 t,oid 为 10022077,reltoastrelid 为 10022080

toast 表为:pg_toast_10022077,oid 为 10022080。

普通表与 toast 表在 pg_class 中通过字段 relkind 与可以区别,普通表为 ‘r’,toast 表为 ‘t’

Design to Code (变长attr操作)

从拿到一个指针开始,做两步

1. 解析类型->转换

2. 解析数据

第一步:解析类型

  1. 所有变长类型的【指针】的第一个字节识别使用的哪种struct存储数据:
  • 0x00 (xxxx xxx0):is 4B(未压缩数据用varattrib_4b,最大1G)
  • 0x00 (xxxx xx00):is 4B_U(未压缩数据用varattrib_4b,第二个二进制如果没用使用且长度满足,可以把4B转换为1B)
  • 0x00 (xxxx xx10):is 4B_C(压缩数据用varattrib_4b,最大1G,用于判断是否压缩)
  • 0x00 (xxxx xxx1):is 1B(未压缩数据,1B断类型,最大126b)
  • 0x00 (0000 0001):is 1B_E(TOAST专用)

对应的宏:

#define VARATT_IS_4B(PTR) ((((varattrib_1b*)(PTR))->va_header & 0x01) == 0x00)
#define VARATT_IS_4B_U(PTR) ((((varattrib_1b*)(PTR))->va_header & 0x03) == 0x00)
#define VARATT_IS_4B_C(PTR) ((((varattrib_1b*)(PTR))->va_header & 0x03) == 0x02)
#define VARATT_IS_1B(PTR) ((((varattrib_1b*)(PTR))->va_header & 0x01) == 0x01)
#define VARATT_IS_1B_E(PTR) ((((varattrib_1b*)(PTR))->va_header) == 0x01)
  1. 对应的3种struct:
  • varlena 最通用(早)变长类型数据结构:
struct varlena
{
	char		vl_len_[4];		/*标志值的总长度*/
	char		vl_dat[FLEXIBLE_ARRAY_MEMBER];	/*存放数据内容*/
};

但现在大多数数据类型就是不直接使用这个struct,都使用1B头和4B头;

  • 1B头

当然va_header中1个bit标志自身1B头,剩下7位标志数据长度,所以1B的头能存储的数据长度不超过127Byte的数据

typedef struct
{
    uint8       va_header;
    char        va_data[FLEXIBLE_ARRAY_MEMBER]; /* Data begins here */
} varattrib_1b;

第一个字节如果等于0000 0001,那么就是varattrib_1b_e,这个结构体通常存一个toast指针,并不存数据,体现在var_tag: varatt_external, varatt_indirect, varatt_expanded标志三种toast pointer指针,指针数据在va_data中:

typedef struct
{
    uint8       va_header;                          /* Always 0x80(大端序) or 0x01 (小端序)*/
    uint8       va_tag;                             /* Type of datum, 指出 va_data 域中TOAST指针的种类*/
    char        va_data[FLEXIBLE_ARRAY_MEMBER];       /* Type-specific data,存放三种不同的TOAST指针 */
} varattrib_1b_e;

同时需要注意的是varattrib_1b_e 类型和varattrib_1b一样,内部的字段都是未对齐的(因为va_data是一个char数组),因此如果需要访问对应的va_data字段,只能使用memcpy的方法,将其va_data范围内的数据copy到varatt_external, varatt_indirect, varatt_expanded结构体中(后说),然后在对其进行访问,使用宏:VARATT_EXTERNAL_GET_POINTER。

  • 4B头
    这种数据形式一般被称为“ flat”的形式,即没有经过TOAST机制的行外存储,也就是说其va_data中存储就是实际的变长数据。

varattrib_4b用union提供了两套用法:存一般数据(4B_U);存压缩数据(4B_C),4B头共32个二进制位,低2位存控制数据,长度保存在高30位中,即最大存1GB(2^30 - 1)字节的数据:

typedef union
{
    // 这种数据形式一般被称为“ flat”的形式,即没有经过TOAST机制的行外存储,也就是说其存储仍然是连续的。
    struct                      /* Normal varlena (4-byte length) */
     {
        uint32      va_header;
        char        va_data[FLEXIBLE_ARRAY_MEMBER];
     } va_4byte;
    struct                      /* Compressed-in-line format */
     {
        uint32      va_header;
        uint32      va_rawsize;/* Original data size (excludes header) */
        char        va_data[FLEXIBLE_ARRAY_MEMBER]; /* Compressed data */
     } va_compressed;
} varattrib_4b;
  • 对于未压缩的数据,使用va_4byte结构体存储,这是一个最原始的变长类型。观察va_4byte对象,其内部的定义和一个原始的varlena结构体一样,所以varattrib_4b.va_4byte是变长类型中唯一和TOAST没关系的类型。

  • 对于压缩的数据,使用va_compressed结构体存储,其内部存储了数据压缩之前的大小。

4B到1B的转换:4B的头是4字节,1B的头是1字节,转换也提供了几个宏,简单的说就是判断数据len能不能被1B装得下:VARATT_CAN_MAKE_SHORT

  1. Toast指针(3种)
  • varatt_external 行外存储toast指针

struct varatt_external是一个传统的:TOAST指针“(也就在官方文档中提到的基于物理存储的TOAST指针)。 也就是说,其包含了行外存储在TOAST表中的Datum所需的信息。 仅当va_extsize < va_rawsize - VARHDRSZ 时,才压缩数据。该结构不得包含任何padding,因为我们有时会使用memcmp比较这些结构体。

由于varatt_external并未对齐地存储在实际的元组中,因此,在查看这些字段之前,需要将元组中的数据 memcpy 到本地struct变量中,然后才可以查看里面的字段! (我们之所以使用 memcmp ,是为了避免通过比较两个指针内部的字段值判断两个指针相等,而只检测两个TOAST指针(结构体)的值是否相等就可以了。

typedef struct varatt_external
{
	int32		va_rawsize;  //原始数据大小,包含文件头。
	int32		va_extsize;	 //线外存储的大小,不包含文件头。
	Oid			va_valueid;	 //在toast表中值的独有的Oid,也就是线外存储的数据的Oid。
	Oid			va_toastrelid; //toast表的Oid
}varatt_external;
  • varatt_indirect(指向varlena的TOAST指针)
    truct varatt_indirect只是一个varlena指针,可以指向varatt_external,varatt_expanded,或者是varattrib_1b,varattrib_4b 类型的原始数据。其指向的必须是存储在内存中而不是行外磁盘存储的toast关系中的Datum。 创建者就需要完全负责被引用的空间的生存周期, 只要该引用Datum指针存在。
typedef struct varatt_indirect
{
	struct varlena *pointer; //指向内存的可变属性指针
} varatt_indirect;

  • varatt_expanded (内存扩展存储的TOAST指针)
    struct varatt_expanded是一个“ TOAST指针”,表示存储在内存中的行外数据, 采用某种特定于类型的,不一定物理连续的格式,便于计算而不是存储。 src/include/utils/expandeddatum.h中提供了 ExpandedObjectHeader 类型的操作API。<暂未深入>
typedef struct ExpandedObjectHeader ExpandedObjectHeader;
typedef struct varatt_expanded
{
 ExpandedObjectHeader *eohptr;
} varatt_expanded;

Toast操作【搬运:https://blog.csdn.net/weixin_45644897/article/details/120833840】

pg:src/backend/access/heap/tuptoaster.c

  • toast_insert_or_update 负责更新和插入元组
  • toast_delete 删除TOAST元组
  • heap_tuple_untoast_attr 获取TOAST元组

FormData_pg_attribute表属性

  • attrelid attr的relation属主
  • attname 名字
  • atttypid 类型 (类型的oid,在pg_type表里存的)
  • attstorage 见上
  • attbyval 是不是变长存储
  • attlen 长度
  • attalign 存储此类型的值时所需的对齐方式,当连续存储多个值时(例如在磁盘上完整行的表示中),将在此类型的数据之前插入填充,以使填充开始于指定的边界。对齐参考是序列中第一个基准的起点。(也是pg_type里的copy)
c = char对齐,即无需对齐。
s = short对齐(在大多数计算机上为 2 字节)。
i = int对齐(在大多数计算机上为 4 字节)。
d = double对齐(在许多机器上为 8 字节,但绝不是全部)。
  • attnum relation里面排第几
  • attndims att有几维,非数组类型为0
  • attcacheoff 这个attr在tuple里的偏移,之前说到每个attr可能是变长的,具体多长还需要解析,对于一个tuple来说,如果访问第5个attr,如果每次都要通过计算前四个attr的长度找到第五个attr的偏移很费时间,这里把计算出来的值放里面节省后面访问这个attr的开销

当数据自然对齐时,CPU可以有效地对内存执行读写操作。因此,PostgreSQL中的每种数据类型都有特定的对齐要求。当在一个元组中连续存储多个属性时,在属性之前插入填充,使其从所需的对齐边界开始。更好地理解这些对齐要求可能有助于减少在磁盘上存储元组时所需的填充量,从而节省磁盘空间。
Postgres中的数据类型分为以下几类:

  1. Pass-by-value, fixed length: 通过值传递给Postgres内部例程并具有固定长度的数据类型属于这种类型。长度可以是1、2或4个字节(64位系统上为8个字节)。
  2. Pass-by-reference, fixed length: 对于这些数据类型,内存堆页中的地址引用被发送到内部Postgres例程。它们也有固定的长度。
  3. Pass-by_reference, variable length: 对于可变长度的数据类型,Postgres在实际数据之前添加一个varlena头。它存储了一些关于数据如何实际存储在磁盘上的信息(未压缩、压缩或TOASTed)以及数据的实际长度。对于 TOAST 属性,实际数据存储在一个单独的关系中。在这些情况下,varlena标头后面跟着一些关于数据在相应TOAST关系中的实际位置的信息。
    通常,varlena头的磁盘上大小是1字节。但是,如果不能 TOAST 数据,并且未压缩数据的大小超过126个字节,则使用4个字节的头。例如,

https://developer.aliyun.com/article/834875

Numeric(precision, scale):精度最高
2345.78 (6, 2)
内存中的数据结构:struct NumericVar
Nbase: 10000 表示单个digit的范围0-9999, 用两个字节覆盖int16
ndigit:表示数字占用digit的个数(有效digit位数)
weight:数组最高位的权重,及小数点的偏移,可为正、负、零;例如为1,最高位需要乘以10000,为-1最高位乘以10-4,scale表示小数位数

例如:12345.06789

  • ndigits: 4
  • weight: 1 (1 * 10^4)
  • sign: 0
  • scale: 5
  • digits: 0001 2345 0678 9000
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值