linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Главы
  • How it worksскоро
  • Уроки
  • База знаний
  • Собеседование
Часть I — Хранение данных

$ глава 8 · 50 минут

Длинные значения: TOAST

Мы выяснили, что строка не может пересекать границу страницы 8 КБ. Но в колонку text или jsonb легко положить мегабайт. Как уживаются эти два факта? Через механизм, который сжимает длинные значения, а если не помогает - выносит их в отдельное хранилище. Он называется TOAST, и это последняя глава части про хранение.

В лабе мы положим в таблицу и хорошо сжимаемое, и несжимаемое значение и своими глазами увидим, где каждое осело: одно осталось в строке, другое ушло наружу чанками.

8.1 Строка не пересекает страницу

Одно жёсткое правило хранения: строка целиком должна помещаться в одну страницу 8 КБ. Пересечь границу страницы она не может. Раскладку страницы мы разбирали в page-layout - там просто нет механизма «продолжить строку на следующей странице».

Но пользователь кладёт в текстовую колонку статью на сто килобайт. Если бы такое значение требовалось целиком впихнуть в строку, оно бы не влезло ни в какую страницу. Нужен обходной путь, и он называется TOAST (The Oversized-Attribute Storage Technique) - техника хранения негабаритных значений.

8.2 Порог и два приёма

Когда строка после упаковки оказывается длиннее порога (около 2000 байт, примерно четверть страницы), PostgreSQL берётся за самые длинные значения переменной длины и применяет два приёма по очереди:

Сначала сжатие. Если сжатого значения хватает, чтобы строка влезла, на этом всё. Если нет - значение выносится наружу. Разберём оба случая на эксперименте. Полный справочник - в toast.

8.3 Сжатие на месте

Положим хорошо сжимаемое значение - пять тысяч одинаковых букв:

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 - реальный размер на диске. Разница между ними - это и есть работа сжатия.

8.4 Вынос наружу

Теперь несжимаемое значение - случайный текст, который сжатие не берёт:

sql
INSERT INTO toast_demo
SELECT 2, string_agg(md5(g::text), '') FROM generate_series(1, 200) g;
-- ~6400 несжимаемых байт
SELECT id, pg_column_size(big) AS stored, octet_length(big) AS logical
FROM toast_demo WHERE id = 2;
--  id | stored | logical
-- ----+--------+---------
--   2 |   6400 |    6400

Сжатие не помогло: stored равен logical. Значит, значение вынесено в отдельную TOAST-таблицу. У каждой таблицы с длинными колонками она своя:

sql
SELECT reltoastrelid::regclass FROM pg_class WHERE relname = 'toast_demo';
-- pg_toast.pg_toast_16566   (число - oid, у тебя своё)

Внутри строки остаётся короткий указатель, а само значение лежит в TOAST-таблице, нарезанное на чанки примерно по 1996 байт. 6400 байт превращаются в 4 чанка. Сервер собирает значение из чанков, только когда оно реально нужно в запросе: короткий SELECT id длинное поле даже не трогает.

8.5 Четыре стратегии хранения

Поведение колонки задаёт её стратегия хранения. Менять её можно через ALTER TABLE ... ALTER COLUMN ... SET STORAGE:

СтратегияСжиматьВыносить
PLAINнетнет (только типы фиксированной длины)
MAINдав последнюю очередь
EXTERNALнетда
EXTENDEDдада (по умолчанию для длинных типов)

По умолчанию длинные типы (text, jsonb, bytea) получают EXTENDED: сжать, при необходимости вынести. EXTERNAL иногда ставят осознанно: без сжатия чтение куска большого значения (подстроки, среза bytea) быстрее, потому что не надо распаковывать всё целиком.

Узнать алгоритм сжатия по умолчанию:

sql
SHOW default_toast_compression;   -- pglz (или lz4, если включён)

8.6 Подводный камень: длинное значение и btree

Есть ограничение, которое легко не заметить. Значение длиннее примерно трети страницы нельзя положить в btree-индекс целиком - запись индекса не помещается в страницу индекса. Поэтому попытка проиндексировать большой текст «как есть» (CREATE INDEX ON t (big_text_column)) упрётся в ошибку на длинной строке.

