在以往文章我们有讲到 toask 技术,今天我们来了解下数据是怎么被压缩的。

我们先来复习下 Toask,在pg中,行不能跨页存储,为了存储更大的行,pg使用Toast技术将行压缩成更小的块。pg使用固定的页面大小(通常为8kb),并且不允许跨页存储。因此,单行不能直接存储很大的字段值。为了克服这个限制,大字段被压缩或分解成多个物理行。

Toast的默认压缩采用lz字典压缩算法,可通过 show default_toast_compression ;进行查看。

当插入的值超过 2kb 的时候,pglz 算法它会自动启动,事实上 Postgres 会尽量避免将数据存储在 toast 表中。因为压缩存储和查询解压缩都需要消耗 cpu 资源,如果行超过TOAST_TUPLE_THRESHOLD
(2Kb),它将尝试压缩列以尝试将行放入块中。更准确地说,大小必须小于
TOAST_TUPLE_TARGET如果幸运的话,压缩的行将适合堆。如果没有,它将尝试压缩列,从最大到最小,并将它们存储在 toast 部分中,直到剩余的列适合堆的一行。

还要注意,如果压缩增益太小,它认为花费资源尝试压缩是没有用的。因此,它不压缩地存储数据。

整个解压缩过程如下图所示,我们重点要学的是数据是怎么被压缩的和怎么被解压缩的。

PGLZ是一种用于PostgreSQL数据库的压缩算法,通过查找数据中的重复字符串,并用较短的引用来替换它们,从而实现压缩。

压缩流程

其主要流程如下:

  1. 初始化:给定一个待压缩串source和压缩结果输出目标dest。

  2. 检查control byte:查看是否已经分配了control byte,如果没有,则在dest中分配一个control byte。

  3. 获取待压缩序列:从source中获取一个长度至少为3的尽量长的序列。

  4. 查找重复序列:在已压缩的source串中查找与待压缩序列相同的连续串。

  5. 写入control bit和偏移量:如果找到了重复序列,将control bit置为1,并将偏移量和长度写入dest。

  6. 写入原始字节:如果没有找到重复序列,将control bit置为0,并将待压缩序列的第一个字节直接写入dest。

  7. 重复步骤:重复步骤3至6,直到压缩完成source串中的所有字节为止。

PGLZ算法,其解压流程如下:

  1. 初始化:给定一个待解压的压缩串source和解压结果输出目标dest。

  2. 读取control byte:从source中读取一个control byte,获取压缩信息。

  3. 检查control bit:查看control bit,判断是否有重复序列。

  4. 如果有重复序列:将control bit后面的偏移量和长度信息解析出来,根据偏移量在已经解压的dest中找到对应的重复序列,将其复制到dest的当前位置。

  5. 如果没有重复序列:将source中的下一个字节直接复制到dest的当前位置。

  6. 重复步骤:重复步骤2至5,直到解压完成source中的所有字节为止。

在13.0以前,我们没办法选择如何压缩数据,因为只有一种算法可以压缩PostgreSQL中的数据,那就是pglz。但是14.0版本提供了可配置的lz4 TOAST压缩特性,将会允许我们选用LZ4作为Toast的压缩算法。

下面我们将使用 lz4 来对比两种算法的差别

# ./configure --help | grep LZ4
  --with-lz4              build with LZ4 support
  LZ4_CFLAGS  C compiler flags for LZ4, overriding pkg-config
  LZ4_LIBS    linker flags for LZ4, overriding pkg-config

sudo yum install epel-release
yum install lz4
sudo yum install lz4-devel

sudo ./configure --prefix=/usr/local/postgresql --with-lz4

make && make install

安装好带有zl4支持版本后,让我们创建两个表:一个使用默认压缩,另一个使用新的LZ4压缩:

create table t1 ( a text );
create table t2 ( a text compression LZ4 );

接下来,我们分别往两张表中新增一些数据。

postgres=# \timing on
Timing is on.
postgres=# insert into t1(a) select lpad('a',1000000,'a') from generate_series(1,1000);
INSERT 0 1000
Time: 8185.073 ms (00:08.185)
postgres=# insert into t2(a) select lpad('a',1000000,'a') from generate_series(1,1000);
INSERT 0 1000
Time: 412.227 ms

可以发现t2插入更快。来看下两张表的toast表。

postgres=# select oid,relname from pg_class where oid in (select reltoastrelid from pg_class where relname ='t1' );
  oid  |    relname     
-------+----------------
 24579 | pg_toast_24576
(1 row)

Time: 2.450 ms
postgres=# select oid,relname from pg_class where oid in (select reltoastrelid from pg_class where relname ='t2' );
  oid  |    relname     
-------+----------------
 24584 | pg_toast_24581
(1 row)

Time: 1.208 ms
postgres=# select pg_size_pretty(pg_relation_size('pg_toast.pg_toast_24576'));
 pg_size_pretty 
----------------
 12 MB
(1 row)

Time: 38.538 ms
postgres=# select pg_size_pretty(pg_relation_size('pg_toast.pg_toast_24581'));
 pg_size_pretty 
----------------
 4000 kB
(1 row)

Time: 0.903 ms

postgres=# select count(1) from pg_toast.pg_toast_24576;
 count 
-------
  6000
(1 row)

Time: 2.544 ms

postgres=# select count(1) from pg_toast.pg_toast_24581;
 count 
-------
  2000
(1 row)

Time: 39.850 ms

可以看到lz4不仅速度更快,而且压缩效果也更好。

查看默认的压缩策略。

postgres=# show default_toast_compression ;
 default_toast_compression
---------------------------
 pglz
(1 row)

Time: 0.156 ms

修改默认压缩策略。

postgres=# show default_toast_compression ;
 default_toast_compression
---------------------------
 pglz
(1 row)

Time: 0.156 ms
postgres=#  SET default_toast_compression TO lz4;
SET
Time: 0.163 ms
postgres=#  show default_toast_compression ;
 default_toast_compression
---------------------------
 lz4
(1 row)

Time: 0.163 ms

修改具体表的某个列的压缩策略。

postgres=#  ALTER TABLE t2 ALTER COLUMN a SET COMPRESSION pglz;


参考文档:

https://www.modb.pro/db/84780

https://zhuanlan.zhihu.com/p/661662835