Сжатие и влияние на производительность

Каждые compress_threshold строк (по умолчанию: 1200) образуют Range. В пределах Range данные конкретного столбца (содержащие compress_threshold строк) называются Stripe. Если данные столбца внутри Stripe особенно велики, Stripe разбивается на несколько фрагментов по 1 МБ. При чтении система не обязательно выбирает весь объем compress_threshold за один раз.

Параметр compress_threshold также влияет на коэффициент сжатия:

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

Влияние compress_threshold на производительность чтения

ID compress_threshold=1200 3600 10000 50000
1 8.8032 7.7846 6.6471 6.6824
2 0.981 0.997 2.2284 2.2322
3 3.3512 3.3143 3.3184 3.3177
4 7.7059 7.7326 6.6034 5.5994
5 5.5732 5.5042 5.5939 6.633
6 0.478 0.38 0.545 0.545
7 1.1908 1.1917 2.2172 2.2222
8 3.3602 3.3129 3.3018 3.3227
9 8.8429 7.74 7.7751 7.7795
10 3.3835 4.4239 5.5496 5.5499
11 0.845 0.743 2.2307 2.2299
12 1.1998 1.1887 2.2164 2.2138
13 6.6514 6.6351 6.6784 6.6424
14 0.837 0.776 1.1364 1.1275
15 1.1436 1.1359 1.183 1.1783
16 1.1768 1.1787 2.2673 2.2757
17 9.9419 8.8705 8.8922 8.8771
18 11.11337 11.11522 10.10384 10.10647
19 4.4792 3.3942 4.4918 4.467
20 2.2668 2.2389 3.3475 3.3346
21 11.11391 10.10633 11.1119 10.10742
22 3.3009 2.2864 3.372 3.3377
Сумма 97.73958 92.05015 100.81134 99.78189

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

Сценарий теста Метрика compress_threshold=1200 3600 10000 50000
Производительность записи Секционированная таблица (строк/с) 852,334 919,031 1,055,371 1,057,424
Производительность записи Несекционированная таблица (строк/с) 991,463 1,033,751 1,054,292 1,076,714
Производительность запросов time_bucket=1ч (диапазон 1 день) 357 мс 342 мс 333 мс 324 мс
Производительность запросов time_bucket=1д (диапазон 1 месяц) 7,384 мс 6,312 мс 6,008 мс 6,025 мс
Производительность запросов time_bucket=30д (диапазон 1 год) 94,278 мс 75,101 мс 68,184 мс 69,006 мс
Производительность запросов Точечный поиск 19.416 мс 20.043 мс 23.692 мс 37.844 мс

Сжатие индексов

Информацию об архитектуре сжатия индексов см. в разделе mars3btree.

CREATE INDEX idx_name ON table_name
USING mars3btree (column_list)
WITH (
    compresstype = 'lz4',           -- Алгоритм сжатия
    compresslevel = 1,              -- Уровень сжатия
    compressctid = true,            -- Сжимать столбец CTID?
    encodechain = '',               -- Цепочка кодирования
    minmax = true                   -- Включить оптимизацию min/max
);

Параметры:

  1. compresstype: Поддерживаемые алгоритмы: lz4, zstd и mxcustom.
    • По умолчанию: lz4. Обеспечивает быстрое сжатие/распаковку со средним коэффициентом сжатия.
    • zstd: Обеспечивает более высокий коэффициент сжатия, но работает немного медленнее.
    • mxcustom: Должен использоваться в сочетании с encodechain.
  2. compresslevel: Поддерживает значения от 1 до 9 (по умолчанию: 1).
    • Для рабочих нагрузок с интенсивным чтением используйте 1–3, чтобы приоритизировать скорость распаковки.
    • Для сценариев, чувствительных к объему хранилища, используйте 6–9, чтобы приоритизировать коэффициент сжатия.
  3. compressctid: Определяет, будет ли сжиматься столбец CTID. По умолчанию true. Рекомендуется оставить этот параметр включенным, так как столбцы CTID обычно обеспечивают высокий коэффициент сжатия.

Кейс клиента: В конкретном сценарии клиента включение сжатия индексов дало следующие результаты:

  • Кластер TOB: Минимальное влияние на использование процессора и памяти узла при экономии 24% пространства раздела YMatrix.
  • Кластер TOC: Значительные результаты. Несмотря на небольшое увеличение потребления процессора и памяти, было сэкономлено 63% пространства раздела YMatrix.
  • Пропускная способность (50 транспортных средств, данные GPS за 1 день): Пропускная способность сервиса со сжатием индекса была в 3.3 раза выше, чем без него.
  • Пропускная способность (50 транспортных средств, данные GPS за 3 дня): Пропускная способность сервиса со сжатием индекса была в 1.1 раза выше, чем без него.

