PostgreSQL 14: 支持可配置的 LZ4 TOAST 压缩

John Doe 七月 16, 2025

你在 PostgreSQL 中存储了大量文本内容的数据吗?你想要对它们进行压缩存储吗?

在山坡漫步的大象

特性提交日志

支持可配置的 LZ4 TOAST 压缩。

现在,每个列都有一个 COMPRESSION 选项,可将其设置为 pglz(默认值,也是迄今为止唯一的选项)或 lz4。或者,你也可以将新的 default_toast_compression 配置参数设置为 lz4,这样对于未指定压缩方式的新表列,将默认使用 lz4 压缩。由于 PostgreSQL 代码中本身不包含 lz4 支持,因此要使用 lz4 压缩,必须在编译 PostgreSQL 时使用 –with-lz4 选项。

通常,TOAST 压缩指的是对单个列值的压缩,而非整个元组的压缩,这些值既可以在元组内进行内联压缩,也可以压缩后存储在外部的 TOAST 表中,这些特性同样适用于此功能。

在此提交之前,TOAST 指针的 va_extsize 字段中有两个未使用的位,压缩数据的 va_rawsize 字段中也有两个未使用的位。这些位未被使用是因为变长数据(varlena)的长度限制为 1GB;现在,我们使用这些位来指示所使用的压缩类型。这意味着我们最多还能再支持 2 种内置压缩类型,但如有必要,我们可以通过为更多压缩类型引入新的 vartag_external 值来解决这个问题。希望这里不需要提供太多的算法选择,因为每添加一种算法不仅需要更多的代码编写,还会增加每个打包者的构建依赖。尽管如此,至少添加 LZ4 是值得的,因为 LZ4 相比 PGLZ 压缩效果更好,且 CPU 使用率更低。

与 PGLZ 一样,LZ4 压缩的数据可能会泄露到存储在磁盘上的复合类型值中。通过 CREATE TABLE ASINSERT .. SELECT 等 SQL 命令,LZ4 压缩的属性也可能被复制到其他表中。强制这些值解压缩会非常耗时,因此 PostgreSQL 从未这样做过。出于同样的原因,即使目标表偏好的压缩方法与源数据使用的压缩方法不同,我们也不会强制对已压缩的值重新压缩。这些架构决策或许存在争议,但重新审视它们超出了本项目力所能及的范围。不过,在 VACUUM FULL 或 CLUSTER 操作中进行重新压缩成本相对较低,因此如果表的配置压缩方法与其中存储的某些列值所使用的压缩方法不匹配,此提交会调整这些命令以进行重新压缩。

讨论:http://postgr.es/m/CAFiTN-uUpX3ck%3DK0mLEk-G_kUQY%3DSNOTeqdaNRR9FMdQrHKebw%40mail.gmail.com

示例

你可能知道,当存储大文本时,它会在内部被压缩。当然,不仅是文本,不过这里我们将重点讨论文本值。

到目前为止,压缩是通过 PostgreSQL 实现的 LZ 算法完成的。

现在,得益于本次提交,我们可以使用另一种算法。要使用它,必须安装 liblz4-dev 包,然后在配置时使用--with-lz4选项。

编译后,可以通过以下方式查看 LZ4 是否可用:

$ pg_config --configure | grep -oP '\S+lz4\S+'
'--with-lz4'

为了测试,我们来创建一个简单的表:

create table pgdocs (
    id serial primary key,
    filename text not null unique,
    body text
);

然后,将整个 PostgreSQL 文档以 html 格式加载到该表中。每个文件对应一行数据。数据如下所示:

select id, filename, length(body) from pgdocs order by random() limit 5;
 id  |              filename              | length
-----+------------------------------------+--------
 829 | sql-altertsconfig.html             |   8617
   2 | admin.html                         |  17620
 746 | spi-spi-cursor-open-with-args.html |   7572
 248 | ecpg-concept.html                  |   6130
 726 | source.html                        |   3042
