Перейти к основному содержимому

Сжатие данных на уровне страниц (блоков)

Краткое описание

С версии 6.5.0 в Pangolin доступно сжатие данных на уровне страниц (блоков) СУБД. Теперь к файлам данных таблиц и индексов можно применять алгоритмы сжатия по страницам. Сжатие и распаковка происходят прозрачно для приложений, то есть не требуют модификации ни приложений, ни описаний объектов в схеме.

Контекст и область применения

Сжатие данных Pangolin

Сжатие данных на уровне страниц можно применять к отношениям (таблицам / секциям таблиц, материализованным представлениям, индексам), которые содержат большие объемы текстовой или другой повторяющейся информации в блоке. Сжимаются:

  • обычные таблицы, за исключением TOAST;
  • секции партиционированных таблиц;
  • материализованные представления;
  • индексы, встроенные в ядро Pangolin: B-Tree, Hash, GiST, SP-GiST и GIN.

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

Важно не путать алгоритм сжатия Pangolin со стандартным сжатием TOAST. Они работают на разном уровне, по-разному включаются и настраиваются, просматриваются разными инструментами. Эти алгоритмы не пересекаются. Смотрите раздел «Отличия от сжатия TOAST».

Архитектура, принципы и концепции

Включение

По умолчанию функциональность выключена. Включается параметром сервера enable_page_compression. Изменение значения требует перезапуска.

warning

При запуске сервер проверяет наличие сжатых отношений и, если они есть, а сжатие отключено, то отказывается стартовать с ошибкой. В логе ошибка будет дополнена relfilenode сжатых объектов:

$ pg_ctl start
<..>
2026-04-09 17:26:12 MSK [3225]: [8-1] app=,user=,db=,client=,type=postmaster WARNING: page compression feature is disabled, but there are 4 compressed relations within the database
2026-04-09 17:26:12 MSK [3225]: [9-1] app=,user=,db=,client=,type=postmaster DETAIL: relfilenode list of compressed relations: 65460, 65463, 65455, 24271
2026-04-09 17:26:12 MSK [3225]: [10-1] app=,user=,db=,client=,type=postmaster HINT: cannot start up until at least one compressed relation exists.
stopped waiting
pg_ctl: could not start server
Examine the log output.

Чтобы избежать ошибки запуска в кластере с физической репликацией, следует параметр устанавливать на всех узлах одинаковым. Лучше настроить его в DCS, не забывая о секции bootstrap конфигурации Pangolin Manager.

Выбор объектов

Сжатым может быть объявлено отношение (таблица, секция, материализованное представление, индекс) или табличное пространство. Для этого используются параметры хранения в разделе WITH () предложения CREATE объект. Обязательный параметр - compresstype, к нему можно добавить дополнительные (ниже).

Параметры сжатия индекса устанавливаются только через его собственные параметры хранения. Они не наследуются от настроек таблицы. Если к сжатой таблице добавить индекс по умолчанию, без параметра compresstype, то он будет создан несжатым.

Сжатый индекс объявляется так:

CREATE TABLE compressed_table_1(id int PRIMARY KEY WITH (compresstype=pglz), c1 text) WITH (compresstype=pglz);
CREATE INDEX compressed_table_1_idx_1 on compressed_table_1 USING btree(c1) WITH (compresstype=pglz, compresslevel=1, compress_chunk_size=1024, compress_prealloc_chunks=2);

При создании секционированной таблицы не получится настроить параметры сжатия будущих секций, вернется ошибка ERROR: unrecognized parameter "compresstype". Параметры сжатия могут быть установлены только для секций поодиночке.

Хранение сжатого объекта

Во время создания сжатого объекта его физическая структура на диске дополняется и создаются два новых двоичных файла:

  • relfilenode_pcd - постранично сжатые данные (page compressed data);
  • relfilenode_pca - постранично сжатые адреса (page compressed addresses).

В первом хранятся сжатые данные по номеру исходного блока, поделенные на фрагменты. Каждый фрагмент защищен своей контрольной суммой. Второй – описание параметров сжатия объекта и карта, отображающая номера исходных блоков в номера сжатых фрагментов.

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

В карте адресов записи, относящиеся к блоку данных, всегда находятся в определенном месте файла. Этот «слот» можно вычислить по системному номеру блока, каким он был бы в исходном файле. А вот в файле сжатых данных порядок выделения пространства под фрагменты не гарантирован даже для одного блока, он произвольный.

Структура и соотношение файлов PCA и PCD (детально описаны в документации):

В процессе изменения и пополнения блоков может возникать трудно контролируемая фрагментация, при которой фрагменты не только соседних блоков, но и одного блока окажутся в разных регионах файла PCD. Не гарантирован и общий порядок фрагментов между лидером и репликой в физической репликации (смотрите ниже).