Влияние ключей сортировки на сжатие

Как упоминалось ранее, сжатие (будь то ZSTD, LZ4 или схемы кодирования, такие как RLE, Dictionary и Bitpacking) основывается на ключевом принципе: чем регулярнее данные внутри блока или Stripe, тем лучше сжатие.

  • LZ4/ZSTD: Зависят от повторяющихся подстрок и паттернов. Сортировка данных гарантирует, что комбинации полей внутри блока будут более схожими, особенно в широких таблицах.
  • Эффект кластеризации: Когда данные от схожих сущностей или временных периодов кластеризуются в одном Stripe:
    • Диапазоны значений внутри блоков сходятся.
    • Увеличивается повторяемость и длина серий (run-lengths).
    • Уменьшаются размеры словарей.
    • Сокращается битовая ширина для дельта-кодирования/bitpacking.
    • Следовательно, повышается эффективность как кодирования, так и общего сжатия.

Пример настройки:

-- Хороший ключ сортировки: device_id, ts
adw=# CREATE TABLE t_sort_good (
  device_id   int NOT NULL,
  ts          timestamptz NOT NULL,
  site_id     int NOT NULL,
  status      smallint NOT NULL,
  v1          double precision NOT NULL,
  v2          double precision NOT NULL,
  attrs       jsonb NOT NULL
)
USING MARS3
WITH (compresstype=zstd, compresslevel=3, mars3options='prefer_load_mode=bulk')
ORDER BY (device_id, ts);
NOTICE:  Table doesn't have 'DISTRIBUTED BY' clause -- Using column named 'device_id' as the Greenplum Database data distribution key for this table.
HINT:  The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
CREATE TABLE

-- Плохой ключ сортировки: ts, device_id
adw=# CREATE TABLE t_sort_bad (
  device_id   int NOT NULL,
  ts          timestamptz NOT NULL,
  site_id     int NOT NULL,
  status      smallint NOT NULL,
  v1          double precision NOT NULL,
  v2          double precision NOT NULL,
  attrs       jsonb NOT NULL
)
USING MARS3
WITH (compresstype=zstd, compresslevel=3, mars3options='prefer_load_mode=bulk')
ORDER BY (ts, device_id);
NOTICE:  Table doesn't have 'DISTRIBUTED BY' clause -- Using column named 'device_id' as the Greenplum Database data distribution key for this table.
HINT:  The 'DISTRIBUTED BY' clause determines the distribution of data. Make sure column(s) chosen are the optimal data distribution key to minimize skew.
CREATE TABLE

Генерация и вставка данных:

DROP TABLE IF EXISTS t_src;
CREATE UNLOGGED TABLE t_src AS
WITH
  dev AS (
    SELECT d AS device_id,
           (d % 200) AS site_id,
           (1000 + (d % 500))::double precision AS base
    FROM generate_series(1, 10000) AS d
  ),
  pts AS (
    SELECT device_id, site_id, base,
           g AS seq,
           (timestamp '2026-01-01' + (g || ' seconds')::interval) AS ts
    FROM dev
    CROSS JOIN generate_series(1, 1000) AS g
  )
SELECT
  device_id,
  ts,
  site_id,
  -- status: Изменяется сегментом каждые 300 секунд, образуя длинные серии
  ((seq / 300) % 4)::smallint AS status,
  -- v1/v2: Базовое значение устройства + небольшие флуктуации
  (base + (seq % 10) * 0.1)::double precision AS v1,
  (base * 0.1 + (seq % 20) * 0.01)::double precision AS v2,
  -- attrs: Высоко схожие паттерны ключей, значения варьируются в зависимости от устройства/времени
  jsonb_build_object(
    'fw',  '1.2.' || (device_id % 10),
    'model', 'm' || (device_id % 50),
    'tag',  'S' || site_id,
    'k',    seq % 5
  ) AS attrs
FROM pts;

INSERT INTO t_sort_good SELECT * FROM t_src;
INSERT INTO t_sort_bad  SELECT * FROM t_src;

Проверка: Для обеспечения точности данных выполните VACUUM FULL, а затем несколько раз VACUUM после вставки.

-- Проверка статистики t_sort_good
adw=# SELECT segid, level, total_nruns, visible_nruns, invisible_nruns, level_size 
FROM matrixts_internal.mars3_level_stats('t_sort_good') 
WHERE level_size <> '0 bytes';
 segid | level | total_nruns | visible_nruns | invisible_nruns | level_size
