F.11. Колоночно-ориентированный формат (ORC) Citus Columnar#

F.11. Колоночно-ориентированный формат (ORC) Citus Columnar

F.11. Колоночно-ориентированный формат (ORC) Citus Columnar

F.11.1. О хранилище Citus Columnar

Версия: 12.1.5

Страница проекта www.citusdata.com

GitHub

Copyright © Citus Data, Inc.

F.11.2. Введение

В традиционных аналитических системах (OLAP) размер и объем данных со временем увеличиваются, что может привести к различным проблемам с производительностью. Хранилище Columnar обеспечивает значительные преимущества в случаях, когда системы управления базами данных (СУБД) используются для аналитических задач, в которых пакетная загрузка является основным способом загрузки данных.

При использовании данного формата можно сжимать таблицы для значительного снижения потребления ресурсов и увеличения производительности. При этом нет необходимости переключения на СУБД с массово-параллельной архитектурой. Колоночная структура ORC в хранилище Columnar обеспечивает высокую производительность за счет чтения соответствующих данных с диска и их более эффективного сжатия.

Примечание

Хранилище Columnar не полностью заменяет СУБД с массивной параллельной обработкой, но исключает их использование в некоторых случаях. Решение о переходе на MPP СУБД зависит от конкретных объемов данных и требований к рабочей нагрузке. Хотя пороговое значение варьируется, обычно оно находится в пределах десятков терабайт.

F.11.3. Установка Сolumnar

Хранилище Columnar устанавливается как расширение. Чтобы добавить Columnar в вашу локальную базу данных Tantor SE, подключитесь с помощью psql и выполните:

CREATE EXTENSION pg_columnar;

Теперь вы можете создавать и использовать колоночные таблицы в вашей базе данных.

F.11.4. Основные принципы колоночного хранения данных

Стандартный метод хранения данных heap в Tantor SE хорошо работает для нагрузки OLTP:

  • Поддержка операций UPDATE/DELETE

  • Эффективный поиск одиночных кортежей

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

  • Сжатие

  • Не читаются ненужные столбцы

  • Эффективное использование VACUUM

F.11.5. Использование функциональности метода доступа к таблице

Преимущества Columnar над cstore_fdw:

  • Хранение в колонках основано на API-методе доступа к таблице. В большинстве случаев этот API позволяет выполнять аналогичные операции как над колоночными таблицами, так и над традиционными строковыми таблицами (таблицами heap).

  • Поддержка журнала предварительной записи (WAL)

  • Поддержка транзакций ROLLBACK;

  • Поддержка физической репликации;

  • Поддержка восстановления, включая восстановление до определенного момента времени (PITR);

  • Поддержка pg_dump и pg_upgrade без необходимости специальных операций или дополнительных шагов;

  • Поддерживает большую часть функциональности, которая работает с обычными таблицами heap (строковыми таблицами)

Ограничения

  • Только для добавления (UPDATE/DELETE операции не поддерживаются);

  • Не освобождает пространство (например, откатившиеся транзакции могут все еще занимать дисковое пространство);

  • Поддержка сканирования битовых индексов отсутствует;

  • Не поддерживается tidscans;

  • Поддержка выборочного сканирования отсутствует;

  • Метод хранения TOAST не поддерживается (большие значения хранятся внутренне);

  • ON CONFLICT не поддерживается (кроме DO NOTHING без указания цели);

  • Уровень строковых блокировок не поддерживается (SELECT ... FOR SHARE, SELECT ... FOR UPDATE);

  • Уровень изоляции serializable не поддерживается.

  • Поддерживаются версии Tantor SE 14+;

  • Внешние ключи, ограничения уникальности или ограничение-исключения не поддерживаются.

  • Логическое декодирование не поддерживается;

  • Параллельные сканирования внутри узла не поддерживаются;

  • AFTER ... FOR EACH ROW триггеры не поддерживаются;

  • Непротоколируемые колоночные таблицы не поддерживаются.