То, что значение ушло в TOAST, тут не помогает: ограничение именно у btree, а не у хранения. Для поиска по длинным значениям берут другие подходы: индекс по хешу или короткому префиксу, либо полнотекстовый индекс по разобранному на слова представлению. Но это уже тема части про индексы.

Уроки в sandbox

lab-8.1. Где живёт большой текст

Положи в таблицу хорошо сжимаемое и несжимаемое значение и определи, где каждое осело. Перед каждым измерением предскажи: останется ли значение в строке или уйдёт в TOAST-таблицу.

  1. Создай таблицу toast_demo(id int, big text).

  2. Вставь хорошо сжимаемое значение: repeat('A', 5000). Предскажи pg_column_size(big) - больше или меньше порога?

  3. Проверь: SELECT pg_column_size(big), octet_length(big) FROM toast_demo; - сравни физический размер с логическим.

  4. Вставь несжимаемое значение из string_agg(md5(g::text), '') по generate_series(1,200) - около 6400 байт.

  5. Сравни stored и logical для несжимаемого: если они равны, значение вынесено наружу без сжатия.

  6. Найди TOAST-таблицу: SELECT reltoastrelid::regclass FROM pg_class WHERE relname='toast_demo'; - предскажи, есть ли она вообще.

sandbox с автопроверкой - открыть в песочнице

Резюме

  • Строка не может пересекать границу страницы 8 КБ, поэтому длинные значения обрабатываются особо.
  • При превышении порога (~2000 байт) PostgreSQL сначала сжимает длинные поля, потом выносит наружу.
  • Сжимаемое значение остаётся в строке в сжатом виде; несжимаемое уходит в TOAST-таблицу чанками по ~1996 байт.
  • У каждой таблицы с длинными колонками своя TOAST-таблица (reltoastrelid); значение собирается из чанков по требованию.
  • Стратегии хранения: PLAIN, MAIN, EXTERNAL, EXTENDED (по умолчанию для длинных типов).
  • Значение длиннее ~1/3 страницы нельзя положить в btree-индекс - ограничение btree, не хранения.

Контрольные вопросы

  1. Какие два приёма и в каком порядке применяет TOAST к длинному значению?

    Показать ответ

    Сначала сжатие: длинные поля переменной длины сжимаются, и если строка после этого влезает под порог, значение остаётся в строке в сжатом виде. Если сжатия мало, применяется второй приём - вынос наружу: значение перемещается в отдельную TOAST-таблицу и нарезается на чанки примерно по 1996 байт, а в строке остаётся короткий указатель на него.

  2. Как по pg_column_size и octet_length понять, что произошло со значением?

    Показать ответ

    octet_length - логическая длина значения, pg_column_size - его реальный размер на диске. Если pg_column_size заметно меньше octet_length - значение сжалось и, скорее всего, осталось в строке. Если они примерно равны и при этом велики - значение несжимаемо и вынесено в TOAST-таблицу без сжатия. Так по двум числам видно судьбу значения.

  3. Зачем существует стратегия EXTERNAL, если EXTENDED умнее?

    Показать ответ

    EXTENDED сжимает значение перед выносом, EXTERNAL выносит без сжатия. EXTERNAL ставят осознанно, когда часто читают не всё значение целиком, а его куски - подстроки текста, срезы bytea. Несжатое значение можно прочитать частично, не распаковывая целиком, поэтому частичные чтения получаются быстрее. Это размен места (без сжатия значение крупнее) на скорость частичного доступа.

  4. Почему нельзя просто построить btree-индекс по длинному текстовому полю?

    Показать ответ

    Потому что запись btree-индекса должна помещаться в страницу индекса, а значение длиннее примерно трети страницы туда не влезает. Ограничение именно у btree, и вынос значения в TOAST его не обходит. Для поиска по длинным значениям используют другие подходы: индекс по хешу или префиксу либо полнотекстовый индекс по разобранному представлению.

  5. Почему короткий SELECT id не платит за длинное значение в той же строке?

    Показать ответ

    Потому что вынесенное в TOAST значение хранится отдельно, а в самой строке лежит только короткий указатель на него. Сервер собирает большое значение из чанков TOAST-таблицы лишь тогда, когда оно реально понадобилось в запросе. SELECT id длинную колонку не запрашивает, поэтому её чанки не читаются - запрос платит только за нужные колонки.

← Предыдущая07-column-alignmentСледующая →09-why-mvcc
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки