# TOAST: вынос и сжатие длинных значений _Хранение и формат на диске · PostgreSQL Knowledge Base_ **TL;DR:** Строка не может пересекать границу страницы 8 КБ, поэтому длинные значения PostgreSQL сначала сжимает, а если всё равно не влезают - выносит во вторичную TOAST-таблицу чанками примерно по 2 КБ. Порог срабатывает около 2000 байт на строку. Управляется стратегией хранения: PLAIN, MAIN, EXTERNAL, EXTENDED. Одно жёсткое правило: строка целиком должна помещаться в одну страницу 8 КБ, пересекать границу страницы она не может. Но в колонку `text`, `jsonb` или `bytea` легко положить мегабайт. Как уживаются эти два факта - через **TOAST** (The Oversized-Attribute Storage Technique), механизм сжатия и выноса длинных значений. ## Порог и два приёма Когда строка после упаковки оказывается длиннее порога (около 2000 байт, примерно четверть страницы), PostgreSQL берётся за самые длинные значения переменной длины и применяет к ним два приёма по очереди: ```mermaid flowchart TD R["Строка длиннее ~2000 байт?"] -->|нет| INLINE["лежит в странице как есть"] R -->|да| C["сжать длинные поля"] C --> F["теперь влезает?"] F -->|да| INLINE2["хранится в строке, но в сжатом виде"] F -->|нет| EXT["вынести в TOAST-таблицу чанками по 1996 байт"] ``` Сначала **сжатие**. Если сжатого значения хватает, чтобы строка влезла, на этом всё - значение остаётся в строке, просто компактнее. ```sql 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-таблицу. У каждой таблицы с длинными колонками она своя: ```sql 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. ## Команды ```sql SELECT pg_column_size(big), octet_length(big) FROM toast_demo; ``` Физический размер на диске против логической длины значения ```sql SELECT reltoastrelid::regclass FROM pg_class WHERE relname = 'toast_demo'; ``` Имя TOAST-таблицы, обслуживающей длинные колонки ```sql ALTER TABLE toast_demo ALTER COLUMN big SET STORAGE EXTERNAL; ``` Сменить стратегию: выносить без сжатия ```sql SHOW default_toast_compression; ``` Алгоритм сжатия по умолчанию: pglz или lz4 ## См. также - [Раскладка страницы 8 КБ](/courses/postgres/kb/page-layout.md) - [Выравнивание и порядок колонок](/courses/postgres/kb/column-alignment.md) - [Заголовок кортежа](/courses/postgres/kb/tuple-header.md)