postgreSQL源码分析——存储管理——外存管理(6)

2021SC@SDUSC

概述

这次来分析postgreSQL关于大数据的存储方式,一共有两种方式:TOAST机制和大对象机制,这次就先来分析TOAST机制。
TOAST,全称为The Oversized-Attribute Storage Technique,中文名为超尺寸属性存储技巧,是postgreSQL提供的一种存储大数据的机制。

源码分析

TOAST的起源

在postgreSQL中存在可变长度的数据类型:

  • varchar(n)
  • text
  • ……

导致的问题
这就导致一个问题的出现,我们假设这样一种极限的状态:一个text非常大,大到比一个表块还要大的时候,它该如何存放呢?
通过查阅官方的文档,发现了官方对这一问题的解答:

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

这说明,在原本限制的情况下,单个数据类型的大小是有极限的,只能限制在单个块内。而为了解决大数据的存放问题,TOAST应运而生。当然,这并不意味着只要有变长类型就会触发TOAST机制,只有在准备向支持TOAST的属性中存储超过BLCKSZ/4 (默认为8K/4=2K)字节的数据时,TOAST机制才会触发。如官方文档所说,TOAST机制会将要存储的数据进行压缩或线外存储(把数据存储在其他的表中),直到数据低于刚才提到的阈值,或者没有办法获得更好的结果时才会停止。

变长类型数据结构
关于变长类型的数据结构,位于src/include/c.h文件中。

struct varlena
{
	char		vl_len_[4];		/*表示值的总长度*/
	char		vl_dat[FLEXIBLE_ARRAY_MEMBER];	/*存放数据内容*/
};

先分析一下这个数据结构,vl_len数组有4个字节,也就是32位。
TOAST占用使用变长类型的长度字(vl_len)的最高两个二进制位( 大端法机器上的高位,小端法机器上的低位),这样就把任何可TOAST值的逻辑长度限制在1GB((230 - 1)字节)。这两个二进制位的作用如下:

  • 00,表示数值是该数据类型的一个普通的未TOAST的数值,余下的30位表示数据的大小。
  • 01,表示该数据已经被压缩了,使用该数据前必须先解压。余下的30位像上面说的,表示压缩后数据的大小,并且在这四个字节之后还会附加32位来表示压缩之前的数据大小。
  • 1x,第一位为1时,表明数据头部仅使用了1个字节。这时会出现两种情况:
    1. 当这个字节为10000000时,表示这是线外存储的数据,跟在后面的1个字节用于记录指针的大小(TOAST_POINTER_SIZE),数据区域用于记录TOAST指针。
    2. 当该字节剩余部分不全为0时,表明存储的为短字符串(<128B),该字节剩下的7位用于表示字符串的长度。

但这样表示的话会漏掉一种情况,也就是数据可能在线外存储时也可能会被压缩,这种情况就需要通过TOAST指针中的va_rawsize和va_extsize来进行比较。
上面提到的压缩
源码位于src/common/pg_lzcompress.c,是 LZ 压缩技术家族中一种相对简单且非常快速的成员。这里不是我分析的重点,因此就不展开讲了。有时间的话我会专门开一帖博客来分析这个压缩算法的源码。

TOAST的原理

TOAST指针
源码位于src/include/postgres.h

//磁盘线外存储指针
typedef struct varatt_external
{
	int32		va_rawsize;  //原始数据大小,包含文件头。
	int32		va_extsize;	 //线外存储的大小,不包含文件头。
	Oid			va_valueid;	 //在toast表中值的独有的Oid,也就是线外存储的数据的Oid。
	Oid			va_toastrelid; //toast表的Oid
}			varatt_external;

//内存线外存储指针
typedef struct varatt_indirect
{
	struct varlena *pointer; //指向内存的可变属性指针
}	varatt_indirect;

前面提到过既压缩又线外存储的情况,就可以根据这个结构体的属性来进行判断:当 va_extsize < va_rawsize - VARHDRSZ 时,就说明这个数据被压缩了。这个公式很简单,表达的意思是 线外存储的大小(不包含文件头) 如果小于(原始数据大小(包含文件头)-文件头大小)时,很显然,小的那部分数据是因为被压缩了。
文件头的定义如下:

#define VARHDRSZ		((int32) sizeof(int32))

顾名思义,VARHDRSZ全名就是Var Header Size。
存储策略
TOAST代码代码识别四种不同的在磁盘上存储可TOAST列的策略:

  1. PLAIN避免压缩或者线外存储;而且它禁用变长类型的单字节头部。这是不可TOAST数据类型列的唯一可能的策略。只是对那些不能TOAST的数据类型才有可能。

  2. EXTENDED允许压缩和线外存储。这是大多数可TOAST数据类型的默认策略。 首先将尝试进行压缩,如果行仍然太大,那么则进行线外存储。

  3. EXTERNAL允许线外存储,但是不许压缩。使用EXTERNAL将令那些在宽text和 bytea列上的子串操作更快(代价是增加了存储空间), 因此这些操作被优化为只抓取未压缩线外数据中需要的部分。

  4. MAIN允许压缩,但不允许线外存储(实际上,在这样的列上仍然会进行线外存储,但只是作为没有办法把行变得足以放入一页的情况下的最后手段)。

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

