Ключи сортировки и локальность данных

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

  • Правильно выбранные ключи сортировки создают сильную локальность данных внутри Run и на более высоких уровнях иерархии. Условия фильтрации запросов с большей вероятностью будут соответствовать непрерывным диапазонам, делая операции пропуска (skip-scan) более эффективными.
  • Неправильно выбранные ключи сортировки приводят к разрозненному распределению данных. Условия фильтрации не могут сузить диапазон сканирования, заставляя систему вести себя так, как будто выполняется полное сканирование таблицы, несмотря на наличие индексов или метаданных.

Проблемы, решаемые ключами сортировки

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

  1. Повышение эффективности фильтрации и диапазонных запросов: Когда предложения WHERE тесно согласуются с ключом сортировки (например, диапазоны времени, диапазоны ID устройств), данные демонстрируют более сильную кластеризацию на уровне хранения. Запросы могут раньше и точнее пропускать нерелевантные блоки данных, снижая потребление ресурсов ввода-вывода и процессорного времени.
  2. Повышение надежности и возможностей пропуска при сканировании статистики: Эффективность метаданных на уровне блоков (таких как минимальные/максимальные значения и BRIN-индексы) зависит от распределения данных. Если один блок содержит широкий диапазон значений ключа или разрозненное распределение, диапазон min/max становится слишком широким, вынуждая выполнять консервативное сканирование (чтение большего количества блоков). Хорошо спроектированный ключ сортировки обеспечивает концентрацию диапазонов значений в каждом блоке, делая метаданные более дискриминирующими.
  3. Влияние на фоновое слияние и долгосрочные операционные расходы: Ключи сортировки влияют на структуру и эффективность слияния Run. Высокоупорядоченные и кластеризованные данные приводят к формированию регулярных и компактных Run после слияния, оптимизируя использование пространства и пути чтения. Напротив, если ключ сортировки вызывает внутреннюю дисперсию данных, слияние не может обеспечить хорошую локальность, что ведет к более высоким долгосрочным затратам на управление для поддержания производительности.
  4. Влияние на сжатие: Алгоритмы сжатия (будь то ZSTD, LZ4 или схемы кодирования, такие как RLE, Dictionary и Bitpacking) полагаются на регулярность данных внутри блока или стрипа. Когда данные от схожих сущностей или временных периодов кластеризуются в одном стрипе, диапазоны значений сходятся, повторяемость увеличивается, размеры словарей уменьшаются, а битовые ширины для дельта-кодирования/bitpacking сокращаются. Это повышает эффективность как кодирования, так и общего сжатия. Для подробной проверки см. раздел Влияние ключей сортировки на сжатие.
  5. Влияние на производительность записи: См. раздел Влияние ключей сортировки на производительность записи.

Принципы выбора

Организуйте данные, используя измерения запросов, которые являются наиболее распространенными и обеспечивают наибольший эффект фильтрации. На практике выбор ключа сортировки обычно сочетает два измерения:

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

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

  • Правило 1: Разместите столбец, который чаще всего встречается в предложениях WHERE, как самый левый префикс ключа сортировки.
  • Правило 2: Разместите столбцы с высокой кардинальностью и высокой селективностью в начале ключа сортировки.
  • Правило 3: При использовании BRIN-индексов позиция столбца в ключе сортировки коррелирует с его важностью. Более ранние столбцы оказывают большее влияние на способность индекса пропускать нерелевантные данные.

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

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

Рассмотрим реальный кейс клиента в сценарии временных рядов со следующим запросом:

SELECT time_bucket_gapfill ('5 min', time) AS bucket_time,
    locf (LAST (value, time)) AS last_value,
    locf (LAST (quality, time)) AS last_quality,
    locf (LAST (flags, time)) AS last_flags
FROM
    xxx
WHERE
    id = '116812373032966284'
    AND type = 'ANA'
    AND time >= '2025-11-16 00:00:00.000'
    AND time <= '2025-11-16 23:59:59.000'
GROUP BY
    bucket_time;

Результаты показывают, что различные ключи сортировки значительно влияют на производительность сканирования по индексу, функционируя аналогично составным индексам.

Image