Поставить развитие процесса под контроль призвано предварительное выделение фрагментов под блок (смотрите параметр compress_prealloc_chunks). Его цена - расход дискового пространства при добавлении блоков. Если впоследствии блок так и не будет пополнен реальными данными, то выделенное дисковое пространство будет пустовать.

Упорядочение файла данных не производится.

примечание

Для однажды объявленного объекта изменение параметров сжатия не поддерживается. Чтобы изменить параметры, придется объявить объект заново, перенести данные и удалить старый объект.

Инструменты низкого уровня

Расширение pageinspect дополнено функциями:

  • get_compress_address_header() – просмотр заголовка PCA-файла;
  • get_compress_address_items() – просмотр содержимого адресной записи в PCA-файле;
  • page_compress() – сжать одну страницу и получить в виде результата сжатые данные;
  • relation_estimate_compression_ratio() – сжать диапазон страниц и получить в виде результата степень сжатия.

Влияние на форматы данных в памяти

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

Чтобы в сжатом блоке поменять хотя бы 1 бит, серверу придется:

  1. Собрать фрагменты в непрерывный блок в соответствии с их номером следования.
  2. Распаковать собранный блок.
  3. Выполнить обычное изменение блока, включая перерасчет стандартной контрольной суммы блока (ванильная CRC).
  4. Сжать весь блок.
  5. Снова разделить его на фрагменты, подсчитать контрольную сумму каждого и записать результаты на диск.

Те же действия по получении записи об изменении придется самостоятельно повторить каждой из реплик – смотрите следующий раздел.

Влияние на резервирование и восстановление

Механизм не вносит изменений в процедуры снятия дампов/архивных копий/бэкапов СУБД. При этом в режиме инкрементальной копии сжатые файлы будут копироваться в полном объеме.

В журнал предзаписи WAL сжатые данные не попадают ни в виде страниц, ни в виде фрагментов.

Файлы _pca и _pcd сами по себе не защищены WAL. Точнее, журналом защищены не физические (сжатые) данные, а логические записи в том виде, в котором они представлены в несжатом блоке, в памяти. Именно по этой причине физическая реплика, получив запись об изменении 1 бита на лидере, второй раз полностью повторит алгоритм распаковки-сжатия (1-5) из предыдущего раздела.

warning

Порядок поступления блоков на диск может быть разным во время деятельности лидера («стирка» буферов, выполнение контрольных точек) и во время восстановления копии/реплики. По этой причине не гарантируется ни одинаковый порядок фрагментов в _pcd лидера и реплик, ни действительность файла _pca от одного из узлов на остальных узлах.

При этом атрибут-признак сжатого объекта в pg_class – реплицируется обычным образом, это часть данных pg_class.

Чтобы описать новые файлы, без изменения формата WAL (сохраняя обратную совместимость) расширены существующие записи контейнера (resource manager) Storage Manager (SMGR): XLOG_SMGR_CREATE и XLOG_SMGR_TRUNCATE. К ним добавлены поля для опций сжатия, а заголовок записи дополнен новым флагом XLR_REL_COMPRESS в поле xl_info. При воспроизведении расширенных записей процесс восстановления startup, встречая дополненные записи, пересоздает файлы _pca и _pcd в обычном процессе восстановления резервной копии или обновления физической реплики.

Дополнительные действия при поступлении описания изменения в блоке существенно различаются в реализациях для Pangolin 6.Х.Х и 7.1.0+:

Pangolin 6.Х.ХPangolin 7.1.0+
Запись relfilenode без нарушения совместимости по формату WAL не предоставляет места для опций сжатия.

Вместо получения опций сжатия в relfilenode, при восстановлении или на реплике делаются постоянные проверки, есть ли для main файла соответствующий файл _pca. Если есть (был ранее скопирован или создан при воспроизведении записи XLOG_SMGR_CREATE), то объект считается сжатым. Тогда читается заголовок файла _pca. Из него выделяются опции сжатия и добавляются в локальный кэш процесса. Если файл _pca не найден, объект считается несжатым.

Такая процедура выполняется на каждую запись, где передается relfilenode, потому что поля этой структуры с опциями сжатия нужно заполнить: потенциально, эта информация может быть использована при чтении блока из файла
Опции сжатия передаются в WAL вместе со структурами relfilenode/relfilelocator при передаче записей, в которых затрагиваются блоки. Точнее, под них использовано место в конце структуры relfilenode.

Используется оптимизация: если несколько записей изменяют один и тот же блок последовательно, то для них будет один раз передан общий relfilenode.

В записи после relfilenode следует логический номер блока, каким он был бы в несжатом файле данных. Его хватает в качестве ключа для того, чтобы войти в файл _pca, объединить фрагменты и выполнить распаковку - изменение - сжатие блока