相关源码如下(位于src/include/catalog/pg_attribute.h):
位于一个名为FormData_pg_attribute的结构体内,由于比较长,我只放上相关属性和注释:

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

TOAST的文件

主要源码位于这里src/backend/access/heap/toasting.c
本来想在源码里找一下toast表的结构,结果找了半天没有找到。于是就研究了一下关于toast表的创建函数,发现确实不存在toast表的结构体,但函数大概声明了toast表文件的结构。部分源码如下:

tupdesc = CreateTemplateTupleDesc(3);
	TupleDescInitEntry(tupdesc, (AttrNumber) 1,
					   "chunk_id",
					   OIDOID,
					   -1, 0);
	TupleDescInitEntry(tupdesc, (AttrNumber) 2,
					   "chunk_seq",
					   INT4OID,
					   -1, 0);
	TupleDescInitEntry(tupdesc, (AttrNumber) 3,
					   "chunk_data",
					   BYTEAOID,
					   -1, 0);

可以从里面窥探出,toast表一共由三个属性组成,我用表格的形式来描述一下:

属性名数据类型简介
chunk_idOID线外存储时为整个TOAST数据分配的OID
chunk_seqINT4序列号,存储该片段在整个TOAST数据中的位置
chunk_dataBYTEA存储该片段实际的数据

当然,这个TOAST表结构只有在线外存储时才会启用。线外数据会被分割成不超过TOAST_MAX_CHUNK_SIZE(下面有详细介绍)字节的片段,每个片段都作为独立的元组存储在相关联的TOAST表中。
根据表的结构,每一个被线外存储的TOAST数据都会被分配一个OID,通过这个OID可以在TOAST表中找到属于该TOAST数据的所有片段,进而可以重组该数据,同时,它也是构成上面提到的TOAST指针的一部分。
TOAST片段的大小限制
TOAST的chunk(片段)的最大大小TOAST_MAX_CHUNK_SIZE定义如下(位于src/include/access/tuptoaster.h):

//定义toast片段的最大大小 大概就是线外存储的元组的最大大小减去一系列头部数据的大小
#define TOAST_MAX_CHUNK_SIZE	\
	(EXTERN_TUPLE_MAX_SIZE -							\
	 MAXALIGN(SizeofHeapTupleHeader) -					\
	 sizeof(Oid) -										\
	 sizeof(int32) -									\
	 VARHDRSZ)

//线外存储的元组的最大大小 = 每个元组最大字节数
#define EXTERN_TUPLE_MAX_SIZE	MaximumBytesPerTuple(EXTERN_TUPLES_PER_PAGE)

//每个TOAST页(块)包含的元组数量
#define TOAST_TUPLES_PER_PAGE	4

//用于计算每个元组最大字节数的函数
#define MaximumBytesPerTuple(tuplesPerPage) \
	MAXALIGN_DOWN((BLCKSZ - \
				   MAXALIGN(SizeOfPageHeaderData + (tuplesPerPage) * sizeof(ItemIdData))) \
				  / (tuplesPerPage))

这里要讲解一个知识点:换行标记: \

在C语言程序编写中,我们有时会遇到一行代码太长而影响阅读或者出现与部分公司或组织要求的编码规范不符的情况,此时我们需要将这行代码分成多行来写。在编译时,\后面的换行符将被忽略,当做一行处理。比如宏定义时使用
#define my_puts(x) printf("%s", \
x);
和写作
#define my_puts(x) printf("%s",x);
是没区别的。

TOAST的相关操作

src/backend/access/heap/tuptoaster.c这个文件中定义了和TOAST相关的操作。
根据源码开始的注释,有以下三个函数:

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

我们就逐个开始分析:

toast_insert_or_update函数(插入更新)

这段源码比较长,也比较复杂,因此难以做到十分详尽的分析,但重点地方还是会详尽的分析。

HeapTuple
toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
					   int options)