Со временем, перечисленные выше ограничения будут постепенно сниматься.

F.11.6. Использование колоночного формата

Создайте колоночную таблицу, указав опцию USING columnar:

CREATE TABLE my_columnar_table
(
    id INT,
    i1 INT,
    i2 INT8,
    n NUMERIC,
    t TEXT
) USING columnar;

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

Чтобы просмотреть внутреннюю статистику для таблицы, используйте VACUUM VERBOSE. Обратите внимание, что VACUUM (без FULL) гораздо быстрее работает на колоночной таблице, потому что сканируются только метаданные, а не фактические данные.

Установите параметры таблицы с помощью ALTER TABLE:

ALTER TABLE my_columnar_table SET(
  columnar.compression = 'none',
  columnar.stripe_row_limit = 10000
);

Можно настроить следующие параметры:

  • columnar.compression: ['none'|'pglz'|'zstd'|'lz4'|'lz4hc'] - устанавливает тип сжатия для вновь вставленных данных. Существующие данные повторно не сжимаются и не распаковываются. Значение по умолчанию - 'zstd' (если поддержка данного алгоритма была указана при компиляции).

  • columnar.compression_level: <integer> - устанавливает уровень сжатия. Допустимые значения от 1 до 19. Если метод сжатия не поддерживает выбранный уровень, вместо него будет выбрано ближайшее к нему значение. Значение по умолчанию 3.

  • columnar.stripe_row_limit: <integer> - максимальное количество строк в полосе для вновь вставленных данных. Существующие полосы данных не будут изменены и могут содержать больше строк, чем указанное максимальное значение. Допустимые значения от 1000 до 100000000. Значение по умолчанию - 150000.

  • columnar.chunk_group_row_limit: <integer> - максимальное количество строк в блоке для вновь вставленных данных. Существующие блоки данных не будут изменены и могут содержать больше строк, чем это максимальное значение. Допустимые значения от 1000 до 100000000. Значение по умолчанию - 10000.

Можно просмотреть параметры всех таблиц, используя:

SELECT * from columnar.options;

Или конкретной таблицы:

SELECT * FROM columnar.options
WHERE relation = 'my_columnar_table'::regclass;

Можно сбросить один или несколько параметров таблицы до их значений по умолчанию (или текущих значений, установленных командой SET) с помощью columnar.alter_columnar_table_reset:

SELECT columnar.alter_columnar_table_reset(
  'my_columnar_table',
  chunk_group_row_limit => true
);

Установите общие параметры columnar, используя SET:

SET columnar.compression TO 'none';
SET columnar.enable_qual_pushdown TO false;
SET columnar.compression TO default;
  • columnar.enable_custom_scan: <boolean> - включает использование настраиваемого сканирования для передачи проекций и квалификаторов непосредственно на уровень хранения. Значение по умолчанию - true.

  • columnar.enable_qual_pushdown: <boolean> - включает передачу квалификаторов в колоночный формат. Не сработает, если columnar.enable_custom_scan не true. Значение по умолчанию - true.

  • columnar.qual_pushdown_correlation_threshold: <real> - порог корреляции при достижении которого предпринимается попытка передать квалификатор, ссылающийся на заданную колонку. При установленном значении 0 предпринимается попытка передать все квалификаторы, даже если колонка некоррелированная. Допустимые значения - от 0.0 до 1.0. Значение по умолчанию - 0.9.

  • columnar.max_custom_scan_paths: <integer> - максимальное количество путей настраиваемого сканирования, которые нужно сгенерировать для колоночной таблицы при планировании. Допустимые значения от 1 до 1024. Значение по умолчанию - 64.

  • columnar.planner_debug_level - уровень сообщений по планированию колоночного хранения в порядке увеличения детальности:

    • 'log'

    • 'warning'

    • 'notice'

    • 'info'

    • 'debug'

    • 'debug1'

    • 'debug2'

    • 'debug3'

    • 'debug4'

    • 'debug5'

    Значение по умолчанию — debug3.

  • Также можно настроить следующие параметры таблиц: columnar.compression columnar.compression_level, columnar.stripe_row_limit, columnar.chunk_group_row_limit. Но изменения коснутся только вновь создаваемых таблиц, а не вновь создаваемых полос в существующих таблицах.

