












Postgres 19 is planning to change the default TOAST compression from pglz to LZ4, so let's look at how Postgres compresses data in table storage and indexes. Postgres uses a single, unified compression framework for table (heap), TOAST, and indexes. In heap and TOAST, compression is automatic and on by default for variable-length types like TEXT, VARCHAR, BYTEA, and JSONB. In indexes, it is opportunistic: compression fires only when an individual key exceeds the size threshold, not for every variable-length value stored in an index.
click to expand
Compression was first added in Postgres 7.0, released in 2000, but not in the form it takes today. At the time, Postgres had a strict 8kB maximum row size, and trying to insert more than 8kB would throw an error. Fixing this row size limit was a priority for the Postgres core team.
The first attempt to work around the limit was an explicitly compressed field. Postgres 7.0 shipped an lztext data type that used the pglz compression algorithm. This implementation had tradeoffs: the 8kB row limit still existed, and users had to explicitly choose a compressed data type.
After the 7.0 release, the next logical step might have been to add more compressed data types like LONG or BLOB (as many closed source databases were doing at the time). Instead, the Postgres core team rejected additional data types and rallied around TOAST. Discussions on the pgsql-hackers mailing list show the group split the 8kB problem into two distinct problems: a data type problem and a physical storage problem. Compression and TOAST were the answer to the physical storage problem.
With Postgres 7.1, TOAST was implemented using pglz.
The pglz algorithm lives in the pg_lzcompress.c file. Because Postgres is open source, we can read the author's reasoning for home-rolling a compression algorithm right in the comments:
Jan Wieck authored the algorithm and left an acknowledgement at the bottom of the file:
Many thanks to Adisak Pochanayon, who's article about SLZ inspired me to write the PostgreSQL compression this way.
Adisak Pochanayon's SLZ article described a compression scheme built for the game industry, where it was used to compress graphics and audio data.
pglz was built for a different era, and LZ4 is a modern algorithm whose tradeoffs match modern hardware. Postgres' LZ4 rollout follows a path similar to the original pglz implementation: first as an option, then as standard. Since Postgres 14, LZ4 has been available via a system-wide setting (default_toast_compression = 'lz4') or a column-specific setting (column_name text COMPRESSION lz4).
First off, you need to know that Postgres has a few different storage strategies for columns:
EXTENDED (we capitalize it because the Postgres docs do, not because we're yelling) lets Postgres use every tool available. This is the default for variable-length types like TEXT, VARCHAR, BYTEA, and JSONB. Values can be stored uncompressed, compressed, in the heap, or in TOAST.
PLAIN stores the column inline in the heap, uncompressed. It is the default for fixed-width types like INT, FLOAT, and BOOL. These values rarely benefit from compression.
EXTERNAL tells Postgres to store the column in TOAST, but not compress it.
MAIN tells Postgres to try compressing the column, but avoid moving it to TOAST if possible.
For variable-length types, Postgres uses a format called varlena to store the data. varlena is a self-describing format with a header that records the length of the data and whether it is compressed. It is used for all variable-length types (including TEXT, VARCHAR, BYTEA, and JSONB) and it is what gets stored in the heap, in TOAST, and in indexes.
Only variable-length types are compressed. Fixed-length types (like INT, FLOAT, and BOOL) are never compressed; they are stored directly in the heap.
When writing data (insert or update), Postgres attempts to get the row size below a threshold of roughly 2 kB (toast_tuple_target, default 2040 bytes). It uses the following decision tree for EXTENDED columns:
For more on TOAST, check out Postgres TOAST: The Greatest Thing Since Sliced Bread?
Checking actual compression savings
-- pg_column_size: bytes as stored in the heap or TOAST table (after compression)
-- octet_length: bytes of raw character data (uncompressed)
SELECT
pg_column_size(payload) AS stored_bytes,
octet_length(payload) AS raw_bytes,
round(
(1 - pg_column_size(payload)::numeric
/ NULLIF(octet_length(payload), 0)) * 100, 1
) AS compression_pct
FROM events WHERE length(payload) > 100 LIMIT 5;
pg_column_size reports the compressed data size. When it returns a value much smaller than octet_length, the value was successfully compressed. When the two are approximately equal, the value did not compress well enough to save space. In that case, if the value is large (above the ~2 kB threshold), it will still have been moved to the TOAST table uncompressed.
B-tree index pages store key values as IndexTuple entries. Each entry contains an IndexTupleData header (8 bytes) followed by the key datum. For variable-length types, the datum uses the same varlena format as heap tuples (piggybacking on the workaround built for the 8kB row limit).
Postgres uses a single compression framework tied to the TOAST architecture. It flags in the varlena header whether the data is compressed. If the heap already compressed a value, it goes into the index compressed. If a value is uncompressed and exceeds 510 bytes (TOAST_INDEX_TARGET, about 1/16 of the 8 kB buffer page), the index code invokes the same TOAST compression routine inline to try to make it fit. If the compressed form fits, it is stored compressed. If even the compressed form exceeds the limit, the write transaction fails.
This is why repeat('x', 5000) can be B-tree indexed: LZ4 compresses 5,000 repeated characters down to ~38 bytes, well within the 2704-byte cap. Random or pseudo-random data of the same length produces a compressed form nearly equal to the original, which exceeds the cap and cannot be indexed.
-- These succeed: both compressible, both fit after LZ4 compression
CREATE INDEX ON docs (body);
INSERT INTO docs VALUES (repeat('x', 5000)); -- stored: ~38 bytes compressed
INSERT INTO docs VALUES (repeat('ab', 2000)); -- stored: ~35 bytes compressed
-- This fails: md5 output is pseudo-random, essentially incompressible
INSERT INTO docs VALUES (
(SELECT string_agg(md5(g::text), '') FROM generate_series(1, 88) g)
);
-- 2816 chars of MD5 → compressed form ≈ 2816 bytes → index row size 2832 > 2704
-- ERROR: index row size 2832 exceeds btree version 4 maximum 2704 for index "..."
-- HINT: Values larger than 1/3 of a buffer page cannot be indexed.
There's a lot to learn about how Postgres moves forward by looking at compression. The early false step of dedicated compressed data types was acknowledged, and the underlying pglz work was retooled into TOAST, which has been a huge success. In moving from pglz to LZ4, Postgres is taking a similar approach: first a test, then a migration. The core team has moved intentionally to make sure the compression algorithm change is the correct path.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。