//传入参数中有两个元组,一个为新版本元组,另一个为旧版本元组。
//旧版本元组的作用是根据旧版本元组是否为空来判断进行插入还是更新操作。
{
	HeapTuple	result_tuple;//最终处理后的元组,用于返回
	TupleDesc	tupleDesc;//元组描述符
	int			numAttrs;//属性数量
	int			i;//for循环用变量

	bool		need_change = false;//是否线外存储了属性,存储了则为真(需要新建立一个结果元组)
	bool		need_free = false;//内存是否需要释放
	bool		need_delold = false;//旧元组的某一属性是否需要删除
	bool		has_nulls = false;//新元组是否含有NULL属性

	Size		maxDataLen;//存储最大的数据长度
	Size		hoff;

	char		toast_action[MaxHeapAttributeNumber];//存放对元组属性的操作,有' ' 表示默认处理方式,'p'表示已经处理过了,'x'表示不可以进行压缩,但是可以线外存储。
	bool		toast_isnull[MaxHeapAttributeNumber];//存放新元组该属性是否为Null
	bool		toast_oldisnull[MaxHeapAttributeNumber];//存放旧元组该属性是否为Null
	Datum		toast_values[MaxHeapAttributeNumber];//存放新元组属性的值
	Datum		toast_oldvalues[MaxHeapAttributeNumber];//存放旧元组属性的值
	struct varlena *toast_oldexternal[MaxHeapAttributeNumber];//存放旧元组线外存储(toast表)的指针
	int32		toast_sizes[MaxHeapAttributeNumber];//存放toast表的大小,只对可变长属性并且toast_action为'p'的属性有效
	bool		toast_free[MaxHeapAttributeNumber];//存放是否释放旧元组属性
	bool		toast_delold[MaxHeapAttributeNumber];//存放是否删除旧元组的属性

	 //获取元组的描述符并且将元组分解成不同的属性
	tupleDesc = rel->rd_att;
	numAttrs = tupleDesc->natts;

	Assert(numAttrs <= MaxHeapAttributeNumber);//属性数量不能超过限制,否则报错
	heap_deform_tuple(newtup, tupleDesc, toast_values, toast_isnull);//从新元组中提取出属性数组(包括属性的描述和值)等信息
	if (oldtup != NULL)//如果旧元组不为NULL,说明是要进行修改操作。
		heap_deform_tuple(oldtup, tupleDesc, toast_oldvalues, toast_oldisnull);//从旧元组中提取出属性数组(包括属性的描述和值)等信息

	memset(toast_action, ' ', numAttrs * sizeof(char));//初始化toast_action数组,全部置为' '
	memset(toast_oldexternal, 0, numAttrs * sizeof(struct varlena *));//初始化toast_oldexternal数组,全部置为0
	memset(toast_free, 0, numAttrs * sizeof(bool));//初始化toast_free数组,全部置为0
	memset(toast_delold, 0, numAttrs * sizeof(bool));//初始化toast_delold数组,全部置为0

	for (i = 0; i < numAttrs; i++)//对每个属性进行检查
	{
		Form_pg_attribute att = TupleDescAttr(tupleDesc, i);
		struct varlena *old_value;//旧元组的线外存储的属性的指针
		struct varlena *new_value;//新元组的线外存储的属性的指针

		if (oldtup != NULL)//如果旧的元组不为NULL,进行修改操作
		{
			old_value = (struct varlena *) DatumGetPointer(toast_oldvalues[i]);//获取旧元组的对应属性
			new_value = (struct varlena *) DatumGetPointer(toast_values[i]);//获取新元组的对应属性

			//判断是否需要对旧值变动
			if (att->attlen == -1 && !toast_oldisnull[i] &&
				VARATT_IS_EXTERNAL_ONDISK(old_value))//如果旧的线外存储值已经存储在磁盘上
			{
				if (toast_isnull[i] || !VARATT_IS_EXTERNAL_ONDISK(new_value) ||
					memcmp((char *) old_value, (char *) new_value,
						   VARSIZE_EXTERNAL(old_value)) != 0)
				//如果新元组的该属性不为null或者没有线外存储在磁盘上或者旧的属性和新的属性的值不相等,则需要进行更新
				{
					 //则旧的线外存储的数据在更新后就会失去作用,可以直接删除
					toast_delold[i] = true;//将该删除旧数据标记为真
					need_delold = true;//设置需要删除为真
				}
				else//这时无需更新,可以不做处理
				{
					toast_action[i] = 'p';//将该属性的操作标记为p,意为无需处理。
					continue;
				}
			}
		}
		else//这时旧元组为NULL,需要进行插入操作
		{
			new_value = (struct varlena *) DatumGetPointer(toast_values[i]);//获取新属性的值,用于插入。
		}
	
		//处理NULL属性
		if (toast_isnull[i])//如果新元组的该属性为空
		{
			toast_action[i] = 'p';//将该属性的操作设置为P,即无需处理
			has_nulls = true;//含有NULL设置为真
			continue;//此时无论插入还是修改都没有意义,所以继续检查下一个属性
		}

		//线外存储属性相关信息检查
		if (att->attlen == -1)//如果是变长属性,则需要进行TOAST处理。
		{
			
			if (att->attstorage == 'p')//如果该属性的存储策略(上面有详细分析)为p,则不进行线外存储和压缩
				toast_action[i] = 'p';//将新元组该属性对应操作设置为p

			if (VARATT_IS_EXTERNAL(new_value))//如果新元组的属性是线外存储的
			{
				toast_oldexternal[i] = new_value;
				if (att->attstorage == 'p')//如果该属性的存储策略为p(没有进行压缩、线外存储)
					new_value = heap_tuple_untoast_attr(new_value);//直接用 heap_tuple_untoast_attr函数读取TOAST数据
				else//否则(可能进行了压缩、线外存储)
					new_value = heap_tuple_fetch_attr(new_value);//用heap_tuple_fetch_attr从线外获取TOAST数据(保留压缩特性,该函数不会对压缩的数据进行解压)
				toast_values[i] = PointerGetDatum(new_value);//给新元组的属性赋值
				toast_free[i] = true;//设置为需要释放
				need_change = true;//需要修改设置为真
				need_free = true;//需要释放设置为真
			}

			toast_sizes[i] = VARSIZE_ANY(new_value);//记录该属性的大小
		}
		else//并非变长属性,则只能p模式存储。
		{
			toast_action[i] = 'p';//将该属性的操作设置为p,即不压缩也不线外存储。
		}
	}

	hoff = SizeofHeapTupleHeader;//获取元组文件头的长度
	if (has_nulls)//如果新元组中有null属性
		hoff += BITMAPLEN(numAttrs);//将刚才文件头的长度再加上属性数据的长度
	hoff = MAXALIGN(hoff);//元组中没有null属性则可直接赋值
	maxDataLen = RelationGetToastTupleTarget(rel, TOAST_TUPLE_TARGET) - hoff;//利用刚才计算出的hoff来限制元组的数据大小

第一次while循环
先处理存储策略为’x’和’e’的两种属性。
主要流程:

  1. 遍历属性,找到大小最大并且仍未进行TOAST处理的属性,不存在则直接进入下一个while循环。
  2. 如果属性的存储策略为 ‘x’,即EXTENDED,允许压缩和线外存储,则先进行压缩,如果压缩成功则返回压缩后的新值,压缩失败则将对应的toast_action设置为p,即以后都忽略压缩(避免重复)。
  3. 如果属性的存储策略为 ‘x’,即EXTERNAL,允许线外存储,不允许压缩,将对应的toast_action设置为p,即以后都忽略压缩。
  4. 经过刚才的处理,如果属性大小仍超过限制,则调用toast_save_datum将其进行线外存储。
	 //第一个while循环,寻找可以压缩的属性对其进行压缩
	while (heap_compute_data_size(tupleDesc,
								  toast_values, toast_isnull) > maxDataLen)//如果元组仍然超过限制大小
	{
		int			biggest_attno = -1;
		int32		biggest_size = MAXALIGN(TOAST_POINTER_SIZE);
		Datum		old_value;
		Datum		new_value;

		for (i = 0; i < numAttrs; i++)//遍历属性,寻找还没有被处理的最大的线内属性
		{
			Form_pg_attribute att = TupleDescAttr(tupleDesc, i);//赋值

			if (toast_action[i] != ' ')
				continue;
			if (VARATT_IS_EXTERNAL(DatumGetPointer(toast_values[i])))//如果数据存储在线外
				continue;	
			if (VARATT_IS_COMPRESSED(DatumGetPointer(toast_values[i])))//如果该属性的数据已经被压缩
				continue;//跳到下一个属性继续
			if (att->attstorage != 'x' && att->attstorage != 'e')//如果存储策略既不为x又不为e,即不可以压缩,则跳到下一个属性继续
				continue;
			if (toast_sizes[i] > biggest_size)//通过比较找出大小最大的属性
			{
				biggest_attno = i;//设置最大的属性的数组序号
				biggest_size = toast_sizes[i];//赋值
			}
		}

		if (biggest_attno < 0)//没有找到符合要求的(初值为-1,结果<0说明没有找到)
			break;//终止第一个while循环

		i = biggest_attno;//能运行到这里,说明一定找到了可以压缩的属性,将序号赋值给i
		if (TupleDescAttr(tupleDesc, i)->attstorage == 'x')//如果该属性的存储策略为x,即允许压缩
		{
			old_value = toast_values[i];//获取未压缩时的值
			new_value = toast_compress_datum(old_value);//将压缩后的值赋给new_value

			if (DatumGetPointer(new_value) != NULL)//如果成功拿到到了压缩后的属性的指针
			{
				//说明压缩成功
				if (toast_free[i])//判断第i个属性的内存空间是否需要释放
					pfree(DatumGetPointer(old_value));//释放内存空间
				toast_values[i] = new_value;//将新值赋给存储属性值的数组
				toast_free[i] = true;
				toast_sizes[i] = VARSIZE(DatumGetPointer(toast_values[i]));//获取新的值的大小
				need_change = true;
				need_free = true;
			}
			else//压缩失败,可能数据是不可压缩的
			{
				toast_action[i] = 'x';//将操作设置为x,忽视后面的压缩过程。
			}
		}
		else//存储策略不为x,则说明策略为e,此时不允许压缩,但是允许线外存储
		{
			toast_action[i] = 'x';//将操作设置为x,忽视后面的压缩过程。
		}

		if (toast_sizes[i] > maxDataLen &&
			rel->rd_rel->reltoastrelid != InvalidOid)//如果经过刚才的压缩处理后,大小仍然超出限制
		{
			old_value = toast_values[i];//将处理后的值赋给old_value
			toast_action[i] = 'p';//将操作设置为p
			toast_values[i] = toast_save_datum(rel, toast_values[i],
											   toast_oldexternal[i], options);//将值存储到线外
			//关于toast_save_datum函数,将一个属性保存到线外并且返回该属性的引用。
			
			//内存释放
			if (toast_free[i])
				pfree(DatumGetPointer(old_value));
			toast_free[i] = true;
			need_change = true;
			need_free = true;
		}
	}

第二次while循环
找到可以线外存储但未线外存储的属性(即刚才处理的x和e),进行线外存储的操作。
主要流程:

  1. 找到最大的,且存储策略为x或e,并且仍未进行线外存储的大小最大的属性,如果未找到则进入下一个while循环。
  2. 调用函数toast_save_datum将其存储到线外,并将对应的toast_action设置为p,从而忽略以后的压缩和线外存储操作。
	 //第二次循环:寻找存储策略为x或者e,也就是可以线外存储的属性
	while (heap_compute_data_size(tupleDesc,
								  toast_values, toast_isnull) > maxDataLen &&
		   rel->rd_rel->reltoastrelid != InvalidOid)//如果元组仍然超过限制大小
	{
		int			biggest_attno = -1;//记录最大的属性的数组序号
		int32		biggest_size = MAXALIGN(TOAST_POINTER_SIZE);//初始化最大大小
		Datum		old_value;//存储旧数据

		for (i = 0; i < numAttrs; i++)//遍历所有属性,找出存储策略为x或e,并且仍未进行线外存储的最大的属性
		{
			Form_pg_attribute att = TupleDescAttr(tupleDesc, i);

			if (toast_action[i] == 'p')//如果操作为p,则无需压缩或线外存储
				continue;//继续下一个属性
			if (VARATT_IS_EXTERNAL(DatumGetPointer(toast_values[i])))//如果已经线外存储(这种情况不会出现,因为线外存储以后会将操作设置为p,在上面就被过滤掉了)
				continue;//继续下一个属性
			if (att->attstorage != 'x' && att->attstorage != 'e')//如果存储策略不为x或e
				continue;//继续下一个属性
			if (toast_sizes[i] > biggest_size)//和上面一样,选出最大值
			{
				biggest_attno = i;
				biggest_size = toast_sizes[i];
			}
		}

		if (biggest_attno < 0)//没有找到符合条件的
			break;//跳出while循环

		//走到这里说明找到了可以进行线外存储的属性,将开始线外存储的操作
		i = biggest_attno;
		old_value = toast_values[i];
		toast_action[i] = 'p';//将操作设置为p,从而忽略以后的操作。
		toast_values[i] = toast_save_datum(rel, toast_values[i],
										   toast_oldexternal[i], options);//将数据存储到线外,并返回引用
		//释放内存
		if (toast_free[i])
			pfree(DatumGetPointer(old_value));
		toast_free[i] = true;

		need_change = true;
		need_free = true;
	}

第三次while循环
这次循环处理存储类型为m的属性,即只允许线内压缩而不允许线外存储的类型。
主要流程:

  1. 遍历属性,寻找存储类型为m且仍未被处理过的最大的属性,如果未找到则进入下一个循环。
  2. 将找到的属性尝试压缩,如果压缩未成功则设置对应的toast_action为’x’,忽略以后的压缩操作。
	 //第三次循环:处理策略为m的允许压缩但未压缩的属性
	while (heap_compute_data_size(tupleDesc,
								  toast_values, toast_isnull) > maxDataLen)//如果元组仍然超过限制大小
	{
		int			biggest_attno = -1;//初始化最大序号
		int32		biggest_size = MAXALIGN(TOAST_POINTER_SIZE);//初始化比较大小
		Datum		old_value;
		Datum		new_value;

		for (i = 0; i < numAttrs; i++)//遍历属性,寻找最大的未压缩的属性
		{
			if (toast_action[i] != ' ')//操作为空则继续
				continue;
			if (VARATT_IS_EXTERNAL(DatumGetPointer(toast_values[i])))//如果是线外存储的数据则跳过
				continue;		
			if (VARATT_IS_COMPRESSED(DatumGetPointer(toast_values[i])))//如果数据已经压缩过了则跳过
				continue;
			if (TupleDescAttr(tupleDesc, i)->attstorage != 'm')//如果存储策略不为m则跳过
				continue;
				//用于比较获取最大值
			if (toast_sizes[i] > biggest_size)
			{
				biggest_attno = i;
				biggest_size = toast_sizes[i];
			}
		}

		if (biggest_attno < 0)//不存在则退出循环
			break;

		//执行到这里说明找到了对应的属性,开始尝试压缩
		i = biggest_attno;
		old_value = toast_values[i];//赋值要压缩的值到old_value
		new_value = toast_compress_datum(old_value);//进行压缩,并将结果返回给new_value

		if (DatumGetPointer(new_value) != NULL)//如果压缩成功
		{
			if (toast_free[i])//如果需要释放内存则释放
				pfree(DatumGetPointer(old_value));
			toast_values[i] = new_value;//存储压缩后的值
			//压缩完成后修改一下相关信息
			toast_free[i] = true;
			toast_sizes[i] = VARSIZE(DatumGetPointer(toast_values[i]));
			need_change = true;
			need_free = true;
		}
		else//压缩失败
		{
			//将操作设置为x,后续压缩操作直接忽略。
			toast_action[i] = 'x';
		}
	}

第四次while循环
此时已经经过了前面三次的循环,如果此时元组的大小仍然超过限制,但是前面线外存储和压缩都已经尝试过了,还有什么办法继续缩小元组的大小呢?我想答案已经呼之欲出了,那就是将压缩过的数据进行线外存储。而这种数据,只能是存储策略为m的数据(只允许压缩,不允许线外存储),其他存储类型的数据已经没有可以继续转移的可能了。因此,第四次循环会将存储策略为m且已经被处理过的属性存储到线外。

大体流程:

  1. 将存储策略为’m’且已经被压缩过的属性进行线外存储。
  2. 判断是否需要构造一个结果元组,如果需要,则利用新元组的信息以及TOAST处理过的属性值构造一个结果元组,再从旧元组中删除没有在新元组中重用的线外存储数据。
  3. 返回结果元组。

	while (heap_compute_data_size(tupleDesc,
								  toast_values, toast_isnull) > maxDataLen &&
		   rel->rd_rel->reltoastrelid != InvalidOid)//如果元组仍然超过限制大小
	{
		int			biggest_attno = -1;//初始化最大的属性的序号
		int32		biggest_size = MAXALIGN(TOAST_POINTER_SIZE);//初始化最大值
		Datum		old_value;

		for (i = 0; i < numAttrs; i++)//遍历所有属性,寻找最大的且已经处理过的m类型数据。
		{
			if (toast_action[i] == 'p')//操作类型为p,直接跳过
				continue;
			if (VARATT_IS_EXTERNAL(DatumGetPointer(toast_values[i])))//已经线外存储,直接跳过
				continue;		
			if (TupleDescAttr(tupleDesc, i)->attstorage != 'm')//如果存储策略不为m,直接跳过
				continue;
				//符合条件,则判断是否为最大的
			if (toast_sizes[i] > biggest_size)
			{
				biggest_attno = i;
				biggest_size = toast_sizes[i];
			}
		}

		if (biggest_attno < 0)//没有找到,则跳出循环
			break;

		//程序执行到这里,说明已经找到对应的属性,进行线外存储即可(前面分析过)
		i = biggest_attno;
		old_value = toast_values[i];
		toast_action[i] = 'p';
		toast_values[i] = toast_save_datum(rel, toast_values[i],
										   toast_oldexternal[i], options);
		//释放内存以及修改相关信息
		if (toast_free[i])
			pfree(DatumGetPointer(old_value));
		toast_free[i] = true;

		need_change = true;
		need_free = true;
	}

	 //来到这里时,我们已经对很多数据进行了toast的相关操作,所以需要通过新元组的信息以及TOAST处理后的属性值来构造一个结果元组。
	if (need_change)
	{
		HeapTupleHeader olddata = newtup->t_data;//原来的文件头
		HeapTupleHeader new_data;//结果生成元组的文件头
		int32		new_header_len;//结果元组头的长度
		int32		new_data_len;//结果元组数据的长度
		int32		new_tuple_len;//结果元组的长度

		 //计算结果元组的长度
		new_header_len = SizeofHeapTupleHeader;//首先是文件头的长度
		if (has_nulls)//如果元组中含有Null数据
			new_header_len += BITMAPLEN(numAttrs);
		new_header_len = MAXALIGN(new_header_len);
		new_data_len = heap_compute_data_size(tupleDesc,
											  toast_values, toast_isnull);
		new_tuple_len = new_header_len + new_data_len;
		
		result_tuple = (HeapTuple) palloc0(HEAPTUPLESIZE + new_tuple_len);//分配内存空间
		//一系列的赋值
		result_tuple->t_len = new_tuple_len;
		result_tuple->t_self = newtup->t_self;
		result_tuple->t_tableOid = newtup->t_tableOid;
		new_data = (HeapTupleHeader) ((char *) result_tuple + HEAPTUPLESIZE);
		result_tuple->t_data = new_data;
		
		 //复制已有的元组文件头,调整natts和t_hoff
		memcpy(new_data, olddata, SizeofHeapTupleHeader);
		HeapTupleHeaderSetNatts(new_data, numAttrs);
		new_data->t_hoff = new_header_len;

		//复制数据并按需填充位图为null的部分
		heap_fill_tuple(tupleDesc,
						toast_values,
						toast_isnull,
						(char *) new_data + new_header_len,
						new_data_len,
						&(new_data->t_infomask),
						has_nulls ? new_data->t_bits : NULL);
	}
	else//如果没有进行线外存储
		result_tuple = newtup;//无需生成结果元组,直接返回新元组即可

	if (need_free)//如果需要释放空间
		for (i = 0; i < numAttrs; i++)//遍历所有属性
			if (toast_free[i])//如果该属性需要释放(临时数据)
				pfree(DatumGetPointer(toast_values[i]));//释放临时数据的空间

	if (need_delold)//如果需要删除旧元组的线外存储的数据
		for (i = 0; i < numAttrs; i++)//遍历所有属性
			if (toast_delold[i])//判断该属性是否需要删除
				toast_delete_datum(rel, toast_oldvalues[i], false);//调用toast_delete_datum删除线外存储的数据

	return result_tuple;
}

