PostgreSQL 教程: 采用 LZ4 压缩 TOAST 表

六月 13, 2024

摘要:本教程介绍了 PostgreSQL 版本 14 中引入的 LZ4 压缩 TOAST 的功能,并演示了其用法。

目录

背景

TOAST 是 PostgreSQL 用来防止物理数据行大小超过数据块大小(通常为 8KB)的一种机制。PostgreSQL 不支持跨块边界的物理行,因此块大小是行大小的硬性上限。为了允许用户表具有比这更宽的行,TOAST 机制将宽字段值分解为更小的部分,这些部分“线外”存储在与用户表关联的 TOAST 表中。

您创建的每个表都有其关联的(唯一的)TOAST 表,该表最终可能会被使用,也可能不会被使用,具体取决于您插入的行的大小。所有这些都对用户是透明的,并且默认情况下处于启用状态。

当要存储的行“太宽”(默认情况下,该行的阈值为 2KB)时,TOAST 机制首先尝试压缩任何宽字段值。如果这还不足以使行小于 2KB,它会将宽字段值分解和存储到关联的 TOAST 表中的块。每个原始的字段值都被替换为一个小的指针,该指针指向 TOAST 表中查找此“线外”数据的位置。TOAST 将尝试用这种方式将用户表行压缩到 2KB,但只要它能低于 8KB,其实就足够了,这样该行就可以完成存储了。

TOAST 压缩方法

在版本 14 以前,PostgreSQL 中只有一种压缩数据的算法,那就是 pglz。这是一种非常古老的内置算法。现在有一个每列压缩选项,可以设置为 pglz(默认值)或 lz4。并且,引入了新的 postgresql.conf 配置参数 default_toast_compression,用于设置默认值为 pglz 的默认压缩方法。当表列未指定压缩方法时,该配置参数的值会应用于新表中的列。在 PostgreSQL 内核中并没有 lz4 的支持,因此要使用 lz4 压缩,必须使用 –with-lz4 选项来构建 PostgreSQL。

示例

让我们创建两个表:一个使用默认压缩,另一个使用新的 LZ4 压缩:

CREATE TABLE t1 ( a text );

CREATE TABLE t2 ( a text compression lz4 );

两张表都自动附加了一个 toast 表:

SELECT oid, relname FROM pg_class WHERE oid IN
  (SELECT reltoastrelid FROM pg_class WHERE relname IN ('t1', 't2'));
  oid  |    relname
-------+----------------
 16387 | pg_toast_16384
 16392 | pg_toast_16389
(2 rows)

\d+ pg_toast.pg_toast_16384
TOAST table "pg_toast.pg_toast_16384"
   Column   |  Type   | Storage
------------+---------+---------
 chunk_id   | oid     | plain
 chunk_seq  | integer | plain
 chunk_data | bytea   | plain
Owning table: "public.t1"
Indexes:
    "pg_toast_16384_index" PRIMARY KEY, btree (chunk_id, chunk_seq)
Access method: heap

让我们来检查一下,如果我们用一些虚拟数据填充这些表,是否会有区别:

\timing on

INSERT INTO t1(a) SELECT lpad('a',1000000,'a') FROM generate_series(1,1000);
Time: 10943.866 ms (00:10.944)

INSERT INTO t2(a) SELECT lpad('a',1000000,'a') FROM generate_series(1,1000);
Time: 280.503 ms

这是一个巨大的差异,即使重复进行多次测试,也会得到几乎相同的数字。这在速度方面确实是一个很大的改进。但是磁盘上的大小呢?

SELECT pg_size_pretty(pg_relation_size('pg_toast.pg_toast_16384')) AS toast1_size;
 toast1_size
-------------
 12 MB
(1 row)

SELECT pg_size_pretty(pg_relation_size('pg_toast.pg_toast_16389')) AS toast2_size;
 toast2_size
-------------
 4000 kB
(1 row)

相当令人印象深刻。除了速度之外,我们还大大降低了磁盘占用空间。由于 LZ4 的压缩效果更好,因此我们在 t2 的 toast 表中看到的行数更少:

SELECT count(*) FROM pg_toast.pg_toast_16384 AS toast1_rows;
 count
-------
  6000
(1 row)

SELECT count(*) FROM pg_toast.pg_toast_16389 AS toast2_rows;
 count
-------
  2000
(1 row)

当然,我在这里使用的字符串不是很有代表性,但这个新功能看起来确实很有用处。如果要全局更改此行为,还有一个新参数;

SHOW default_toast_compression;
 default_toast_compression
---------------------------
 pglz
(1 row)

ALTER SYSTEM SET default_toast_compression = 'lz4';

SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)