-------+-------+-------------+---------------+-----------------+------------
     0 |     1 |           1 |             1 |               0 | 8839 kB
     1 |     1 |           1 |             1 |               0 | 8933 kB
     3 |     1 |           1 |             1 |               0 | 8552 kB
     2 |     1 |           1 |             1 |               0 | 8728 kB
(4 строки)

-- Проверка статистики t_sort_bad
adw=# SELECT segid, level, total_nruns, visible_nruns, invisible_nruns, level_size 
FROM matrixts_internal.mars3_level_stats('t_sort_bad') 
WHERE level_size <> '0 bytes';
 segid | level | total_nruns | visible_nruns | invisible_nruns | level_size
-------+-------+-------------+---------------+-----------------+------------
     0 |     2 |           1 |             1 |               0 | 26 MB
     1 |     2 |           1 |             1 |               0 | 26 MB
     3 |     2 |           1 |             1 |               0 | 25 MB
     2 |     2 |           1 |             1 |               0 | 25 MB
(4 строки)

-- Проверка размеров таблиц
adw=# \dt+ t_sort_bad
                           List of relations
 Schema |    Name    | Type  |  Owner  | Storage |  Size  | Description
--------+------------+-------+---------+---------+--------+-------------
 public | t_sort_bad | table | mxadmin | mars3   | 103 MB |
(1 строка)

adw=# \dt+ t_sort_good
                           List of relations
 Schema |    Name     | Type  |  Owner  | Storage | Size  | Description
--------+-------------+-------+---------+---------+-------+-------------
 public | t_sort_good | table | mxadmin | mars3   | 35 MB |
(1 строка)

-- Пример данных из t_sort_good (Отсортировано по device_id, затем ts)
adw=# SELECT * FROM t_sort_good LIMIT 10;
 device_id |           ts           | site_id | status |   v1   |         v2         |                        attrs
-----------+------------------------+---------+--------+--------+--------------------+-----------------------------------------------------
         3 | 2026-01-01 00:00:01+08 |       3 |      0 | 1003.1 | 100.31000000000002 | {"k": 1, "fw": "1.2.3", "tag": "S3", "model": "m3"}
         3 | 2026-01-01 00:00:02+08 |       3 |      0 | 1003.2 | 100.32000000000001 | {"k": 2, "fw": "1.2.3", "tag": "S3", "model": "m3"}
         3 | 2026-01-01 00:00:03+08 |       3 |      0 | 1003.3 | 100.33000000000001 | {"k": 3, "fw": "1.2.3", "tag": "S3", "model": "m3"}
         3 | 2026-01-01 00:00:04+08 |       3 |      0 | 1003.4 | 100.34000000000002 | {"k": 4, "fw": "1.2.3", "tag": "S3", "model": "m3"}
         3 | 2026-01-01 00:00:05+08 |       3 |      0 | 1003.5 | 100.35000000000001 | {"k": 0, "fw": "1.2.3", "tag": "S3", "model": "m3"}
         3 | 2026-01-01 00:00:06+08 |       3 |      0 | 1003.6 | 100.36000000000001 | {"k": 1, "fw": "1.2.3", "tag": "S3", "model": "m3"}
         3 | 2026-01-01 00:00:07+08 |       3 |      0 | 1003.7 |             100.37 | {"k": 2, "fw": "1.2.3", "tag": "S3", "model": "m3"}
         3 | 2026-01-01 00:00:08+08 |       3 |      0 | 1003.8 | 100.38000000000001 | {"k": 3, "fw": "1.2.3", "tag": "S3", "model": "m3"}
         3 | 2026-01-01 00:00:09+08 |       3 |      0 | 1003.9 | 100.39000000000001 | {"k": 4, "fw": "1.2.3", "tag": "S3", "model": "m3"}
         3 | 2026-01-01 00:00:10+08 |       3 |      0 |   1003 |              100.4 | {"k": 0, "fw": "1.2.3", "tag": "S3", "model": "m3"}
(10 строк)

-- Пример данных из t_sort_bad (Отсортировано по ts, затем device_id - смешанные устройства)
adw=# SELECT * FROM t_sort_bad LIMIT 10;
 device_id |           ts           | site_id | status |   v1   |         v2         |                         attrs