Когда поле времени стоит первым, кортежи с одинаковым ID разбросаны по пространству хранения во времени. Следовательно, сканирование по индексу должно выполнять множество случайных чтений для получения соответствующих блоков данных. Однако, когда ID стоит первым, все кортежи, принадлежащие одному и тому же ID, хранятся в соседних locations. Это резко снижает случайный ввод-вывод, так как сканированию по индексу нужно прочитать лишь несколько смежных блоков.

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

Данные сортируются во время преобразования из Rowstore в Columnstore. Сам Rowstore не отсортирован; однако, если в Rowstore существует B-Tree индекс, этот индекс остается упорядоченным. Указание ключа сортировки влечет следующие накладные расходы:

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

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

  • Ключ сортировки соответствует шаблону поступления данных: Формирует более регулярные Run, снижает количество последующих перезаписей и обеспечивает стабильную долгосрочную производительность записи.
  • Ключ сортировки не соответствует шаблону данных: Увеличивает потребление фоновых ресурсов (более интенсивное слияние), что конкурирует за ресурсы ввода-вывода и процессора, в конечном итоге замедляя фоновую запись.
CREATE TABLE t_w0_nosort (
  id      bigint      NOT NULL,
  k1      bigint      NOT NULL,     -- Высокая кардинальность
  k2      smallint    NOT NULL,     -- Низкая кардинальность
  k3      bigint      NOT NULL,     -- Монотонный столбец (имитация инкремента через bigint)
  v1      double precision NOT NULL,
  v2      double precision NOT NULL,
  payload text        NOT NULL      -- Контролирует объем записи: рекомендуется 256Б/1024Б
) USING mars3;

CREATE TABLE t_w1_1key (LIKE t_w0_nosort INCLUDING ALL)
USING mars3
ORDER BY (k3);

CREATE TABLE t_w3a_3key (LIKE t_w0_nosort INCLUDING ALL)
USING mars3
ORDER BY (k1, k3, k2);

CREATE TABLE t_w3b_3key (LIKE t_w0_nosort INCLUDING ALL)
USING mars3
ORDER BY (k3, k1, k2);

Создание промежуточного набора данных:

DROP TABLE IF EXISTS t_src_s;
CREATE TABLE t_src_s USING MARS3 AS
SELECT
  g AS id,
  (hashint8(g)::bigint) AS k1,
  (g % 64)::smallint AS k2,
  g AS k3,
  (g % 1000) * 0.01 AS v1,
  (g % 10000) * 0.001 AS v2,
  repeat('x', 256) AS payload
FROM generate_series(1, 200000000) g;

TRUNCATE t_w0_nosort;
\timing on
INSERT INTO t_w0_nosort SELECT * FROM t_src_s;
\timing off

TRUNCATE t_w1_1key;
\timing on
INSERT INTO t_w1_1key SELECT * FROM t_src_s;
\timing off

TRUNCATE t_w3a_3key;
\timing on
INSERT INTO t_w3a_3key SELECT * FROM t_src_s;
\timing off

TRUNCATE t_w3b_3key;
\timing on
INSERT INTO t_w3b_3key SELECT * FROM t_src_s;
\timing off

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

adw=# TRUNCATE t_w0_nosort;
TRUNCATE TABLE
adw=# \timing on
Timing is on.
adw=# INSERT INTO t_w0_nosort SELECT * FROM t_src_s;
INSERT 0 200000000
Time: 76859.371 ms (01:16.859)
adw=# \timing off
Timing is off.   

adw=# TRUNCATE t_w1_1key;
TRUNCATE TABLE
adw=# \timing on
Timing is on.
adw=# INSERT INTO t_w1_1key SELECT * FROM t_src_s;
INSERT 0 200000000
Time: 82864.008 ms (01:22.864)
adw=# \timing off
Timing is off. 

adw=# TRUNCATE t_w3a_3key;
TRUNCATE TABLE
adw=# \timing on
Timing is on.
adw=# INSERT INTO t_w3a_3key SELECT * FROM t_src_s;
INSERT 0 200000000
Time: 106929.500 ms (01:46.930)
adw=# \timing off
Timing is off.  

adw=# TRUNCATE t_w3b_3key;
TRUNCATE TABLE
adw=# \timing on
Timing is on.
adw=# INSERT INTO t_w3b_3key SELECT * FROM t_src_s;
INSERT 0 200000000
Time: 83456.346 ms (01:23.456)
adw=# \timing off 