Нет необходимости в доработках pg_waldump/pg_walinspect, потому что используются стандартные механизмы Resource Manager и форматы записей. В случае 7.1.0 опции сжатия в записях relfilenode видны обычным образом.

Рекомендации и лучшие практики

Применения

  • Экономия дисковых ресурсов в условиях высокой стоимости.
  • Хранение исторических, малоактуальных данных в БД с экономией дискового пространства.

Не рекомендуется

  • Данные, преобразованные алгоритмом защиты TDE, потому что выполняется сначала TDE и только после него – сжатие. В таком порядке сжатие почти бесполезно, но обратный порядок сделал бы TDE уязвимым для подбора ключа по известным данным (по образцу атак CRIME для TLS, BREACH для HTTPS). Стоит отметить, что подходы, подобные BREACH/CRIME, можно теоретически представить себе для wal_compression при передаче журналов по SSL и для TOAST в TDE. Эти методы в первую очередь выполняют сжатие, только потом – защитное преобразование данных.
  • Оперативные участки транзакционной системы, постоянно изменяемые большим количеством процессов.

Влияние на производительность оказывают

  • Выбранный алгоритм.
  • Для алгоритма zstd - уровень сжатия.
  • Размер фрагмента (чанка). Чанк – минимальная единица хранения сжатых данных. Чем меньше чанк, тем выше степень сжатия, но больше накладные расходы и выше риск фрагментации. В чанке нет отдельных служебных структур алгоритма сжатия, сначала блок сжимается весь, а затем для хранения получившиеся сжатые данные дробятся на чанки (фрагменты). Однако, каждый чанк защищен своей контрольной суммой.

Для предварительной оценки эффекта сжатия для отношения в расширение pageinspect добавлена функция relation_estimate_compression_ratio(). Она принимает идентификатор отношения и параметры алгоритма, читает данные и пробует сжимать их, после чего выдает примерную оценку степени сжатия.

Ограничения

  1. Параметр full_page_writes должен быть включен.

  2. Не поддерживается сжатие таблиц системного каталога, временных таблиц, последовательностей, индекса BRIN и глобального индекса.

  3. В процессе очистки (VACUUM) не поддерживается удаление пустых страниц в конце сегмента сжатого отношения. Возможно только усечение всего сегмента.

  4. Постраничное сжатие не подходит для сжатия отношений небольшого размера. Фиксированное пространство, занимаемое файлами сжатых данных (PCA + PCD), может оказаться больше, чем объем объекта до сжатия.

  5. Не поддерживается изменение параметров сжатия, в том числе для табличных пространств. Попытка изменить параметры завершится ошибкой:

    ERROR:  Unsupported feature
    DETAIL: Don't allow ALTER an option "compresstype"
  6. Файлы адресов и фрагментов сжатых данных копируются в инкрементальные резервные копии полностью.

  7. Планировщик не «обучен» как-либо учитывать фактическую фрагментацию данных в файле PCD в стоимости операций. Статистика, которая описывала бы упорядоченность файла PCD наподобие pg_stats.correlation для таблицы по индексу, еще не добавлена.

  8. Сжатие объемных значений атрибутов (например, мегабайтов текста в поле типа text) может выполняться только алгоритмом TOAST, потому что большая часть таких данных будет обязательно вытеснена из файлов основной таблицы в TOAST. PostgreSQL не имеет механизма распределения одной записи между блоками основной таблицы, подобного chained rows в Oracle.

  9. Алгоритм сжатия физически вызывается при вытеснении буфера на диск. В хорошо настроенной СУБД с достаточными ресурсами это обычно происходит в процессах bgwriter или checkpointer. Оба они – одиночные процессы без нитей. Если данные в сжатых объектах постоянно меняются и есть пики потребления 100% CPU у checkpointer, то придется обратить внимание на checkpoint_timeout, max_wal_size и checkpoint_completion_target, чтобы сделать более пологими «зубья пилы» в картине нагрузки на диск. Если checkpointer не справляется, часть нагрузки лучше отдать bgwriter (bgwriter_delay, bgwriter_lru_maxpages, bgwriter_lru_multiplier).

  10. Сконвертировать объекты в сжатые с минимальной блокировкой при помощи pg_repack по состоянию на 6.7.0/7.1.1 не получится – есть известный дефект pg_repack.

Примеры

Примеры создания объектов БД с параметрами сжатия

Сжатая таблица с несжатым индексом-первичным ключом. Параметры сжатия при первичном ключе не заданы, поэтому автоматически созданный индекс compressed_table_1_pkey не будет сжатым:

CREATE TABLE compressed_table_1 (id int PRIMARY KEY, c1 text) WITH (compresstype=pglz);