F.11.6.1. Преобразование между heap и columnar

Примечание

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

CREATE TABLE my_table(i INT8 DEFAULT '7');
INSERT INTO my_table VALUES(1);
-- convert to columnar
SELECT columnar.alter_table_set_access_method('my_table', 'columnar');
-- back to row
SELECT columnar.alter_table_set_access_method('my_table', 'heap');

F.11.6.2. Тип таблицы по умолчанию

Тип таблицы по умолчанию для базы данных по умолчанию - heap. Вы можете изменить тип таблицы по умолчанию, используя default_table_access_method:

SET default_table_access_method TO 'columnar';

Теперь для любой таблицы, которую вы создаете вручную с помощью CREATE TABLE, тип по умолчанию будет columnar.

F.11.6.3. Секционирование

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

CREATE TABLE parent(ts timestamptz, i int, n numeric, s text)
  PARTITION BY RANGE (ts);

-- columnar partition
CREATE TABLE p0 PARTITION OF parent
  FOR VALUES FROM ('2020-01-01') TO ('2020-02-01')
  USING COLUMNAR;
-- columnar partition
CREATE TABLE p1 PARTITION OF parent
  FOR VALUES FROM ('2020-02-01') TO ('2020-03-01')
  USING COLUMNAR;
-- row partition
CREATE TABLE p2 PARTITION OF parent
  FOR VALUES FROM ('2020-03-01') TO ('2020-04-01');

INSERT INTO parent VALUES ('2020-01-15', 10, 100, 'one thousand'); -- columnar
INSERT INTO parent VALUES ('2020-02-15', 20, 200, 'two thousand'); -- columnar
INSERT INTO parent VALUES ('2020-03-15', 30, 300, 'three thousand'); -- row

При выполнении операций с секционированной таблицей, состоящей из строк и колонок, обратите внимание на следующие особенности операций, которые поддерживаются для строк, но не поддерживаются для колонок (например, UPDATE, DELETE, блокировки строк и т.д.):

  • Если операция применяется на определенной таблице,в которой данные хранятся в кучах, (например, UPDATE p2 SET i = i + 1), она будет успешной; но если она выполняется на колоночной таблице (например UPDATE p1 SET i = i + 1), она не пройдет.

  • Если операция выполняется на секционированной таблице и содержит предложение WHERE, которое исключает все таблицы, сохраненные в виде колонок (например, UPDATE parent SET i = i + 1 WHERE ts = '2020-03-15'), она выполняется успешно.

  • Если операция выполняется на секционированной таблице, но не исключает все ctrwbb колонок, она не пройдет; даже если обновляемые фактические данные затрагивают только таблицы heap (например, UPDATE parent SET i = i + 1 WHERE n = 300).

Обратите внимание, что Columnar поддерживает индексы btree и hash (и ограничения, которые требуют их использования), но не поддерживает индексы gist, gin, spgist и brin. По этой причине, если некоторые таблицы имеют колоночный формат и если индекс не поддерживается, невозможно создать индексы непосредственно на секционированной (родительской) таблице. В таком случае нужно создать индекс на отдельных таблицах heap. То же самое касается ограничений, которые требуют использование индексов, например:

CREATE INDEX p2_ts_idx ON p2 (ts);
CREATE UNIQUE INDEX p2_i_unique ON p2 (i);
ALTER TABLE p2 ADD UNIQUE (n);

F.11.7. Микробенчмарк производительности

Важно

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

Установите plpython3u перед запуском теста:

CREATE EXTENSION plpython3u;

Схема:

