70.2. TOAST #

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

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

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

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

Метод, который будет применяться для сжатия данных при внутреннем и внешнем хранении, можно выбрать для каждого отдельного столбца, задав параметр COMPRESSION в команде CREATE TABLE или ALTER TABLE. Если метод сжатия для столбца не задан явным образом, по умолчанию при вставке данных будет использоваться метод из параметра default_toast_compression.

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

70.2.1. Отдельное размещение TOAST на диске #

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

Отделённые значения делятся на порции (после сжатия, если оно применяется) размером не более 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). Для удобства в данных указателя также хранится логический размер элемента данных (исходных данных без сжатия), фактический размер хранимых данных (отличающийся, если было применено сжатие) и используемый метод сжатия, если он задан. Учитывая байты заголовка varlena, общий размер указателя на хранимое на диске значение TOAST составляет 18 байт, независимо от фактического размера собственно значения.

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

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

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

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

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

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

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

TOAST_TUPLE_TARGET можно задавать на уровне таблиц с помощью команды ALTER TABLE ... SET (toast_tuple_target = N)

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

70.2.2. Отдельное размещение TOAST в памяти #

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

Косвенный указатель TOAST просто указывает на значение varlena, хранящееся где-то в памяти. Этот вариант изначально был реализован просто как подтверждение концепции, но в настоящее время он применяется при логическом декодировании, чтобы не приходилось создавать физические кортежи больше одного 1 ГБ (что может потребоваться при консолидации всех отделённых значений полей в одном кортеже). Данный вариант имеет ограниченное применение, так как создатель такого указателя должен полностью понимать, что целевые данные будут существовать, только пока существует указатель, и никакой инфраструктуры для сохранения их нет.

Указатели на развёрнутые данные TOAST полезны для сложных типов, представление которых на диске плохо приспособлено для вычислительных целей. Например, стандартное представление в виде varlena массива Postgres Pro включает информацию о размерности, битовую карту элементов NULL (если они в нём содержатся), а затем значения всех элементов по порядку. Когда элемент сам по себе имеет переменную длину, единственный способ найти N-ый элемент — просканировать все предыдущие элементы. Это представление компактно и поэтому подходит для хранения на диске, но для вычислительной обработки массива гораздо удобнее иметь «развёрнутое» или «деконструированное» представление, в котором можно определить начальные адреса всех элементов. Механизм указателей TOAST способствует решению этой задачи, допуская передачу по ссылке элемента Datum как указателя на стандартное значение varlena (представление на диске) или указателя TOAST на развёрнутое представление где-то в памяти. Детали развёрнутого представление определяются самим типом данных, хотя оно может иметь стандартный заголовок и удовлетворять другим требованиям API, описанным в src/include/utils/expandeddatum.h. Функции уровня C, работающие с этим типом, могут реализовать поддержку любого из этих представлений. Функции, не знающие о развёрнутом представлении, а просто применяющие PG_DETOAST_DATUM к своим входным данным, будут автоматически получать традиционное представление varlena; так что поддержка развёрнутого представления может вводиться постепенно, по одной функции.

Указатели TOAST на развёрнутые значения далее подразделяются на указатели для чтения/записи и указатели только для чтения. Представление, на которое они указывают, в любом случае одинаковое, но функции, получающей указатель для чтения/записи, разрешается модифицировать целевые данные прямо на месте, тогда как функция, получающая указатель только для чтения, не должна этого делать; если ей нужно получить изменённую версию значения, она должна сначала сделать копию. Это отличие и связанные с ним соглашения позволяют избежать излишнего копирования развёрнутых значений при выполнении запросов.

Для всех типов указателей TOAST на данные в памяти, код обработки TOAST гарантирует, что такие данные не окажутся случайно сохранены на диске. Указатели TOAST в памяти автоматически сворачиваются в обычные значения varlena перед сохранением — а затем могут преобразоваться в указатели TOAST на диске, если без этого не смогут уместиться в содержащем их кортеже.

70.2.3. Подключаемый механизм TOAST #

Механизм TOAST является частью ядра Postgres Pro. Он использует одну стратегию для всех типов данных, не является расширяемым и неэффективен в работе со структурированными данными (JSON) или данными, требующими специальной обработки (bytea).

Экспериментальный подключаемый механизм TOAST предоставляет открытый API, позволяющий разрабатывать и использовать собственные реализации TOAST для столбцов таблиц и типов данных в дополнение к стандартной реализации. Реализации TOAST могут обрабатывать данные TOAST, используя знания о внутренней структуре данных и рабочем процессе.

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

Подключаемый механизм TOAST состоит из ядра, то есть API TOAST, и настраиваемых реализаций TOAST, подключаемых к Postgres Pro через этот API.

Этот API никак не связан с логикой функциональности TOAST, а является просто обёрткой, позволяющей регистрировать, вызывать и удалять подключённые реализации TOAST.

Чтобы разрешить применение пользовательских реализаций TOAST, API TOAST определяет новый тип Custom указателя TOAST в дополнение к External и Extended (соответствует стратегиям EXTERNAL и EXTENDED хранения столбцов, совместимых с TOAST).