Сжатая таблица со сжатым первичным ключом, параметры сжатия разные. Таблица будет создана с фрагментом 512 байт, а индекс compressed_table_2_pkey – 1024 байт:

CREATE TABLE compressed_table_2 (id int PRIMARY KEY WITH (compresstype=pglz, compress_chunk_size=1024), c1 text) WITH (compresstype=pglz, compress_chunk_size=512);

Алгоритм zstd позволяет настраивать уровень сжатия. Выбор уровня по умолчанию в библиотеке zstd, обычно это 3:

CREATE TABLE compressed_table_3(id int PRIMARY KEY, c1 text) WITH (compresstype=zstd, compresslevel=0, compress_chunk_size=1024);

Выбор минимального уровня сжатия с фрагментом 2048 байт и предварительным выделением 1 фрагмента на блок (по умолчанию 0):

CREATE TABLE compressed_table_4(id int PRIMARY KEY, c1 text) WITH (compresstype=zstd, compresslevel=1, compress_chunk_size=2048, compress_prealloc_chunks=1);

Добавление сжатого материализованного представления к сжатой таблице:

CREATE MATERIALIZED VIEW compressed_mv_1 WITH (compresstype=pglz, compresslevel=0, compress_chunk_size=1024) AS SELECT id, c1 FROM compressed_table_1 WHERE id <= 10;

Добавление сжатого индекса B-Tree с фрагментом 1024 байта и предварительным выделением 2 фрагментов на блок:

CREATE INDEX compressed_table_1_idx_1 on compressed_table_1 USING btree(c1) WITH(compresstype=pglz, compresslevel=1, compress_chunk_size=1024, compress_prealloc_chunks=2);

Добавление сжатого индекса Hash с теми же настройками:

CREATE INDEX compressed_table_1_idx_2 on compressed_table_1 USING hash(c1) WITH(compresstype=pglz, compresslevel=1, compress_chunk_size=1024, compress_prealloc_chunks=2);

Добавление сжатого индекса GIN:

CREATE INDEX compressed_table_1_idx_3 on compressed_table_1 USING gin((ARRAY[id])) WITH(compresstype=pglz, compresslevel=1, compress_chunk_size=1024, compress_prealloc_chunks=2);

Добавление сжатого индекса GiST:

CREATE INDEX compressed_table_1_idx_4 on compressed_table_1 USING gist((point(id,id))) WITH(compresstype=pglz, compresslevel=1, compress_chunk_size=1024, compress_prealloc_chunks=2);

Добавление сжатого индекса SP-GiST:

CREATE INDEX compressed_table_1_idx_5 on compressed_table_1 USING spgist(c1) WITH(compresstype=pglz, compresslevel=1, compress_chunk_size=1024, compress_prealloc_chunks=2);

Примеры создания табличных пространств с параметрами сжатия и таблиц в сжатом табличном пространстве

Добавление сжатых табличных пространств с разными настройками:

CREATE TABLESPACE compressed_tablespace_1 LOCATION '/tablespaces/compressed_tablespace_1' WITH (compresstype=pglz);
CREATE TABLESPACE compressed_tablespace_2 LOCATION '/tablespaces/compressed_tablespace_2' WITH (compresstype=pglz, compress_chunk_size=512);
CREATE TABLESPACE compressed_tablespace_3 LOCATION '/tablespaces/compressed_tablespace_3' WITH (compresstype=zstd, compresslevel=0, compress_chunk_size=1024);
CREATE TABLESPACE compressed_tablespace_4 LOCATION '/tablespaces/compressed_tablespace_4' WITH (compresstype=zstd, compresslevel=1, compress_chunk_size=2048, compress_prealloc_chunks=0);

В сжатое табличное пространство можно добавить несжатую таблицу. Явно указанный параметр compresstype позволяет uncompressed_table не наследовать опции сжатия табличного пространства compressed_tablespace_1, поэтому таблица создается не в сжатом виде:

CREATE TABLE uncompressed_table(id int, c1 text) WITH (compresstype=none) TABLESPACE compressed_tablespace_1;

Таблица compressed_table_1 наследует опцию сжатия табличного пространства compressed_tablespace_1. Параметры compresslevel, compress_chunk_size и compress_prealloc_chunks применяются по умолчанию: 0, 4096, 0.

CREATE TABLE compressed_table_1(id int, c1 text) TABLESPACE compressed_tablespace_1;

Таблица compressed_table_2 не наследует опции сжатия табличного пространства compressed_tablespace_1, так как для нее явно заданы опции сжатия. У нее параметр compress_prealloc_chunks явно задан не будет, при использовании будет принят по умолчанию: 0.

CREATE TABLE compressed_table_2(id int, c1 text) WITH (compresstype=pglz, compresslevel=0, compress_chunk_size=1024) TABLESPACE compressed_tablespace_1;