CREATE TABLE perf_row(
    id INT8,
    ts TIMESTAMPTZ,
    customer_id INT8,
    vendor_id INT8,
    name TEXT,
    description TEXT,
    value NUMERIC,
    quantity INT4
);

CREATE TABLE perf_columnar(LIKE perf_row) USING COLUMNAR;

Функции для генерации данных:

CREATE OR REPLACE FUNCTION random_words(n INT4) RETURNS TEXT LANGUAGE plpython3u AS $$
import random
t = ''
words = ['zero','one','two','three','four','five','six','seven','eight','nine','ten']
for i in range(0,n):
  if (i != 0):
    t += ' '
  r = random.randint(0,len(words)-1)
  t += words[r]
return t
$$;

Вставьте данные, используя функцию random_words:

INSERT INTO perf_row
   SELECT
    g, -- id
    '2020-01-01'::timestamptz + ('1 minute'::interval * g), -- ts
    (random() * 1000000)::INT4, -- customer_id
    (random() * 100)::INT4, -- vendor_id
    random_words(7), -- name
    random_words(100), -- description
    (random() * 100000)::INT4/100.0, -- value
    (random() * 100)::INT4 -- quantity
   FROM generate_series(1,75000000) g;

INSERT INTO perf_columnar SELECT * FROM perf_row;

Проверьте уровень сжатия:

=> SELECT pg_total_relation_size('perf_row')::numeric/pg_total_relation_size('perf_columnar')
AS compression_ratio;

 compression_ratio
--------------------
 5.3958044063457513
(1 row)

Общий коэффициент сжатия колоночной таблицы по сравнению со сжатием тех же данных, хранящихся в куче, составляет 5.4X.

=> VACUUM VERBOSE perf_columnar;
INFO:  statistics for "perf_columnar":
storage id: 10000000000
total file size: 8761368576, total data size: 8734266196
compression rate: 5.01x
total row count: 75000000, stripe count: 500, average rows per stripe: 150000
chunk count: 60000, containing data for dropped columns: 0, zstd compressed: 60000

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

Характеристики системы:

  • Azure VM: Стандарт D2s v3 (2 виртуальных процессора, 8 ГиБ памяти)

  • Linux (ubuntu 18.04)

  • Дисковый накопитель данных: Стандартный HDD (512GB, максимум 500 IOPS, максимум 60 МБ/с)

  • PostgreSQL 13 (--with-llvm, --with-python)

  • shared_buffers = 128MB

  • max_parallel_workers_per_gather = 0

  • jit = on

Примечание

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

Запросы приложения:

-- OFFSET 1000 so that no rows are returned, and we collect only timings

SELECT vendor_id, SUM(quantity) FROM perf_row GROUP BY vendor_id OFFSET 1000;
SELECT vendor_id, SUM(quantity) FROM perf_row GROUP BY vendor_id OFFSET 1000;
SELECT vendor_id, SUM(quantity) FROM perf_row GROUP BY vendor_id OFFSET 1000;
SELECT vendor_id, SUM(quantity) FROM perf_columnar GROUP BY vendor_id OFFSET 1000;
SELECT vendor_id, SUM(quantity) FROM perf_columnar GROUP BY vendor_id OFFSET 1000;
SELECT vendor_id, SUM(quantity) FROM perf_columnar GROUP BY vendor_id OFFSET 1000;
Timing (median of three runs):
  • строка: 436с

  • колоночный: 16s

  • ускорение: 27X

F.11.8. Общие рекомендации

F.11.8.1. Выбор размера полосы (Stripe Size) и размера «куска» (Chunk Size)

Определение оптимальных размеров полос и блоков для колоночных таблиц в Tantor SE зависит от различных факторов, таких как тип данных, шаблоны запросов и характеристики оборудования. Вот некоторые рекомендации:

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

  • Размер блока: Это определяет, сколько данных будет сжиматься за один раз. Меньший размер блока может привести к более высокому коэффициенту сжатия, но может увеличить накладные расходы на сжатие. Больший размер блока может уменьшить накладные расходы, но потенциально снизить коэффициент сжатия.

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

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

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