-----------+------------------------+---------+--------+--------+--------------------+-------------------------------------------------------
         1 | 2026-01-01 00:00:01+08 |       1 |      0 | 1001.1 | 100.11000000000001 | {"k": 1, "fw": "1.2.1", "tag": "S1", "model": "m1"}
        12 | 2026-01-01 00:00:01+08 |      12 |      0 | 1012.1 | 101.21000000000001 | {"k": 1, "fw": "1.2.2", "tag": "S12", "model": "m12"}
        15 | 2026-01-01 00:00:01+08 |      15 |      0 | 1015.1 |             101.51 | {"k": 1, "fw": "1.2.5", "tag": "S15", "model": "m15"}
        20 | 2026-01-01 00:00:01+08 |      20 |      0 | 1020.1 |             102.01 | {"k": 1, "fw": "1.2.0", "tag": "S20", "model": "m20"}
        23 | 2026-01-01 00:00:01+08 |      23 |      0 | 1023.1 | 102.31000000000002 | {"k": 1, "fw": "1.2.3", "tag": "S23", "model": "m23"}
        35 | 2026-01-01 00:00:01+08 |      35 |      0 | 1035.1 |             103.51 | {"k": 1, "fw": "1.2.5", "tag": "S35", "model": "m35"}
        38 | 2026-01-01 00:00:01+08 |      38 |      0 | 1038.1 | 103.81000000000002 | {"k": 1, "fw": "1.2.8", "tag": "S38", "model": "m38"}
        40 | 2026-01-01 00:00:01+08 |      40 |      0 | 1040.1 |             104.01 | {"k": 1, "fw": "1.2.0", "tag": "S40", "model": "m40"}
        44 | 2026-01-01 00:00:01+08 |      44 |      0 | 1044.1 | 104.41000000000001 | {"k": 1, "fw": "1.2.4", "tag": "S44", "model": "m44"}
        47 | 2026-01-01 00:00:01+08 |      47 |      0 | 1047.1 | 104.71000000000001 | {"k": 1, "fw": "1.2.7", "tag": "S47", "model": "m47"}
(10 строк)

Резюме:

  • t_sort_good: Группирует схожие данные вместе. Данные, записанные на диск в одном пакете, являются более регулярными и повторяющимися, что облегчает эффективную работу компрессора.
  • t_sort_bad: Смешивает совершенно разные данные вместе. Данные, записанные в одном пакете, являются хаотичными и случайными, что затрудняет сжатие.

Результаты сравнения (одинаковый объем данных, разные ключи сортировки):

  • Общий размер t_sort_good: 35 МБ
  • Общий размер t_sort_bad: 103 МБ (примерно в 3 раза больше)

Статистика Run/Level: Хотя в обоих случаях каждый сегмент содержит только один Run:

  • Run таблицы t_sort_good находятся на Level 1, примерно 8.5–8.9 МБ на сегмент.
  • Run таблицы t_sort_bad находятся на Level 2, примерно 25–26 МБ на сегмент.

Анализ:

  1. t_sort_good (Ключ сортировки: device_id, ts): Кластеризация по схожести → Благоприятно для сжатия Сортировка по (device_id, ts) гарантирует, что каждый Stripe/блок содержит «непрерывный временной ряд для одного устройства». Это приводит к:

    • Длинным повторам: Столбцы, такие как device_id, site_id и status, демонстрируют длинные серии идентичных значений, что идеально подходит для кодирования RLE и Dictionary.
    • Малым дельтам: Столбцы, такие как ts, v1 и v2, медленно изменяются во времени в пределах одного устройства, resulting в малых дельтах, идеальных для delta/bitpacking.
    • Регулярности паттернов: Структура ключей в attrs (JSONB) altamente согласована в пределах устройства, что выгодно для ZSTD/LZ4.
    • Результат: Значительно меньший физический размер при одинаковом количестве строк.
  2. t_sort_bad (Ключ сортировки: ts, device_id): Смешивание устройств → Неблагоприятно для сжатия Сортировка по (ts, device_id) смешивает большое количество устройств в каждый момент времени. В пределах одного блока:

    • Высокая кардинальность: device_id меняется почти в каждой строке, увеличивая размер словаря и сокращая длину серий RLE почти до 1.
    • Разрозненные паттерны: site_id и status нарушены, что снижает эффективность длин серий.
    • Нерегулярный JSON: Комбинации паттернов в attrs становятся хаотичными, уменьшая количество повторяющихся подстрок.
    • Итоговый эффект: Несмотря на то, что ts является последовательным, случайность других столбцов нивелирует большинство преимуществ сжатия.
    • Результат: Значительно худший коэффициент сжатия; ожидается трехкратное увеличение физического размера.

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

Вернуться к предыдущему разделу: Принципы движка хранения