Материализованное представление uncompressed_mv при явном compresstype не наследует опции сжатия табличного пространства compressed_tablespace_1, поэтому создается не в сжатом виде.

CREATE MATERIALIZED VIEW uncompressed_mv WITH (compresstype=none) TABLESPACE compressed_tablespace_1 AS SELECT id, c1 FROM uncompressed_table WHERE id <= 10;

Материализованное представление compressed_mv_1 наследует опции сжатия табличного пространства compressed_tablespace_1. Параметры compresslevel, compress_chunk_size и compress_prealloc_chunks применяются по умолчанию: 0, 4096, 0.

CREATE MATERIALIZED VIEW compressed_mv_1 TABLESPACE compressed_tablespace_1 AS SELECT id, c1 FROM uncompressed_table WHERE id <= 10;

Материализованное представление compressed_mv_2 не наследует опции сжатия табличного пространства compressed_tablespace_1, так как для него уже заданы опции сжатия. Параметр compress_prealloc_chunks явно задан не будет, при использовании будет принят по умолчанию: 0.

CREATE MATERIALIZED VIEW compressed_mv_2 WITH (compresstype=pglz, compresslevel=0, compress_chunk_size=1024) TABLESPACE compressed_tablespace_1 AS SELECT id, c1 FROM uncompressed_table WHERE id <= 10;

Пример миграции таблицы и использования сжатия

Создаем и заполняем «таблицу профилей сотрудников» на 10 миллионов записей, для начала несжатую. Добавляем колонку с безразмерным текстовым «резюме» в JSON, хранимым в двоичной форме (jsonb). «Резюме» делаем не очень большим, чтобы не выйти за границу перехода к TOAST (ниже). По jsonb строим индекс GIN.

CREATE TABLE emp (
emp_id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, -- Уникальный ID (автоинкремент)
name VARCHAR(50) NOT NULL, -- Имя
hire_date DATE DEFAULT CURRENT_DATE, -- Дата найма (по умолчанию сегодня)
cv JSONB
);
INSERT INTO emp (name, hire_date, cv)
WITH cv_text(line) AS (
VALUES
('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'),
('Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.'),
('Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.'),
('Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.')
)
SELECT
'Имя ' || i || ' Фамилия ' || i, -- Имя (Имя 1, Имя 2, ..)
CURRENT_DATE - (random() * 365 * 5)::integer, -- Дата найма за последние 5 лет
(SELECT json_build_object(
'Идентификатор', i,
'Дата рождения', CURRENT_DATE - 18*365 - (random() * 365 * 40)::integer, -- Дата рождения от 58 до 18 лет назад
'Резюме', string_agg(cv_text.line, ' ')) quote FROM cv_text WHERE random() > 0.5) cv -- Резюме
FROM generate_series(1, 10000000) AS i;
psql zip
# \t
zip=# SELECT * FROM emp LIMIT 1 \gx
emp_id | 1
name | Имя 1 Фамилия 1
hire_date | 2026-01-31
cv | {"Резюме": "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", "Дата рождения": "1976-07-30", "Идентификатор": 1}
zip=# CREATE INDEX emp_cv_idx ON emp USING gin(cv);
CREATE INDEX

Оценим размер основного слоя таблицы, индекса первичного ключа и GIN:

zip=# SELECT pg_size_pretty(pg_relation_size('emp','main')) t, pg_size_pretty(pg_relation_size('emp_pkey')) i, pg_size_pretty(pg_relation_size('emp_cv_idx')) gin;
4070 MB | 214 MB | 749 MB

Создаем сжатое табличное пространство и мигрируем таблицу в него:

# \! mkdir /tablespaces/compressed
# CREATE TABLESPACE compressed LOCATION '/tablespaces/compressed' WITH (compresstype=pglz, compress_chunk_size=1024);
CREATE TABLESPACE
# CREATE TABLE emp_new TABLESPACE compressed AS SELECT * FROM emp;
SELECT 10000000
zip=# BEGIN;
ALTER TABLE emp RENAME TO emp_old;
ALTER INDEX emp_pkey RENAME TO emp_old_pkey;
ALTER INDEX emp_cv_idx RENAME TO emp_old_cv_idx;
ALTER SEQUENCE emp_emp_id_seq RENAME TO emp_old_emp_id_seq;
ALTER TABLE emp_new RENAME TO emp;
COMMIT;
BEGIN
ALTER TABLE
ALTER TABLE
COMMIT
ALTER TABLE emp ADD PRIMARY KEY (emp_id) USING INDEX TABLESPACE compressed;
ALTER TABLE emp ALTER COLUMN emp_id ADD GENERATED BY DEFAULT AS IDENTITY;
ALTER TABLE emp ALTER COLUMN emp_id SET START WITH 10000001;
CREATE INDEX emp_cv_idx ON emp USING gin(cv) TABLESPACE compressed;