F.11.8.2. Алгоритмы сжатия

Выбор алгоритма сжатия для columnar таблиц в Tantor SE зависит от нескольких факторов, включая характер данных, требования к производительности, и технические характеристики оборудования. Вот несколько рекомендаций, которые могут помочь вам принять решение:

  • none: Этот тип сжатия не применяет никакого сжатия к данным. Он может быть полезен, если ваши данные уже сжаты, или если у вас очень высокие требования к производительности и достаточно дискового пространства.

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

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

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

F.11.9. Работа с Time Series-данными

F.11.9.1. Создание таблиц

Time Series-данные характеризуются последовательной записью значений по возрастанию во времени. Для таких данных важна упорядоченность.

Создайте необходимые расширения:

create extension pg_columnar;
create extension plpython3u;

Создайте тестовую таблицу:

CREATE TABLE perf_columnar(
    id INT8,
    ts TIMESTAMPTZ,
    customer_id INT8,
    vendor_id INT8,
    name TEXT,
    description TEXT,
    value NUMERIC,
    quantity INT4
) USING columnar;

ALTER TABLE public.perf_columnar SET(
    columnar.compression = 'lz4',
    columnar.stripe_row_limit = 100000,
    columnar.chunk_group_row_limit = 10000
);

Создайте функцию для генерации случайного текста:

CREATE OR REPLACE FUNCTION random_words(n INT4) RETURNS TEXT LANGUAGE plpython3u AS $$
import random
t = ''
words = ['zero','one','two','three','four','five','six','seven','eight','nine','ten']
for i in range(0, n):
  if (i != 0):
    t += ' '
  r = random.randint(0, len(words) - 1)
  t += words[r]
return t
$$;

Сгенерируйте тестовые данные:

INSERT INTO perf_columnar
   SELECT
    g, -- id
    '2023-06-01'::timestamptz + ('1 minute'::interval * g), -- ts
    (random() * 1000000)::INT4, -- customer_id
    (random() * 100)::INT4, -- vendor_id
    random_words(7), -- name
    random_words(100), -- description
    (random() * 100000)::INT4/100.0, -- value
    (random() * 100)::INT4 -- quantity
   FROM generate_series(1,7500000) g;

Как видно из приведенного выше запроса, мы вставили данные, отсортированные по ts. Соберите статистику с помощью VACUUM (ANALYZE, VERBOSE):

VACUUM ANALYZE perf_columnar;

Создайте копию таблицы с другими параметрами:

CREATE TABLE perf_columnar2(LIKE perf_columnar) USING COLUMNAR;

ALTER TABLE public.perf_columnar2 SET(
    columnar.compression = 'zstd',
    columnar.stripe_row_limit = 10000,
    columnar.chunk_group_row_limit = 1000
);

INSERT INTO perf_columnar2 SELECT * FROM perf_columnar;

VACUUM ANALYZE perf_columnar2;

Проверьте размер получившихся таблиц:

test_db=# \dt+
                                        List of relations
 Schema |      Name      | Type  |  Owner   | Persistence | Access method |  Size   | Description 
--------+----------------+-------+----------+-------------+---------------+---------+-------------
 public | perf_columnar  | table | postgres | permanent   | columnar      | 1886 MB | 
 public | perf_columnar2 | table | postgres | permanent   | columnar      | 850 MB  | 
(2 rows)

Как видно, таблица perf_columnar в два раза больше, чем perf_columnar2.

Проверьте параметры таблиц:

test_db=# select * from columnar.options;

    relation    | chunk_group_row_limit | stripe_row_limit | compression | compression_level 
----------------+-----------------------+------------------+-------------+-------------------
 perf_columnar  |                 10000 |           100000 | lz4         |                 3
 perf_columnar2 |                  1000 |            10000 | zstd        |                 3

(2 rows)