toast_delete函数(删除)

当包含TOAST数据的元组被删除时,其对应的TOAST数据也需要被删除,该函数就是负责这一操作并且回收toast存储空间的。

void
toast_delete(Relation rel, HeapTuple oldtup, bool is_speculative)
{
	TupleDesc	tupleDesc;//元组描述符
	int			numAttrs;//属性数量
	int			i;//for循环使用
	Datum		toast_values[MaxHeapAttributeNumber];//存储要删除的toast元组的属性
	bool		toast_isnull[MaxHeapAttributeNumber];//存储要删除的toast元组的属性是否为null

	Assert(rel->rd_rel->relkind == RELKIND_RELATION ||
		   rel->rd_rel->relkind == RELKIND_MATVIEW);//对于采取plain策略的关系或者物理视图,不会采用toast机制,因此无需进行toast删除

	tupleDesc = rel->rd_att;//获取元组描述符
	numAttrs = tupleDesc->natts;//获取元组的属性数量

	Assert(numAttrs <= MaxHeapAttributeNumber);//属性数量超过最大数量则报错
	heap_deform_tuple(oldtup, tupleDesc, toast_values, toast_isnull);//获取数据
	//关于是使用这个函数还是heap_getattr()函数获取数据是存在疑问的:当只有很少的变长属性时,heap_getattr的速度更快,反之,heap_deform_tuple的速度更快。
	for (i = 0; i < numAttrs; i++)//对所有属性进行检查
	{
		if (TupleDescAttr(tupleDesc, i)->attlen == -1)//如果该属性为变长属性
		{
			Datum		value = toast_values[i];//获取该属性的数据

			if (toast_isnull[i])//如果该属性为null
				continue;//继续检查下一个属性
			else if (VARATT_IS_EXTERNAL_ONDISK(PointerGetDatum(value)))//如果该属性的数据线外存储在磁盘上
				toast_delete_datum(rel, value, is_speculative);//调用toast_delete_datum函数删除存储在磁盘上的TOAST数据
		}
	}
}