Вот что получилось:

# \t
# \d+ emp
Table "public.emp"
Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description
-----------+-----------------------+-----------+----------+----------------------------------+----------+-------------+--------------+-------------
emp_id | integer | | not null | generated by default as identity | plain | | |
name | character varying(50) | | | | extended | | |
hire_date | date | | | | plain | | |
cv | jsonb | | | | extended | | |
Indexes:
"emp_pkey" PRIMARY KEY, btree (emp_id) WITH (compresstype=pglz, compresslevel='0', compress_chunk_size='1024', compress_prealloc_chunks='0'), tablespace "compressed"
"emp_cv_idx" gin (cv) WITH (compresstype=pglz, compresslevel='0', compress_chunk_size='1024', compress_prealloc_chunks='0'), tablespace "compressed"
Tablespace: "compressed"
Access method: heap
Options: compresstype=pglz, compresslevel=0, compress_chunk_size=1024, compress_prealloc_chunks=0

Оценим изменение размера основного слоя, индекса первичного ключа и GIN:

zip=# SELECT pg_size_pretty(pg_relation_size('emp','main')) t, pg_size_pretty(pg_relation_size('emp_pkey')) i, pg_size_pretty(pg_relation_size('emp_cv_idx')) gin;
1039 MB | 112 MB | 5462 kB

Этот результат – не окончательный. Дело в том, что последние данные при сборке GIN зафиксированы в WAL и отражены в памяти, но еще не записаны на диск, потому что буферы еще не вытеснялись. Сделаем контрольную точку и повторим оценку:

zip=# CHECKPOINT;
CHECKPOINT
zip=# SELECT pg_size_pretty(pg_relation_size('emp','main')) t, pg_size_pretty(pg_relation_size('emp_pkey')) i, pg_size_pretty(pg_relation_size('emp_cv_idx')) gin;
1039 MB | 112 MB | 232 MB

Было 4070, 214 и 749 MB. Соотношение – порядка 4:1 по таблице, 2:1 по индексу первичного ключа, 3.3:1 по индексу GIN. Данные в разных объектах сжимаются по-разному, эффект сильно зависит от структуры блока.

Теперь соберем статистику:

zip=# ANALYZE emp_old;
ANALYZE
zip=# ANALYZE emp;
ANALYZE

Обнулим кэши и СУБД, и ОС перезагрузкой ОС и поищем нескольких сотрудников по известной дате рождения в JSON:

$ sudo shutdown -r now
<повторное подключение>
zip=# EXPLAIN (ANALYZE, VERBOSE, BUFFERS) SELECT * FROM emp_old WHERE cv @> '{"Дата рождения": "1979-05-03"}';
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on public.emp_old (cost=22.06..1131.28 rows=1001 width=383) (actual time=369.604..971.727 rows=676 loops=1)
Output: emp_id, name, hire_date, cv
Recheck Cond: (emp_old.cv @> '{"Дата рождения": "1979-05-03"}'::jsonb)
Heap Blocks: exact=676
Buffers: shared hit=1045 read=1209
-> Bitmap Index Scan on emp_old_cv_idx (cost=0.00..21.81 rows=1001 width=0) (actual time=368.049..368.050 rows=676 loops=1)
Index Cond: (emp_old.cv @> '{"Дата рождения": "1979-05-03"}'::jsonb)
Buffers: shared hit=1045 read=533
Query Identifier: 4771954246685019686
Planning:
Buffers: shared hit=95 read=14 dirtied=3
Planning Time: 14.748 ms
Execution Time: 972.102 ms
(13 rows)
zip=# EXPLAIN (ANALYZE, VERBOSE, BUFFERS) SELECT * FROM emp WHERE cv @> '{"Дата рождения": "1979-05-03"}';
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on public.emp (cost=22.05..1129.06 rows=999 width=383) (actual time=15.789..1328.673 rows=676 loops=1)
Output: emp_id, name, hire_date, cv
Recheck Cond: (emp.cv @> '{"Дата рождения": "1979-05-03"}'::jsonb)
Heap Blocks: exact=676
Buffers: shared hit=1045 read=1209
-> Bitmap Index Scan on emp_cv_idx (cost=0.00..21.80 rows=999 width=0) (actual time=15.281..15.281 rows=676 loops=1)
Index Cond: (emp.cv @> '{"Дата рождения": "1979-05-03"}'::jsonb)
Buffers: shared hit=1045 read=533
Query Identifier: -5512884371611139519
Planning:
Buffers: shared hit=42 read=2 dirtied=3
Planning Time: 1.619 ms
Execution Time: 1329.050 ms
(13 rows)