Время записи:

  • t_w0_nosort (без сортировки): 76.859 с
  • t_w1_1key (ORDER BY k3): 82.864 с
  • t_w3a_3key (ORDER BY k1,k3,k2): 106.930 с
  • t_w3b_3key (ORDER BY k3,k1,k2): 83.456 с

Пропускная способность:

  • t_w0_nosort: 200,000,000 / 76.859 ≈ 2.60 млн строк/с
  • t_w1_1key: 200,000,000 / 82.864 ≈ 2.41 млн строк/с
  • t_w3a_3key: 200,000,000 / 106.930 ≈ 1.87 млн строк/с
  • t_w3b_3key: 200,000,000 / 83.456 ≈ 2.40 млн строк/с

Накладные расходы относительно отсутствия сортировки:

  • Сортировка по одному столбцу (k3): ~7.8% медленнее
  • Сортировка по трем столбцам с k1 первым (k1,k3,k2): ~39% медленнее
  • Сортировка по трем столбцам с k3 первым (k3,k1,k2): ~8.6% медленнее (сопоставимо с сортировкой по одному столбцу)

Размещение монотонного столбца k3 в качестве первого ключа сортировки снижает стоимость записи при многоколоночной сортировке почти до уровня одноколоночной сортировки. Размещение случайного столбца с высокой кардинальностью k1 первым значительно увеличивает затраты на запись.

  1. Почему «Без сортировки» быстрее всего? Избегание сортировки, сравнений и поддержки упорядоченности минимизирует накладные расходы на фоновую запись, устанавливая базовую пропускную способность (2.60 млн строк/с).

  2. Почему ORDER BY (k3) лишь немного медленнее? Входной поток уже является инкрементальным по k3. Поскольку ключ сортировки совпадает с порядком ввода, система в основном выполняет последовательную запись с минимальными дополнительными накладными расходами на поддержку метаданных, что приводит к замедлению всего на ~8%.

  3. Почему ORDER BY (k1,k3,k2) значительно медленнее? Сортировка приоритизирует k1, который является столбцом с высокой кардинальностью и близким к случайному распределением. Это рассеивает весь поток записи по пространству ключей:

    • Это предотвращает использование быстрых путей для последовательного добавления или локально упорядоченных данных.
    • Значительно увеличивается количество сравнений (многоколоночные сравнения, причем первый столбец почти всегда требует оценки).
    • Запускаются более тяжелые задачи организации данных (буферизация, слияние, поддержка внутренней структуры). Таким образом, падение до 1.87 млн строк/с (~39% медленнее) ожидаемо.
  4. Почему ORDER BY (k3,k1,k2) сопоставимо по производительности с сортировкой по одному столбцу? Первый ключ сортировки k3 совпадает со входным потоком, позволяя системе максимально использовать естественный порядок:

    • Большинство записей являются последовательными относительно k3.
    • k1 и k2 участвуют в более глубоких сравнениях только тогда, когда значения k3 идентичны или очень близки, что значительно снижает давление сравнений и пересортировки. Следовательно, его производительность почти идентична ORDER BY (k3) (2.40 млн против 2.41 млн строк/с).
      adw=# \dt+
                              List of relations
      Schema |    Name     | Type  |  Owner  | Storage |  Size   | Description
      --------+-------------+-------+---------+---------+---------+-------------
      public | t_default   | table | mxadmin | mars3   | 160 kB  |
      public | t_sort_bad  | table | mxadmin | mars3   | 103 MB  |
      public | t_sort_good | table | mxadmin | mars3   | 35 MB   |
      public | t_src_s     | table | mxadmin | mars3   | 3040 MB |
      public | t_w0_nosort | table | mxadmin | mars3   | 2769 MB |
      public | t_w1_1key   | table | mxadmin | mars3   | 2764 MB |
      public | t_w3a_3key  | table | mxadmin | mars3   | 3453 MB |
      public | t_w3b_3key  | table | mxadmin | mars3   | 2764 MB |
      public | testmars3   | table | mxadmin | mars3   | 160 kB  |
      (9 rows)

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