F.11.9.2. Сравнение производительности запросов

Выполните типичный запрос для чтения набора записей из таблицы perf_columnar за определенный промежуток времени:

explain (analyze, verbose, buffers) select ts from perf_columnar
where ts < '2023-06-01 10:00:00'::timestamp with time zone and
      ts > '2023-06-01 10:00:05'::timestamp with time zone;
                                                                           QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
 Custom Scan (ColumnarScan) on public.perf_columnar  (cost=0.00..401.12 rows=4 width=8) (actual time=18.838..49.001 rows=4 loops=1)
   Output: ts
   Filter: ((perf_columnar.ts < '2023-06-01 10:00:00+03'::timestamp with time zone) AND (perf_columnar.ts > '2023-06-01 10:00:05+03'::timestamp with time zone))
   Rows Removed by Filter: 9996
   Columnar Projected Columns: ts
   Columnar Chunk Group Filters: ((ts < '2023-06-01 10:00:00+03'::timestamp with time zone) AND (ts > '2023-06-01 10:00:05+03'::timestamp with time zone))
   Columnar Chunk Groups Removed by Filter: 749     <-----------
   Buffers: shared hit=3833 read=264                <-----------
 Query Identifier: 1607994334608619710
 Planning:
   Buffers: shared hit=52 read=7
 Planning Time: 12.789 ms
 Execution Time: 49.188 ms
(13 rows)

Выполните типичный запрос для чтения набора записей из таблицы perf_columnar2 за определенный промежуток времени:

explain (analyze, verbose, buffers) select ts from perf_columnar2
where ts < '2020-01-01 10:00:00'::timestamp with time zone and
      ts > '2020-01-01 10:00:05'::timestamp with time zone;
                                                                            QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Custom Scan (ColumnarScan) on public.perf_columnar2  (cost=0.00..17.95 rows=52 width=8) (actual time=5.175..58.532 rows=49 loops=1)
   Output: ts
   Filter: ((perf_columnar2.ts < '2020-01-01 10:00:00+03'::timestamp with time zone) AND (perf_columnar2.ts > '2020-01-01 10:00:05+03'::timestamp with time zone))
   Rows Removed by Filter: 951
   Columnar Projected Columns: ts
   Columnar Chunk Group Filters: ((ts < '2020-01-01 10:00:00+03'::timestamp with time zone) AND (ts > '2020-01-01 10:00:05+03'::timestamp with time zone))
   Columnar Chunk Groups Removed by Filter: 7499    <-----------
   Buffers: shared hit=40824 read=1                 <-----------
 Query Identifier: -801549076851693482
 Planning:
   Buffers: shared hit=155
 Planning Time: 1.086 ms
 Execution Time: 58.717 ms
(13 rows)

Обратите внимание на Columnar Chunk Groups Removed by Filter: 749 в первом случае и 7499 во втором. Однако, количество прочитанных буферов во втором запросе гораздо выше.

Терминология для понимания поведения этих запросов:

  • Stripe: Этот тип не применяет никакого сжатия к данным. Он может быть полезен, если ваши данные уже сжаты, или если у вас очень высокие требования к производительности и достаточно дискового пространства.

  • Chunk Group: полосы разбиваются на группы блоков по 10 000 строк (по умолчанию).

  • Chunk: Каждая группа блоков состоит из одного блока для каждого столбца. Chunk является единицей сжатия, и для каждого блока отслеживаются минимальные/максимальные значения, что позволяет фильтровать группы блоков (Chunk Group Filtering).

  • Chunk Group Filtering: когда предложение запроса WHERE не соответствует ни одному кортежу в блоке, и можно определить это по минимальному/максимальному значению для блока, тогда фильтрация группы блоков просто пропускает всю группу блоков без ее распаковки.

Как мы видим, были отфильтрованы группы блоков 749/7499, что означает, что строки 7490000/7499000 были отфильтрованы без необходимости извлечения или распаковки данных. Только одна группа блоков (10 000 и 1 000 строк) требовала извлечения и распаковки, поэтому запрос занял всего несколько миллисекунд.