(5 rows)

然后,我们来创建一个副本表,对 body 列使用 LZ4 压缩:

create table pgdocs4 (
    id serial primary key,
    filename text not null unique,
    body text
);
alter table pgdocs4 alter column body set compression lz4;
insert into pgdocs4 select * from pgdocs;

检查下两个表的结构信息,以确保压缩方式符合预期:

postgres=> \d+ pgdocs
                                                        Table "public.pgdocs"
  Column  |  Type   | Collation | Nullable |              Default               | Storage  | Compression | Stats target | Description
----------+---------+-----------+----------+------------------------------------+----------+-------------+--------------+-------------
 id       | integer |           | not null | nextval('pgdocs_id_seq'::regclass) | plain    |             |              |
 filename | text    |           | not null |                                    | extended | pglz        |              |
 body     | text    |           |          |                                    | extended | pglz        |              |
Indexes:
    "pgdocs_pkey" PRIMARY KEY, btree (id)
    "pgdocs_filename_key" UNIQUE CONSTRAINT, btree (filename)
Access method: heap

postgres=> \d+ pgdocs4
                                                        Table "public.pgdocs4"
  Column  |  Type   | Collation | Nullable |               Default               | Storage  | Compression | Stats target | Description
----------+---------+-----------+----------+-------------------------------------+----------+-------------+--------------+-------------
 id       | integer |           | not null | nextval('pgdocs4_id_seq'::regclass) | plain    |             |              |
 filename | text    |           | not null |                                     | extended | pglz        |              |
 body     | text    |           |          |                                     | extended | lz4         |              |
Indexes:
    "pgdocs4_pkey" PRIMARY KEY, btree (id)
    "pgdocs4_filename_key" UNIQUE CONSTRAINT, btree (filename)
Access method: heap

如你在 Compression 列中所见,pgdocs4 表的 body 列使用的是 lz4 压缩。

首先我们检查下表的大小:

postgres=> \dt+
                                        List of relations
 Schema |   Name   | Type  |  Owner  | Persistence | Access Method |    Size    | Description
--------+----------+-------+---------+-------------+---------------+------------+-------------
 public | pgdocs   | table | redrock | permanent   | heap          | 5952 kB    |
 public | pgdocs4  | table | redrock | permanent   | heap          | 5936 kB    |
(2 rows)

这里需要注意的是,复制数据不会重新压缩它。因此,我们必须对表执行 vacuum full,之后:

postgres=> \dt+
                                        List of relations
 Schema |   Name   | Type  |  Owner  | Persistence | Access Method |    Size    | Description
--------+----------+-------+---------+-------------+---------------+------------+-------------
 public | pgdocs   | table | redrock | permanent   | heap          | 5952 kB    |
 public | pgdocs4  | table | redrock | permanent   | heap          | 6592 kB    |
(2 rows)

所以,lz4 占用的磁盘空间更多,大约多 10%。那么,速度方面呢?

为了测试,我们更新全表,将 body 用 [ / ] 包裹,操作如下:

update pgdocs set body = '[' || body || ']';
update pgdocs4 set body = '[' || body || ']';

运行了 3 次,表现效果很好。使用 pglz 压缩的更新时间分别为:279.084、264.014 和 278.946 毫秒。而使用 LZ4 压缩的更新时间分别为:101.977、130.052 和 112.037 毫秒。

这表明,使用 lz4 压缩的相同操作所花费的时间不到 pglz 的 50%!

更新之后,再次检查表的大小:pgdocs 为 25520 kB,而 pgdocs4 为 25792 kB,差异略小于 10%。

我们在这里测试的数据量很小,但是结果表明:对于那些需要存储较大数据值的情况,lz4 压缩的效果相当好。

非常不错的特性。感谢社区的所有相关人员。

参考

提交日志:https://git.postgresql.org/pg/commitdiff/bbe0a81db69bd10bd166907c3701492a29aca294