59.2. TOAST

В данном разделе рассматривается TOAST (The Oversized-Attribute Storage Technique, Методика хранения сверхбольших атрибутов).

PostgreSQL использует фиксированный размер страницы (обычно 8 Кб), и не позволяет версиям строк занимать несколько страниц. Поэтому непосредственное хранение очень больших значений полей невозможно. Для преодоления этого ограничения большие значения полей сжимаются и/или разбиваются на несколько физических строк. Это происходит незаметно для пользователя и на большую часть кода сервера влияет незначительно. Этот метод известен как TOAST (тост, или "лучшее, после изобретения нарезанного хлеба").

Лишь определённые типы данных работают с TOAST, поскольку нет необходимости выделять дополнительную память для типов данных, которые не могут занимать большой объём. Чтобы поддерживать TOAST, тип данных должен представлять значение переменной длины (varlena), в котором первое 32-разрядное слово любого хранящегося значения содержит всю длину значения в байтах (включая само слово). TOAST не ограничивает содержание оставшейся части значения. Все функции уровня C, работающие с типами данных, поддерживающими TOAST, должны адекватно воспринимать входные значения в формате TOAST. (Обычно это выполняется через вызов PG_DETOAST_DATUM перед тем, как сделать что-либо с входным значением, но в некоторых случаях возможны более эффективные подходы.)

TOAST занимает два бита значения, представляющего длину поля (старшие биты на машинах с порядком следования байтов, начиная со старшего; младшие биты — на машинах с порядком следования байтов, начиная с младшего), таким образом ограничивая логический размер любого значения типа данных, работающего с TOAST, до 1 Гб (230 - 1 байт). Когда оба бита равны нулю, значение является нормальным значением типа данных, не TOAST, а оставшиеся биты значения длины данных представляют общий размер элемента данных (включая слово длины) в байтах. Когда установлен бит самого высокого или самого низкого разряда, значение имеет только однобайтовый заголовок вместо обычного четырёхбайтового, а оставшиеся биты составляют общий размер элемента данных (включая байт, содержащий длину строки) в байтах. В особом случае, если все оставшиеся биты равны нулю (что было бы невозможно для длины, учитывающей длину своего представления), значение является указателем на данные, хранящиеся в отдельной таблице TOAST. (Размер TOAST-указателя дан во втором байте элемента данных.) Значения с однобайтовыми заголовками также не выровнены по какой-либо конкретной границе. И наконец, когда бит самого высокого или самого низкого разряда является свободным, но установлен смежный бит, содержание элемента данных было сжато и должно быть распаковано перед использованием. В этом случае оставшиеся биты слова длины составляют общий размер сжатого элемента данных, но не исходных данных. Заметьте, что сжатие также возможно для отдельных от таблицы данных, но заголовок varlena не сообщает, выполнено ли оно — об этом сообщает содержание TOAST-указателя.

Если какие-либо столбцы таблицы хранятся в формате TOAST, у таблицы будет связанная с ней таблица TOAST, OID которой хранится в значении pg_class.reltoastrelid для данной таблицы. Отдельно хранящиеся TOAST-значения хранятся в таблице TOAST, что подробнее описано ниже.

Используемый метод сжатия является одним из довольно простых и очень быстрых методов семейства LZ. Подробнее см. src/backend/utils/adt/pg_lzcompress.c.

Значения, хранящиеся отдельно, делятся на порции (после сжатия, если оно используется) размером не более TOAST_MAX_CHUNK_SIZE байт (по умолчанию это значение выбирается таким образом, чтобы на странице помещались четыре строки порций, каждая из которых составляла бы около 2000 байт). Каждая порция хранится как отдельная строка в таблице TOAST, созданной для исходной таблицы-владельца. Каждая таблица TOAST имеет столбцы chunk_id (OID, идентифицирующий конкретное TOAST-значение), chunk_seq (последовательный номер для порции внутри его значения) и chunk_data (фактические данные порции). Уникальный индекс на chunk_id и chunk_seq обеспечивает быструю выдачу значений. По этой причине элементу указателя элемента данных, представляющему отдельное TOAST-значение, необходимо хранить OID таблицы TOAST, к которой нужно обращаться, и OID определённого значения (его chunk_id). Для удобства элементы данных указателя также хранят логический размер элемента данных (размер исходных данных без сжатия) и фактический размер хранимых данных (отличающийся, если было применено сжатие). Учитывая байты заголовка, указывающие длину значения, общий размер элемента данных указателя TOAST составляет 18 байт, независимо от фактического размера значения, которое он представляет.

Код TOAST срабатывает, только когда значение строки, которая должна храниться в таблице, по размеру больше, чем TOAST_TUPLE_THRESHOLD байт (обычно это 2 Кб). Код TOAST будет сжимать и/или выносить значения поля за пределы таблицы до тех пор пока значение строки не будет меньше TOAST_TUPLE_TARGET байт (также обычно 2 Кб) или не сможет занять пространства больше, чем это значение. Во время операции UPDATE, значения неизменённых полей обычно сохраняются как есть, поэтому UPDATE строки с хранящимися отдельно значениями не несёт издержек, связанных с TOAST, если все эти значения остаются без изменений.

Код TOAST распознаёт четыре различные стратегии хранения столбцов, совместимых с TOAST:

  • PLAIN не допускает ни сжатие, ни отдельное хранение вне таблицы; кроме того, отключается использование однобайтовых заголовков для типов varlena. Это единственно возможная стратегия для столбцов типов данных, которые несовместимы с TOAST.

  • EXTENDED допускает как сжатие, так и хранение вне таблицы. Это стандартный вариант для большинства типов данных, совместимых с TOAST. Сначала происходит попытка выполнить сжатие, затем — сохранение вне таблицы, если строка всё ещё слишком велика.

  • EXTERNAL допускает хранение вне таблицы, но не сжатие. Использование EXTERNAL ускорит операции над частями строк в больших столбцах text и bytea (за счёт увеличения объёма памяти для хранения), так как эти операции оптимизированы для извлечения только требуемых частей вынесенного значения, когда оно не является сжатым.

  • MAIN допускает сжатие, но не отдельное хранение. (Фактически, отдельное хранение, тем не менее, будет выполнено для таких столбцов, но лишь как крайняя мера, когда нет другого способа уменьшить строку так, чтобы она помещалась на странице.)

Каждый тип совместимых с TOAST данных определяет стандартную стратегию для столбцов этого типа данных, но стратегия для заданного столбца таблицы может быть изменена с помощью ALTER TABLE SET STORAGE.

Эта схема имеет ряд преимуществ по сравнению с более простым подходом, когда значения строк могут занимать несколько страниц. Если предположить, что обычно запросы характеризуются выполнением сравнения с относительно маленькими значениями ключа, большая часть работы будет выполняться с использованием главной записи строки. Большие значения атрибутов в формате TOAST будут просто передаваться (если будут выбраны) в тот момент, когда результирующий набор отправляется клиенту. Таким образом, главная таблица получается гораздо меньше, и в общий кеш буферов помещается больше её строк, чем их было бы без использования отдельного хранения. Наборы данных для сортировок также уменьшаются, а сортировки чаще будут выполняться исключительно в памяти. Небольшой тест показал, что таблица, содержащая типичные HTML-страницы и их URL после сжатия занимала примерно половину объёма исходных данных, включая таблицу TOAST, и что главная таблица содержала лишь около 10% всех данных (URL и некоторые маленькие HTML-страницы). Время обработки не отличалось от времени, необходимого для обработки таблицы без использования TOAST, в которой размер всех HTML-страниц был уменьшен до 7 Кб, чтобы они уместились в строках.