Но, как видно из планов запросов, в первом случае было использовано 30 МБ, а во втором - в десять раз больше - 319 МБ.

F.11.9.3. Использование индексов

Columnar поддерживает индексы btree и hash (и ограничения, требующие их), но не поддерживает типы индексов, такие как gist, gin, spgist и brin.

Рассмотрите возможность создания индексов для таблиц perf_columnar и perf_columnar2:

create index test on perf_columnar2 (ts);
create index test_h on perf_columnar (ts);

Проверьте размер индексов:

test_db=# \di+
                                            List of relations

 Schema |  Name  | Type  |  Owner   |     Table      | Persistence | Access method |  Size  | Description 
--------+--------+-------+----------+----------------+-------------+---------------+--------+-------------
 public | test   | index | postgres | perf_columnar2 | permanent   | btree         | 161 MB | 
 public | test_h | index | postgres | perf_columnar  | permanent   | btree         | 161 MB | 
(2 rows)

Размер индексов одинаковый.

Отключите принудительное использование Custom Scan:

SET columnar.enable_custom_scan TO false;

Повторно выполните типичный запрос для чтения набора записей из таблицы perf_columnar для определенного временного интервала:

explain (analyze, verbose, buffers) select ts from perf_columnar
where ts < '2023-06-01 10:00:00'::timestamp with time zone and
      ts > '2023-06-01 10:00:05'::timestamp with time zone;
                                                                             QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Index Scan using test_h on public.perf_columnar  (cost=0.43..3217.48 rows=4 width=8) (actual time=402.144..402.204 rows=4 loops=1)
   Output: ts
   Index Cond: ((perf_columnar.ts < '2023-06-01 10:00:00+03'::timestamp with time zone) AND (perf_columnar.ts > '2023-06-01 10:00:05+03'::timestamp with time zone))
   Buffers: shared hit=181 read=3232      <-----------
 Query Identifier: 1607994334608619710
 Planning:
   Buffers: shared hit=20 read=5
 Planning Time: 16.278 ms
 Execution Time: 402.386 ms
(9 rows)

Почти такое же количество буферов было просканировано, как при использовании Custom Scan.

Повторно выполните типичный запрос для чтения набора записей из таблицы perf_columnar2 для определенного временного интервала:

explain (analyze, verbose, buffers) select ts from perf_columnar2
where ts < '2020-01-01 10:00:00'::timestamp with time zone and
      ts > '2020-01-01 10:00:05'::timestamp with time zone;
                                                                              QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Index Scan using test on public.perf_columnar2  (cost=0.43..153.05 rows=52 width=8) (actual time=14.620..14.821 rows=49 loops=1)
   Output: ts
   Index Cond: ((perf_columnar2.ts < '2020-01-01 10:00:00+03'::timestamp with time zone) AND (perf_columnar2.ts > '2020-01-01 10:00:05+03'::timestamp with time zone))
   Buffers: shared hit=372 read=145     <-----------
 Query Identifier: -801549076851693482
 Planning:
   Buffers: shared hit=97
 Planning Time: 0.813 ms
 Execution Time: 14.978 ms
(13 rows)

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

F.11.9.4. Заключения

Можно сделать следующие выводы:

  • Самым эффективным методом сжатия данных является zstd;

  • Размер полосы и блока влияет на количество буферов, сканируемых с использованием метода Chunk Group Filtering;

  • При чтении небольшого объема данных использование индекса может оказаться более эффективным;

  • Чтобы проверить гипотезу, необходимо принудительно включить использование индексов с помощью команды SET columnar.enable_custom_scan TO false;

  • Последовательная запись данных TS может значительно сократить размер индексов и объем распакованных блоков (Chunk). Поэтому рекомендуется сортировать данные перед их вставкой в базу данных или использовать кластеризацию (cluster).