В настоящее время подключаемый механизм TOAST имеет несколько ограничений:

  • Пользовательские реализации TOAST должны назначаться только для столбцов со стратегией хранения EXTERNAL.

  • В настоящее время подключаемый механизм TOAST не поддерживает логическую репликацию. Пользовательский указатель TOAST пропускается механизмом логической репликации и не реплицируется. Это связано с тем, что поддержка пользовательских указателей требует серьёзных изменений в ядре Postgres Pro (механизме логической репликации) и даже изменения архитектуры, поскольку теперь пользовательские указатели позволяют изменять значения в формате TOAST, а указатели по умолчанию — нет.

  • Пользовательские реализации TOAST не восстанавливаются из копий и требуют восстановления вручную. Расширения пользовательских реализаций TOAST не восстанавливаются из копий, поэтому обработанные ими данные недоступны. Ещё одна особенность заключается в том, что одно и то же расширение пользовательской реализации TOAST имеет разные идентификаторы в разных системах, поэтому для восстановления файла TOAST из копии потребуется определить пользовательскую реализацию TOAST, используемую для обработки данных.

  • Когда столбцу таблицы назначается пользовательская реализация TOAST, во всех транзакциях после этого назначения данные, хранящиеся в TOAST-таблицах, обрабатываются реализацией, назначенной этому столбцу.

  • Когда столбцу таблицы назначается пользовательская реализация TOAST, старые значения остаются неизменными, но при изменении старого значения оно сохраняется в формате TOAST, используя назначенную реализацию, из-за особенной работы механизма TOAST по умолчанию.

  • Если реализация TOAST удалена, невозможно обратиться стандартными методами к данным, преобразованным с её помощью в формат TOAST, и они будут считаться потерянными. Чтобы защитить значения в формате TOAST, API создаёт в системе некоторые зависимости (таблица PG_DEPEND):

    • При добавлении реализации TOAST в систему создаётся зависимость реализации TOAST от расширения.

    • При назначении реализации TOAST столбцу создаётся зависимость реализации TOAST от отношения.

    • При удалении реализации TOAST, назначенной столбцу, удаляется зависимость реализации TOAST от отношения.

    • При удалении реализации TOAST из системы удаляется зависимость реализации TOAST от расширения.

Пользовательские реализации TOAST добавляются как расширения, выполняющие некоторую обязательную функциональность (за дополнительными сведениями обратитесь к файлу README.toastapi в каталоге /contrib/toastapi). Подключаемый механизм TOAST содержит набор функций SQL для управления пользовательскими реализациями TOAST. Эти функции вызываются командой SQL SELECT:

add_toaster(toaster_name text, toaster_handler_func text)integer

Подключить новую реализацию TOAST (добавленную ранее как расширение). Эта функция также создаёт зависимость в PG_DEPEND для OID реализации TOAST от OID её расширения. Эта зависимость используется, чтобы реализация TOAST не удалялась при удалении расширения этой реализации.

Аргументы:

  • toaster_name — имя реализации TOAST, которое будет храниться в PG_TOASTER. Должно быть уникальным.

  • toaster_handler_func — имя функции-обработчика реализации TOAST (автоматически добавляется как regproc при установке расширения реализации TOAST).

Возвращает OID, присвоенный новой реализации TOAST.

set_toaster(toaster_name text, tab_name text, col_name text)integer

Назначить столбцу таблицы реализацию TOAST. Эта функция также создаёт зависимость в PG_DEPEND для OID реализации TOAST от OID отношения. Эта зависимость используется для защиты данных в формате TOAST путём ограничения на удаление реализации TOAST, назначенной столбцу.

Аргументы:

  • toaster_name — имя реализации TOAST, назначенной столбцу таблицы.

  • tab_name — имя таблицы.

  • col_name — имя столбца.

Возвращает OID реализации TOAST, назначенной столбцу.

reset_toaster(tab_name text, col_name text)integer

Сбросить назначенную столбцу таблицы реализацию TOAST и назначить механизм TOAST по умолчанию. Эта функция удаляет зависимость от отношения реализации TOAST, что позволяет затем удалить реализацию TOAST при удалении всех зависимостей.

Аргументы:

  • tab_name — имя таблицы.

  • col_name — имя столбца таблицы.

Всегда возвращает 0.

drop_toaster(toaster_name text)

Удалить реализацию TOAST из системы. Можно удалять только неиспользуемые реализации TOAST, не имеющие данных в формате TOAST. Удаление защищено зависимостями, создаваемыми функциями add_toaster и set_toaster. Если зависимость всё ещё существует, реализация TOAST не будет удалена. Если реализация успешно удалена, расширение с этой реализацией можно удалить командой DROP EXTENSION.

Аргументы:

  • toaster_name — имя удаляемой реализации TOAST.

Возвращает OID реализации TOAST в случае успеха или 0, если реализация TOAST используется.

get_toaster(tab_name text, col_name text)integer

Получить OID реализации TOAST, назначенной столбцу таблицы.

Аргументы:

  • tab_name — имя таблицы.

  • col_name — имя столбца таблицы.

Возвращает OID реализации TOAST, назначенной столбцу, или 0, если она не назначена.

В следующем примере показано использование подключаемого API TOAST:

CREATE EXTENSION toastapi;

CREATE EXTENSION bytea_toaster;

SELECT add_toaster('bytea_toaster');

CREATE TABLE test_bytea_append (id int, a bytea);

ALTER TABLE test_bytea_append ALTER a SET STORAGE external;

SELECT set_toaster('bytea_toaster', 'test_bytea_append', 'a');
 test_set_toaster 
------------------
 
(1 row)

...

SELECT get_toaster('test_bytea_append', 'a') AS bytea_toaster_oid;
 get_toaster 
-------------
        16348
(1 row)


SELECT pgpro_toast.reset_toaster('test_bytea_append','a');
 reset_toaster 
---------------
             0
(1 row)

DROP TABLE test_bytea_append;
SELECT pgpro_toast.drop_toaster('bytea_toaster');
 drop_toaster 
--------------
         16348
(1 row)

DROP EXTENSION bytea_toaster;