Видно, что смысл операций плана и количества обработанных записей/блоков не меняются, а приблизительные оценки количеств записей и стоимости операций остаются почти такими же (разница в доли процента объясняется случайным характером выборки в ANALYZE). Есть разница во времени выполнения, которая объясняется необходимостью чтения фрагментов с диска, распаковки и формирования блоков в памяти. Повторные попытки по времени выполнения будет не отличить, потому что распакованные данные окажутся в кэше.

Отличия от сжатия TOAST

АлгоритмСжатие TOASTСжатие на уровне страниц Pangolin
Какой объект сжимает?Значение атрибута (столбца) таблицы.

Каждый столбец каждой записи сжимается отдельно, алгоритм и настройки могут различаться между ними
Один блок таблицы, секции или индекса.

Сжатие на уровне блоков делается одним алгоритмом с одними настройками на все блоки объекта. Уточнения для записей и столбцов нет, настройки объекта после создания не изменить.

Блоки сжимаются по отдельности: сжатые данные каждого блока не зависят от данных соседних блоков
Включен по умолчанию?ДаНет
Как включить?Установить режим хранения столбца MAIN или EXTENDED (обычно установлен по умолчанию¹)

ALTER TABLE таблица ALTER COLUMN поле SET STORAGE MAIN;
Установить параметр сервера enable_page_compression и перезапустить сервер
Как выключить?Установить режим хранения столбца PLAIN или EXTERNAL.

Новые данные перестанут сжиматься, а существующие останутся сжатыми. Их распаковки можно достичь подобным предложением:

UPDATE таблица SET large_column = large_column;

Или ждать постепенной распаковки во время работы
Обязательно из сжатых объектов перенести данные и удалить их. Например, использовать:

CREATE TABLE uncompressed_table AS SELECT * FROM compressed_table;

Выключить параметр enable_page_compression и перезапустить сервер
Как добавить сжатый объект?Просто добавлять данные. По достижении порога они будут сжиматься прямо в таблице (MAIN) или выноситься в сжатом виде в TOAST-таблицуДобавить объект с параметром compresstype в разделе WITH () предложения CREATE объект. Или добавить объект без compresstype (а для точной настройки - с compresstype не none) в сжатое ТП
Как увидеть, что объект сжат?Использовать для столбца функцию

pg_column_compression(large_column)
Настройки сжатия видны в pg_class.reloptions (у таблицы/секции/индекса), pg_tablespace.spcoptions (у tablespace).

Сжатый объект имеет явный параметр хранения compresstype, не равный none
Как оценить степень сжатия до включения?Нет способаИспользовать функцию relation_estimate_compression_ratio(), которая есть в расширении pageinspect
Как увидеть степень сжатия объекта?Отнести pg_column_size(large_column) столбца к его octet_length(large_column)Отнести pg_relation_size() таблицы/индекса к ее/его (pg_class.relpages * 8192).

Если большая часть данных только что была изменена, то сначала придется выполнить CHECKPOINT, чтобы затронутые страницы из буферного кэша записались на диск в сжатом виде
Где сохраняются сжатые данные?К основной таблице добавляется служебная в схеме pg_toast. Значение атрибута делится на фрагменты так, чтобы на странице уместилось до EXTERN_TUPLES_PER_PAGE фрагментов (задается в коде, обычно 4). Для этого при компиляции кода сервера вычисляется максимальный размер фрагмента EXTERN_TUPLE_MAX_SIZE (для архитектуры x86-64 при 4 фрагментах на страницу это 1996 байт). Фрагменты значения располагаются в служебной таблице с учетом идентификатора значения и порядка следования, найти их помогает индекс по этим двум полям.

В основной таблице запись хранит вместо значения указатель с идентификатором значения и служебными полями
При объявлении сжатого объекта все его данные из файлов слоя main, именованных номером файлового узла pg_class.relfilenode, переносятся в файлы _pcd/_pca.

Файлы слоя main создаются для совместимости, но они всегда нулевого размера. Если блок не поддается сжатию, то он записывается в файл сжатых данных в несжатом виде с известным признаком.

Если объем объекта превышает 1 ГБ, к main добавляется файл relfilenode.1, размер которого всегда будет 0 байт, а данные оказываются в файлах relfilenode.1_pca/relfilenode.1_pcd. И так далее
Параметры уровня сервераdefault_toast_compression = [pglz, lz4]

Алгоритм сжатия, по умолчанию pglz. Перезапуска не требует, применяется по мере добавления данных в записи
enable_page_compression: boolean

Вкл/выкл функциональности, требует перезапуска
Параметры уровня таблицы/ALTER TABLE .. SET ( )Параметр хранения toast_tuple_target: минимальный размер записи в байтах, от которого начинает работать механизм. И (одновременно²) целевой размер записи, до которого TOAST будет пытаться ее сократить. От 128 до 8160 байт, по умолчанию 2040. Применяется только при добавлении / изменении записи, нет переупаковки данных по смене параметраcompresstype - алгоритм сжатия: none, pglz, lz4, zstd - обязателен.

