Short answer: compression.
The data type text
allows (lossless!) compression and storage out of line by default:
SELECT typstorage FROM pg_type WHERE typname = 'text'; -- 'x'
The manual about pg_type.typstorage
:
p: Value must always be stored plain.
e: Value can be stored in a “secondary” relation (if relation has one, see pg_class.reltoastrelid).
m: Value can be stored compressed inline.
x: Value can be stored compressed inline or stored in “secondary” storage.
x
is the usual choice for toast-able types. Note that m
values can
also be moved out to secondary storage, but only as a last resort (e
and x
values are moved first).
Test with pg_column_size()
instead of length()
. Be sure to test actual table columns (with compression applied) not just input values. See:
CREATE TABLE tbl (
id int
, col text -- with default pglz compression
);
INSERT INTO tbl(id, col) VALUES
(1, rpad(md5('non_random'::text), 100, md5('non_random'::text)))
, (2, rpad(md5('non_random'::text), 1000, md5('non_random'::text)))
, (3, rpad(md5('non_random'::text), 10000, md5('non_random'::text)))
, (4, rpad(md5('non_random'::text), 100000, md5('non_random'::text)))
, (5, rpad(md5('non_random'::text), 500000, md5('non_random'::text)))
, (6, rpad(md5('non_random'::text), 1000000, md5('non_random'::text)));
SELECT id, left(col, 10) || ' ...' AS col
, length(col) AS char_length
, pg_column_size(col) AS compressed
, pg_column_size(col || '') AS uncompressed
FROM tbl ORDER BY id;
id | col | char_length | compressed | uncompressed
---+----------------+-------------+------------+-------------
1 | 67ad0f29fa ... | 100 | 101 | 104
2 | 67ad0f29fa ... | 1000 | 1004 | 1004
3 | 67ad0f29fa ... | 10000 | 160 | 10004
4 | 67ad0f29fa ... | 100000 | 1191 | 100004
5 | 67ad0f29fa ... | 500000 | 5765 | 500004
6 | 67ad0f29fa ... | 1000000 | 11487 | 1000004
SELECT pg_column_size(rpad(md5('non_random'::text), 1000000, md5('non_random'::text)));
pg_column_size
--------------
1000004
After compression, add 8 bytes for the tuple header, and round up to the next multiple of MAXALIGN
(typically 8 bytes) to arrive at the total storage for the index tuple.
fiddle -- pg 16
fiddle -- pg 11
The maximum index tuple size was reduced from 2712 to 2704 bytes in Postgres 12. (Still the same in Postgres 17.) Also, the new compression algorithm LZ4 was added in Postgres 14, working remarkably well for highly repetitive patterns. See demo in the fiddle.
Note how the value is forced to be unpacked from its storage format with the no-op expression: pg_column_size(col || '')
.
The 5th row would be too big to fit the index tuple (even with compression) and trigger the error message in the title. Or this one for Postgres 16:
ERROR: index row size 5784 exceeds btree version 4 maximum 2704 for index "tbl_col_idx"
The 6th row would be to big to fit even the index page and trigger the related error message:
ERROR: index row requires 11504 bytes, maximum size is 8191
The test values generated with rpad()
have repeating patterns, which allow for massive compression. Even very long strings still easily fit the max. size after compression this way.
Related:
Long answer
I ran more extensive tests, tampering with storage internals to verify my understanding. Only for testing purposes!
dbfiddle does not allow write access to system catalogs for pg 11. But the queries are there to try "at home".
dbfiddle currently allows write access to pg_attribute
for pg 16. So this version shows more results.
val
in your original test case tocol
. That's the first hint at a solution. The size of the column value is relevant, not the size of the value in RAM ...