对用采用PLAIN和MAIN策略的数据来说,TOAST数据并没有线外存储,因此会包含在元组内一起被删除。
对于采用了线外存储的策略的数据来说,还需要删除线外存储的数据,需要级联删除,这是就会调用上面的函数进行删除。

heap_tuple_untoast_attr函数(获取)

struct varlena *
heap_tuple_untoast_attr(struct varlena *attr)
//入参为线外存储的数据类型指针(上面有具体分析),涵盖了具体的TOAST策略,因此可以用于根据不同的TOAST策略来取数据。
{
	if (VARATT_IS_EXTERNAL_ONDISK(attr))//如果该数据是线外存储在磁盘上的
	{
		
		attr = toast_fetch_datum(attr);//将数据从磁盘上取回来
		if (VARATT_IS_COMPRESSED(attr))//如果数据被压缩了
		{
			struct varlena *tmp = attr;//生成一个临时指针存放原数据
			attr = toast_decompress_datum(tmp);//将解压后的数据赋给原指针
			pfree(tmp);//释放临时指针
		}
	}
	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))//如果该数据是线外存储在内存中的
	{
		struct varatt_indirect redirect;//内存线外存储指针(上面有分析)。

		VARATT_EXTERNAL_GET_POINTER(redirect, attr);//获取线外存储数据的指针
		attr = (struct varlena *) redirect.pointer;

		Assert(!VARATT_IS_EXTERNAL_INDIRECT(attr));//如果取回来的数据仍然是线外存储在内存上的,则为嵌套的间接存储,这是不允许的。

		attr = heap_tuple_untoast_attr(attr);//以递归的形式取数据,防止因数据以其他形式进行了扩展而没有成功获取。

		if (attr == (struct varlena *) redirect.pointer)//如果该数据并没有其他形式的扩展,我们可以直接复制它。
		{
			struct varlena *result;//用于复制的指针

			result = (struct varlena *) palloc(VARSIZE_ANY(attr));//为指针分配等量的内存空间
			memcpy(result, attr, VARSIZE_ANY(attr));//将attr的值复制到result
			attr = result;//将attr指向复制后的内存区,返回attr
		}
	}
	else if (VARATT_IS_EXTERNAL_EXPANDED(attr))//如果是在线外扩展的(上面递归获取数据时可能会碰到这种情况)
	{
		attr = heap_tuple_fetch_attr(attr);//从线外存储获取TOAST数据
		Assert(!VARATT_IS_EXTENDED(attr));//拿回的数据不允许是EXTENDED策略的。
	}
	else if (VARATT_IS_COMPRESSED(attr))//如果数据是没有线外存储但经过压缩的
	{
		attr = toast_decompress_datum(attr);//解压后返回数据
	}
	else if (VARATT_IS_SHORT(attr))//如果数据是短数据
	{
		Size		data_size = VARSIZE_SHORT(attr) - VARHDRSZ_SHORT;//去除短文件头的长度,获取数据的长度。
		Size		new_size = data_size + VARHDRSZ;//添加正常的4字节的文件头长度,和数据长度拼接成正常长度。
		struct varlena *new_attr;//生成新属性的指针

		new_attr = (struct varlena *) palloc(new_size);//使用palloc函数为指针分配刚才获取的正常长度大小的内存空间
		SET_VARSIZE(new_attr, new_size);//为新的属性设置大小(给新文件头赋值)
		memcpy(VARDATA(new_attr), VARDATA_SHORT(attr), data_size);//将原本属性的数据复制到新属性的数据区
		attr = new_attr;//返回新生成的属性
	}

	return attr;
}