compresslevel - уровень сжатия (только для zstd): 0 .. 19, где 0 означает уровень сжатия по умолчанию из библиотеки zstd, сейчас там уровень 3. По умолчанию 0.

compress_chunk_size - размер фрагмента в байтах, 512, 1024, 2048 или 4096. По умолчанию 4096.

compress_prealloc_chunks - кол-во предварительно выделяемых фрагментов на один блок данных, 0 .. 8192 / compress_chunk_size - 1. По умолчанию 0
Параметры уровня столбца/ALTER TABLE .. ALTER COLUMN .. SETSTORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN | DEFAULT } - режим хранения столбца: сжимать/не сжимать, хранить внутри или вне таблицы.

SET COMPRESSION алгоритм - алгоритм сжатия для столбца: pglz, lz4 или default (использовать default_toast_compression сервера)
Нет: алгоритм не различает столбцы
Очередность между сжатием и защитным преобразованием данных в TDETOAST сначала сжимается, затем преобразуется алгоритмом защиты. Теоретически, при известных исходных данных можно представить себе атаку, подобную BREACH/CRIMEВыполняется сначала TDE и только после него – сжатие. Вектор для подобий BREACH / CRIME закрыт

¹ Чтобы использовать режим хранения столбца EXTENDED, тип данных должен в принципе поддерживать вынесение в TOAST (в оригинале «TOAST-able»). Если не поддерживает – режим будет только PLAIN.

² Алгоритм режима EXTENDED – гибкий. Если размер значения столбца больше порогового, то сервер сначала попытается сжать данные до целевого размера. Если цели удалось достичь, значение полностью останется в таблице. Если нет, то сжатые данные полностью отправятся в TOAST-таблицу, а в основной таблице останется только указатель на них (несколько байт). Один и тот же параметр сервера, toast_tuple_target, имеет двойной смысл: это и порог размера значения, от которого включается TOAST, и целевой размер, до которого вначале алгоритм пробует сжать данные.

Сравнение производительности pagecompress с разными параметрами

Методика тестирования

Сценарий нагрузки TPCC. Количество выполняемых транзакций за единицу времени = 1000.

Количество активных клиентов, выполняющие запросы = 20.

Каждый тест в течение 30 минут.

Коэффициент масштабирования (scalefactor) = 1250.

Конфигурация тестового стенда

НазначениеВерсия ОСВерсия ядраCPURAMDiskPRODUCT_BUILD_INFO
PostgreSQL, PGBouncerSberLinux 8.10 (Dykhtau)4.18.0-477.27.1.sl8_8.1.x86_64864 Gi500Gbuild 65 (08:31:26 23.03.2025) commit dce09615a4445876f5073f2843175491e3eeb153

Конфигурация тестового стенда c zstd 1.5

НазначениеВерсия ОСВерсия ядраCPURAMDiskPRODUCT_BUILD_INFO
PostgreSQL, PGBouncerSberLinux 8.10 (Dykhtau)4.18.0-477.27.1.sl8_8.1.x86_64864 Gi500Gbuild 134901 (13:13:40 28.03.2025) commit 24e3643a4e9bf6ff642ec03dfc06f3bec6b6d0d0

Ограничения НТ

  1. Все тесты выполнялись напрямую на порт 5433.
  2. max_wal_size = 27 GB.
  3. shared_buffers = 8 GB.
  4. session_tracing_enable = 'off'.

Результаты

Параметры сжатияБез сжатияcompresslevel=0, compress_chunk_size=512 Нагрузка через pgbouncer zstdcompresslevel=0, compress_chunk_size=512 zstdcompresslevel=0, compress_chunk_size=512 zstd 1.5compresslevel=19, compress_chunk_size=512 zstdcompresslevel=0, compress_chunk_size=4096 zstdcompresslevel=0, compress_chunk_size=512 lz4
Размер tablespace126 G61 G60 G59 G94 G91 G
CPU, %54575154595154
Disk (запись на диск), Mb19.914.713.414.817.613.316.7
RAM, Gb1.451.491.41.351.51.381.53
TPS1000100010001000100010001000
Latency, ms10.412.210.310.51110.310.5
Ошибки обработки транзакций, %000.1600.2370.1710

Выводы

Рекомендуемые параметры сжатия данных методом zstd: compresslevel=0, compress_chunk_size=512.

Ссылки по теме

Поддержка сжатия данных при записи на диск.

Commit Make wal_compression PGC_SUSET rather than PGC_USERSET – о теоретическом векторе атаки на SSL для wal_compression.