Одно жёсткое правило: строка целиком должна помещаться в одну страницу
8 КБ, пересекать границу страницы она не может. Но в колонку text,
jsonb или bytea легко положить мегабайт. Как уживаются эти два факта -
через TOAST (The Oversized-Attribute Storage Technique), механизм
сжатия и выноса длинных значений.
Порог и два приёма
Когда строка после упаковки оказывается длиннее порога (около 2000 байт, примерно четверть страницы), PostgreSQL берётся за самые длинные значения переменной длины и применяет к ним два приёма по очереди:
Сначала сжатие. Если сжатого значения хватает, чтобы строка влезла, на этом всё - значение остаётся в строке, просто компактнее.
CREATE TABLE toast_demo (id int, big text);
INSERT INTO toast_demo VALUES (1, repeat('A', 5000));SELECT pg_column_size(big) AS stored, octet_length(big) AS logical FROM toast_demo;
-- stored | logical
-- --------+---------
-- 69 | 5000
5000 одинаковых букв сжались до 69 байт и спокойно остались в строке.
octet_length - логическая длина значения, pg_column_size - сколько оно
реально занимает на диске.
Когда сжатия мало: вынос наружу
Если значение плохо сжимается (случайные данные, уже сжатый JPEG), сжатие не спасает, и его выносят в отдельную TOAST-таблицу. У каждой таблицы с длинными колонками она своя:
INSERT INTO toast_demo
SELECT 2, string_agg(md5(g::text), '') FROM generate_series(1, 200) g; -- ~6400 несжимаемых байт
SELECT reltoastrelid::regclass FROM pg_class WHERE relname = 'toast_demo';
-- pg_toast.pg_toast_16566 (число в имени - oid таблицы, у тебя своё)
Внутри строки остаётся короткий указатель, а само значение лежит в
TOAST-таблице, нарезанное на чанки примерно по 1996 байт. 6400 байт
превращаются в 4 чанка. Сервер собирает значение обратно из чанков, только
когда оно реально понадобится в запросе - короткий SELECT id длинное
поле даже не трогает.
Четыре стратегии хранения
Поведение колонки задаёт её стратегия (ALTER TABLE ... ALTER COLUMN ... SET STORAGE):
| Стратегия | Сжимать | Выносить |
|---|---|---|
| PLAIN | нет | нет (для типов фиксированной длины) |
| MAIN | да | в последнюю очередь |
| EXTERNAL | нет | да |
| EXTENDED | да | да (по умолчанию для длинных типов) |
EXTERNAL иногда ставят осознанно: без сжатия чтение куска большого
значения (подстроки, среза bytea) быстрее, потому что не надо
распаковывать всё целиком.
Подводный камень: длинное значение и btree
Значение длиннее примерно трети страницы нельзя положить в btree-индекс целиком - запись индекса не помещается. Поэтому индексировать большой текст «как есть» не выйдет: для поиска по длинным значениям берут другие методы (например, индекс по выражению-хешу или полнотекстовый). Сам факт, что значение ушло в TOAST, на это не влияет - ограничение именно у btree.