读取TOAST数据的流程大体如下(详细的分析都写在源码注释中):

  1. 如果数据是线外存储的,则先调用函数toast_fetch_datum从TOAST表中获取该数据的片段来重组数据。获取线外数据的原理非常简单:根据TOAST属性中存储的TOAST指针,从TOAST表中得到所有属于该TOAST数据的片段元组,然后根据元组中记录的片段顺序号将片段拼成TOAST数据。接下来对从线外取回的数据进行判断,如果是经过压缩的则用解压算法进行解压缩然后返回数据,否则直接返回数据。
  2. 如果数据没有线外存储但是经过压缩的,则解压缩然后返回数据。
  3. 如果数据在线外还有扩展,则调用函数heap_tuple_fetch_attr从TOAST表中获取该数据的片段来重组数据,然后返回。
  4. 如果数据是短数据,直接返回数据。

关于如何判断数据类型,在上面有详细的分析,就是借助vl_len的前两位来进行判断。

总结

关于操作调用的总结

TOAST操作
更新和插入TOAST元组
获取TOAST元组
删除TOAST元组
toast_insert_or_update函数
heap_tuple_untoast_attr函数
toast_delete函数
toast_fetch_datum函数
toast_delete_datum函数

收获

这次分析的源码非常多,花费了很多时间,终于算是理清了TOAST机制的相关结构体、文件、以及操作的大体思路和一些细节。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
PostgreSQL是以加州大学伯克利分校计算机系开发的POSTGRES,现在已经更名为PostgreSQL. PostgreSQL支持大部分SQL标准并且提供了许多其它现代特性:复杂查询、外键、触发器、视图、事务完整性等。PostgreSQL 是一个免费的对象-关系数据库服务器(数据库管理系统),它在灵活的 BSD-风格许可证下发行。它提供了相对其他开放源代码数据库系统(比如 MySQL 和 Firebird),和专有系统(比如 Oracle、Sybase、IBM 的 DB2 和 Microsoft SQL Server)之外的另一种选择。事实上, PostgreSQL 的特性覆盖了 SQL-2/SQL-92 和 SQL-3/SQL-99,首先,它包括了可以说是目前世界上最丰富的数据类型的支持,其中有些数据类型可以说连商业数据库都不具备, 比如 IP 类型和几何类型等;其次,PostgreSQL 是全功能的自由软件数据库,很长时间以来,PostgreSQL 是唯一支持事务、子查询、多版本并行控制系统(MVCC)、数据完整性检查等特性的唯一的一种自由软件的数据库管理系统。 Inprise 的 InterBase 以及SAP等厂商将其原先专有软件开放为自由软件之后才打破了这个唯一。最后,PostgreSQL拥有一支非常活跃的开发队伍,而且在许多黑客的努力下,PostgreSQL 的质量日益提高。从技术角度来讲,PostgreSQL 采用的是比较经典的C/S(client/server)结构,也就是一个客户端对应一个服务器端守护进程的模式,这个守护进程分析客户端来的查询请求,生成规划树,进行数据检索并最终把结果格式化输出后返回给客户端。为了便于客户端的程序的编写,由数据库服务器提供了统一的客户端 C 接口。而不同的客户端接口都是源自这个 C 接口,比如ODBC,JDBC,Python,Perl,Tcl,C/C++,ESQL等, 同时也要指出的是,PostgreSQL 对接口的支持也是非常丰富的,几乎支持所有类型的数据库客户端接口。这一点也可以说是 PostgreSQL 一大优点。本课程作为PostgreSQL数据库管理一,主要讲解以下内容: 1.     PostgreSQL 储过程基本知识2.     PostgreSQL 用户自定义函数3.     PostgreSQL 控制结构4.     PostgreSQL 游标和储过程5.     PostgreSQL 索引6.     PostgreSQL 视图7.     PostgreSQL 触发器8.     PostgreSQL 角色、备份和还原9.     PostgreSQL 表空间管理

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值