| From: | Nikhil Kumar Veldanda <veldanda(dot)nikhilkumar17(at)gmail(dot)com> |
|---|---|
| To: | Postgres hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org> |
| Cc: | Michael Paquier <michael(at)paquier(dot)xyz> |
| Subject: | ZSTD TOAST compression, and an extensible compression method encoding |
| Date: | 2026-09-22 02:26:20 |
| Message-ID: | CAFAfj_GGY4W81XjtYLvYOhqPtvfZbBNj=YRS6Mq_0SA9AqEwoQ@mail.gmail.com |
| Views: | Whole Thread | Raw Message | Download mbox | Resend email |
| Thread: | |
| Lists: | pgsql-hackers |
Hi all,
A while back I posted a patch adding zstd as a TOAST compression
method[1]. The main problem with it, and with any new method, is that
the compression method of a compressed varlena is stored in two bits,
and those bits are almost used up: pglz and lz4 take two of the four
values, and TOAST_INVALID_COMPRESSION_ID a third. Taking the last
value for zstd would have made it the last method we could ever add.
Attached is a reworked series, rebased on top of the recent TOAST
changes (varatt_external_oid/oid8, toast_external_data), which first
makes the encoding extensible and then adds zstd as its first user:
0001 Refactor TOAST compression header handling (no behavior change)
0002 Allow more than four TOAST compression methods
0003 Add zstd as a TOAST compression method
Existing on-disk data is not affected by any of them; pglz and lz4
values keep their current representation, bit for bit.
0001: Refactor TOAST compression header handling
------------------------------------------------
Preparatory, with no behavior change, so that 0002 only has to touch
one place for each thing it changes:
- toast_internals.h duplicated the layout of the compressed-in-line
header as toast_compress_header, with TOAST_COMPRESS_* macros to read
and write the tcinfo word, while varatt.h describes the same bytes as
varattrib_4b.va_compressed. The duplicate is removed and the header
is written through one inline function,
toast_compress_set_size_and_method().
- The compression method of an external value was fetched by callers
from the raw bits of va_extinfo. It is now decoded once, in
toast_external_info_get(), into a new compress_method field of
toast_external_data, and detoast_attr_slice(),
toast_get_compression_id() and amcheck read it from there.
- VARTAG_IS_ONDISK() is added next to VARTAG_IS_EXPANDED(), and the
TOAST pointer assembly at the end of toast_save_datum() moves into a
helper, toast_pointer_build().
0002: Allow more than four TOAST compression methods
----------------------------------------------------
The proposal is a "long form" of both the compressed-in-line header
and the on-disk TOAST pointer. The two method bits keep identifying
pglz and lz4 directly; the value 3, VARLENA_COMPRESS_METHOD_LONG, now
means "the method ID is stored in a byte of its own, right after the
fixed part". Value 2 stays unused in those bits.
Compressed-in-line datum:
plain (pglz, lz4) | va_header | va_tcinfo | data ...
long (all others) | va_header | va_tcinfo | va_cmid | data ...
4 bytes 4 bytes 1 byte
va_tcinfo = 30 bits of raw size + 2 method bits:
00 pglz, 01 lz4, 11 long form (see va_cmid), 10 unused
On-disk TOAST pointer (after the usual 2-byte external header):
tag 18 ONDISK_OID | rawsize | extinfo | valueid | toastrelid |
tag 19 ONDISK_OID_LONG | rawsize | extinfo | valueid |
toastrelid | cmid |
tag 4 ONDISK_OID8 | rawsize | extinfo | valueid lo | valueid
hi | toastrelid |
tag 5 ONDISK_OID8_LONG | rawsize | extinfo | valueid lo | valueid
hi | toastrelid | cmid |
So a long-form pointer is 19 or 23 bytes instead of 18 or 22, and the
inline header is 9 bytes instead of 8, for methods other than pglz and
lz4 only. This leaves room for method IDs up to 255.
The inline long form is described by a new struct:
typedef struct
{
uint32 va_header;
uint32 va_tcinfo; /* method bits =
VARLENA_COMPRESS_METHOD_LONG */
uint8 va_cmid; /* compression method ID */
char va_data[FLEXIBLE_ARRAY_MEMBER];
} varattrib_4b_long;
It is deliberately not a member of the varattrib_4b union. A member
with the extra byte pads to 12 bytes and would grow
sizeof(varattrib_4b) from 8, and a lot of code inspects varlena
headers of unknown or smaller size through pointers of that type;
Outside the union the struct's trailing padding is harmless, since
like the rest of varatt.h it is only ever used with offsetof() and
field access, never sizeof(). Two static assertions tie its va_tcinfo
and va_cmid offsets to va_compressed, so the "va_compressed plus one
byte" relationship is checked by the compiler rather than by a
comment.
The pointer side gets no struct at all. Those structs are memcpy'd
whole with sizeof(), and are asserted to have no padding for that
reason; a struct with a trailing uint8 would carry three bytes of
padding that must not reach disk. Instead the long form is "the plain
struct followed by one byte", and toast_external_info_get() decodes it
by reading the fixed part as before and then the byte. Using new tags
rather than a variable-size struct keeps VARTAG_SIZE() a pure function
of the tag, which matters for tuple deforming. The long tags are
their plain counterparts with the low bit set; that detail is confined
to VARTAG_IS_ONDISK_OID(), VARTAG_IS_ONDISK_OID8() and
VARTAG_IS_ONDISK_LONG(), which is what callers use.
Which methods use the long form is decided in exactly one place,
toast_compression_id_needs_cmid_byte(): pglz and lz4 don't, everything
else does.
TOAST_INVALID_COMPRESSION_ID moves from 2 to 3, on purpose equal to
VARLENA_COMPRESS_METHOD_LONG: code that mistakenly interprets the raw
two bits of a long-form value as a method ID gets an invalid ID rather
than a real method. As the method of an external value can no longer
be derived from va_extinfo alone, VARATT_EXTINFO_GET_COMPRESS_METHOD()
is removed in favor of toast_external_data.compress_method.
amcheck learns to verify that the method of an external value is
stored in the pointer form appropriate for it (a plain pointer
claiming a long-form method, or the marker without the long tag, is
reported), and that only compressed values use the long form.
0003: Add zstd as a TOAST compression method
--------------------------------------------
zstd gets ID 2 and is the first user of the long form. It is
available with --with-zstd / -Dzstd; libzstd is already linked into
the backend for WAL compression, so there are no build system changes.
Compression uses ZSTD_compress() at ZSTD_CLEVEL_DEFAULT, without a
dictionary. Slice decompression goes through the streaming API, since
the one-shot API can only decode a whole frame; as for lz4, the whole
compressed value has to be fetched from the TOAST table first, as
there is no way to bound how much compressed input a given prefix
needs.
The patch wires up pg_column_compression(),
default_toast_compression(the built-in default stays lz4 when
available), amcheck, pg_dump, psql's \d+ and tab completion, and the
documentation.
Tests: compression_zstd follows compression_lz4 and skips itself when
the server lacks zstd. It covers inline and external values with both
oid and oid8 value IDs, slices at the start, middle, end and past the
end of a value, values that stay uncompressed (below the TOAST
threshold, or incompressible), bytea/jsonb/arrays, the EXTERNAL, MAIN
and PLAIN storage modes, copying compressed values between tables and
methods in both directions, VACUUM FULL, CLUSTER and a rewriting ALTER
COLUMN TYPE, removal of old TOAST values on UPDATE and DELETE, indexes
on compressed keys, and the GUC. A pg_dump test checks the column
setting survives a dump. Each patch builds and passes the tests on
its own.
Not in this series: a compression level setting, and dictionary
support, which was the motivation for making the ID space large and
would be another method ID using the same long form.
Thanks to Michael Paquier for the TOAST pointer refactoring this
builds on, and for an off-list look at an earlier version.
--
Nikhil Veldanda
| Attachment | Content-Type | Size |
|---|---|---|
| v2-0002-Allow-more-than-four-TOAST-compression-methods.patch | application/octet-stream | 29.0 KB |
| v2-0003-Add-zstd-as-a-TOAST-compression-method.patch | application/octet-stream | 80.5 KB |
| v2-0001-Refactor-TOAST-compression-header-handling.patch | application/octet-stream | 12.6 KB |
| From | Date | Subject | |
|---|---|---|---|
| Next Message | Manu | 2026-09-22 02:45:23 | Re: Add REPACK progress phases for logical decoding setup |
| Previous Message | Wei Sun | 2026-09-22 02:15:29 | Re: Severe performance degradation with concurrent updates due to excessive EvalPlanQual (EPQ) re‑evaluation |