F.83. vops — поддержка векторных операций #

Модуль vops — экспериментальное расширение Postgres Pro Enterprise, поддерживающее векторные операции, что позволяет ускорить запросы OLAP с фильтрацией и агрегированием более чем в 10 раз. Такое увеличение производительности достигается благодаря реализации вертикальной модели данных, в которой данные группируются по значениям столбцов и хранятся в виде «плиток». Этот формат хранения в некоторых аналитических базах данных также называется «паркетом». Уменьшая издержки хранения неупакованных кортежей, вертикальная модель данных позволяет ускорить выполнение запросов без радикальных изменений в планировщике и исполнителе Postgres Pro.

vops предоставляет собственный набор векторных функций, которые можно использовать в предикатах и агрегатных выражениях. Чтобы не изучать новый синтаксис, вы можете воспользоваться postgres_fdw в качестве уровня абстракции и выполнять обычные SQL-запросы к векторизованным данным. Для других типов запросов (не фильтрующих и не агрегирующих) vops может также посредством postgres_fdw представлять векторизованные данные в виде обычной таблицы со скалярными типами столбцов и обрабатывать их как обычные кортежи. Для упрощения использования vops также предоставляет автоматический способ формирования векторизованного представления данных и обращения к нему, о чём подробнее рассказывается в Подразделе F.83.5.5.

F.83.1. Ограничения #

  • Таблицы в виде плиток допускают только операции INSERT и APPEND. Загруженные в них векторизованные данные изменить нельзя.

  • Векторные операции возможны только для фильтрации и агрегирования. Все остальные типы запросов поддерживаются через postgres_fdw.

  • Агрегатные выражения должны быть одинакового типа.

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

  • Так как postgres_fdw не поддерживает выполнение параллельных запросов, такие запросы вы сможете выполнять только применяя непосредственно векторные операции.

F.83.2. Установка и подготовка #

Расширение vops включено в состав Postgres Pro Enterprise. Установив Postgres Pro Enterprise, выполните следующие действия, чтобы подготовить vops к работе:

  1. Добавьте vops в параметр shared_preload_libraries в файле postgresql.conf:

    shared_preload_libraries = 'vops'

    Примечание

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

  2. Перезапустите Postgres Pro Enterprise, чтобы изменения вступили в силу.

  3. Создайте расширение vops:

    CREATE EXTENSION vops;

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

Если вы предпочитаете использовать обычный синтаксис SQL для векторных типов, вы должны также включить postgres_fdw.

F.83.3. Объяснение #

С нагрузкой OLTP Postgres Pro показывает себя достаточно хорошо в сравнении с другими популярными СУБД. Однако запросы OLAP, охватывающие большие объёмы данных, могут выполняться гораздо быстрее системами, ориентированными на аналитическую обработку. Производительность Postgres Pro ограничена следующими факторами:

  • Издержки распаковки кортежей. Чтобы прочитать значение столбца, Postgres Pro должен распаковать кортеж. Так как таблицы имеют столбцы разных типов переменной длины, для извлечения N-го столбца необходимо распаковать предшествующие столбцы (N-1). Таким образом преобразование кортежа — довольно дорогая операция, особенно для таблиц с большим количеством атрибутов. Помимо этого, значения могут храниться в сжатом виде или на другой странице (TOAST). В запросах, подобных запросу 6 теста TPC-H, преобразование кортежей занимает около 40% общего времени выполнения.

  • Издержки интерпретирования. Планировщик и оптимизатор Postgres Pro строят дерево, представляющее план выполнения запроса. Исполнитель запроса рекурсивно вызывает функции, вычисляющие узлы этого дерева. Некоторые узлы могут также содержать условия, выбирающие требуемое действие. Таким образом план запроса не выполняется непосредственно, а интерпретируется исполнителем запросов Postgres Pro. Этот интерпретатор работает примерно в 10 раз медленнее машинного кода. Если исключить издержки интерпретирования, скорость запросов можно увеличить многократно; особенно это касается запросов со сложными предикатами, стоимость вычисления которых высока.

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

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

  • Издержки, связанные с MVCC. Postgres Pro обеспечивает многоверсионное управление конкурентным доступом, что позволяет нескольким транзакциям работать с одной и той же записью параллельно, не блокируя друг друга. Это хорошо для часто изменяемых данных (OLTP), но для данных, которые только читаются или только добавляются в сценариях OLAP, впустую расходуется не только место (около 20 дополнительных байт на кортеж), но и ресурсы процессора (для проверки видимости каждого кортежа).

Векторные операции позволяют устранить большинство этих факторов без радикальных изменений в исполнителе Postgres Pro, как описывается в Подразделе F.83.4.

F.83.4. Архитектура #

Расширение vops реализует вертикальную модель данных для Postgres Pro. В этой модели данные хранятся в вертикальных столбцах, или векторах, формируемых из значений соответствующих атрибутов таблицы.

Вертикальная модель данных, которую реализует vops, имеет следующие преимущества:

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

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

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

  • Использование векторных инструкций процессора (SIMD) при обработке данных.

Традиционный исполнитель запросов Postgres Pro имеет дело с одной строкой данных в каждый момент времени. Например, чтобы вычислить выражение (x+y), он сначала извлекает значение x, затем извлекает значение y, выполняет сложение и, наконец, выдаёт значение результата верхнему узлу. Векторизованный исполнитель, напротив, может обработать множество значений в одной операции. В этом случае x и y представляют векторы значений, а не отдельные скаляры. Возвращаемым результатом также будет вектор. В векторной модели выполнения издержки интерпретации и вызовов функций делятся на размер вектора. Стоимость вызова функции остаётся прежней, но так как функция обрабатывает N значений вместо одного, эти накладные расходы становятся не такими значимыми. Чем больше вектор, тем меньше относительные издержки для одной строки.

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

Расширение vops реализует специальные векторные типы, которые должны использоваться вместо скалярных типов для столбцов таблицы. Доступные типы перечислены в Подразделе F.83.6.1. Для применения vops вы должны создать векторизованные проекции исходных таблиц с атрибутами, использующими эти типы, как описано в Подразделе F.83.5.1.

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

Загрузив все данные в плиточную структуру, вы можете выполнять векторизованные запросы к этим данным. vops предоставляет набор векторных операторов для работы с векторными типами. Применяя эти операторы, можно составлять запросы для фильтрования и агрегирования данных, подобные обычным SQL-запросам. За подробностями обратитесь к Подразделу F.83.5.2.

Для других типов запросов vops позволяет работать с векторизованными данными через обёртку postgres_fdw. Вы можете подключить такие данные в ваш кластер баз данных как стороннюю таблицу и обращаться к ней произвольным образом, применяя обычный SQL. Используя неявные приведения типов и код, внедрённый на стадии после разбора ANALYZE, расширение vops либо для фильтрации и агрегирования преобразует запрос с целью применения векторных операций, либо для других типов запросов обрабатывает данные, применяя скалярные операторы. За подробностями обратитесь к Подразделу F.83.5.3.

F.83.5. Использование #

F.83.5.1. Преобразование данных в векторизованный формат #

Чтобы начать использовать векторные операции, вы должны преобразовать данные в векторизованный формат, как описано ниже.

  1. Создайте пустую векторизованную таблицу.

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

    Например, предположим, что у вас есть следующая таблица из теста производительности TPC-H:

    CREATE TABLE lineitem(
       l_orderkey integer,
       l_partkey integer,
       l_suppkey integer,
       l_linenumber integer,
       l_quantity double precision,
       l_extendedprice double precision,
       l_discount double precision,
       l_tax double precision,
       l_returnflag "char",
       l_linestatus "char",
       l_shipdate date,
       l_commitdate date,
       l_receiptdate date,
       l_shipinstruct char(25),
       l_shipmode char(10),
       l_comment char(44));

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

    CREATE TABLE vops_lineitem(
       l_shipdate vops_date not null,
       l_quantity vops_float8 not null,
       l_extendedprice vops_float8 not null,
       l_discount vops_float8 not null,
       l_tax vops_float8 not null,
       l_returnflag vops_char not null,
       l_linestatus vops_char not null
    );

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

    CREATE TABLE vops_lineitem_projection(
       l_shipdate vops_date not null,
       l_quantity vops_float8 not null,
       l_extendedprice vops_float8 not null,
       l_discount vops_float8 not null,
       l_tax vops_float8 not null,
       l_returnflag "char" not null,
       l_linestatus "char" not null
    );

    В этой таблице поля l_returnflag и l_linestatus — скалярные, тогда как все остальные имеют векторный тип и могут быть использованы в векторных операциях.

  2. Загрузите данные в векторизованную таблицу.

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

      Например:

      SELECT populate(destination := 'vops_lineitem'::regclass, source := 'lineitem'::regclass);

      Наполнив таблицу, вы можете приступить к выполнению запросов с последовательным сканированием.

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

      SELECT populate(destination := 'vops_lineitem_projection'::regclass, source := 'lineitem'::regclass, sort := 'l_returnflag,l_linestatus');

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

    • Если ваши данные ещё не загружены в базу и вы не хотите иметь две копии одних и тех же данных, вы можете импортировать их непосредственно в векторизованную таблицу из файла CSV с помощью функции import(), пропустив этап создания обычной таблицы. Например:

      SELECT import(destination := 'vops_lineitem'::regclass, csv_path := '/mnt/data/lineitem.csv', separator := '|');

Подробное описание и синтаксис функций populate() и import() вы можете найти в Подразделе F.83.6.4.

Когда ваши данные представлены в плиточной структуре, вы можете обращаться к ним в запросах, используя векторные операторы, как описано в Подразделе F.83.5.2. Если вы создадите стороннюю векторизованную таблицу, к ним можно будет обращаться и в обычных SQL-запросах. Подробнее об этом рассказывается в Подразделе F.83.5.3.

F.83.5.2. Выполнение запросов с векторизованными данными #

vops реализует собственные векторные функции и операторы для работы с векторизованными данными. Полный список векторных функций и операторов, а также описание их особенностей приведены в Подразделе F.83.6.

F.83.5.2.1. Агрегирование векторизованных данных #

В запросах OLAP часто агрегируются большие объёмы данных. Для выполнения агрегатных операций с векторными типами vops реализует следующие агрегатные функции: count, min, max, avg, var_pop, var_sampl, variance, stddev_pop, stddev_samp, stddev. Кроме того, vops предоставляет агрегатную функцию wavg, вычисляющую взвешенную по объёму среднюю цену (VWAP, Volume-Weighted Average Price).

Расширение vops поддерживает следующие типы агрегатных функций:

  • агрегатные вычисления для всей таблицы

  • агрегатные вычисления для подмножеств строк таблицы, определённых предложением GROUP BY

F.83.5.2.1.1. Вычисление общих агрегатных значений #

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

Примеры:

Для вычисления взвешенной по объёму средней цены выполните следующий запрос:

SELECT wavg(l_extendedprice,l_quantity) FROM vops_lineitem;

Для вычисления дохода компании за 1996 год по поставленным с определённой скидкой товарам, выполните:

SELECT sum(l_extendedprice*l_discount) AS revenue
  FROM vops_lineitem
  WHERE filter(betwixt(l_shipdate, '1996-01-01', '1997-01-01')
        & betwixt(l_discount, 0.08, 0.1)
        & (l_quantity < 24));
F.83.5.2.1.2. Вычисление агрегатных значений с группировкой #

Для поддержки агрегатных вычислений с группировкой vops предоставляет функции map() и reduce():

  • Функция map() собирает агрегатные состояния по всем группам в хеш-таблицу. Она имеет следующий синтаксис:

    map(выражение_группировки,
          список_агрегатов, выражение {,
          выражение })
  • Функция reduce() проходит по хеш-таблице, сформированной функцией map, и возвращает множество. Потребность в функции reduce объясняется тем, что в Postgres Pro результат агрегирования не может быть множеством.

Рассмотрим следующий пример:

SELECT reduce(map(l_returnflag||l_linestatus, 'sum,sum,sum,sum,avg,avg,avg',
    l_quantity,
    l_extendedprice,
    l_extendedprice*(1-l_discount),
    l_extendedprice*(1-l_discount)*(1+l_tax),
    l_quantity,
    l_extendedprice,
    l_discount))
FROM vops_lineitem
WHERE filter(l_shipdate <= '1998-12-01'::date);

Оператор конкатенации || осуществляет группировку по двум столбцам. Расширение vops поддерживает группировку только по целочисленным типам. Функция map принимает параметры агрегирования в виде массива переменной длины, поэтому все элементы этого массива должны иметь один тип. Например, в одной операции нельзя вычислить агрегатные функции по столбцам vops_float4 и vops_int8.

Строка агрегатов в функции map должна содержать список агрегатных функций, которые будут вызываться, через запятую. В нём могут указываться стандартные имена в нижнем регистре: count, sum, agg, min, max. Подсчёт количества выполняется для конкретного столбца: count(x). Указывать count(*) явно нет необходимости, так как функция reduce в любом случае возвращает число записей в каждой группе.

Функция reduce возвращает множество элементов типа vops_aggregate. Каждый элемент содержит три компонента: значение выражения GROUP BY, число записей в группе и массив чисел с агрегированными значениями. Результаты всех агрегатных функций, включая count и min/max, возвращаются в виде чисел с плавающей точкой.

Вы также можете секционировать векторизованную таблицу по полям GROUP BY для вычисления группируемых агрегатов. В этом случае группирующие ключи хранятся как обычные скалярные значения, тогда как другие поля хранятся в плиточной структуре. Тогда исполнитель Postgres Pro будет применять агрегатные операции vops для каждой группы:

SELECT
    l_returnflag,
    l_linestatus,
    sum(l_quantity) as sum_qty,
    sum(l_extendedprice) AS sum_base_price,
    sum(l_extendedprice*(1-l_discount)) AS sum_disc_price,
    sum(l_extendedprice*(1-l_discount)*(1+l_tax)) AS sum_charge,
    avg(l_quantity) AS avg_qty,
    avg(l_extendedprice) AS avg_price,
    avg(l_discount) AS avg_disc,
    count(*) AS count_order
FROM
    vops_lineitem_projection
WHERE
    filter(l_shipdate <= '1998-12-01'::date)
GROUP BY
    l_returnflag,
    l_linestatus
ORDER BY
    l_returnflag,
    l_linestatus;

В данном примере поля l_returnflag и l_linestatus таблицы vops_lineitem_projection имеют тип char, тогда как все остальные задействованные поля — векторные. Показанный выше запрос выполняется ещё быстрее, чем запрос с функциями map() и reduce(). Основной недостаток этого подхода в том, что вам придётся создавать отдельную проекцию для каждой комбинации ключей GROUP BY, используемой в запросах.

F.83.5.2.2. Использование оконных векторных функций #

Расширение vops обеспечивает ограниченную поддержку оконных функций Postgres Pro. Оно реализует функции mcount, msum, mmin, mmax, mavg и lag, соответствующие стандартным оконным функциям count, sum, min, max, avg и lag.

Примите к сведению следующие важные ограничения:

  1. Фильтрацию, группировку и сортировку можно выполнять только по скалярным атрибутам.

  2. Векторные оконные функции поддерживают только рамки окон, определённые предложением ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.

Примеры:

SELECT vops_unnest(t.*)
  FROM (SELECT mcount(*) over w,mcount(x) over w,msum(x) over w,mavg(x) over w,mmin(x) over w,mmax(x) over w,x - lag(x) over w 
  FROM v window w
  AS (rows between unbounded preceding and current row)) t;
F.83.5.2.3. Преобразование результатов в горизонтальный формат #

Запрос из проекции vops возвращает набор плиток. Функция вывода для типа плитка может отображать содержимое плитки. Однако если вы хотите преобразовать результат в горизонтальный формат, в котором каждый кортеж представляет одну запись, используйте функцию vops-unnest. Например:

SELECT vops_unnest(l.*)
  FROM vops_lineitem l
  WHERE filter(l_shipdate >= '1998-12-01'::date);

F.83.5.3. Использование обычного SQL для векторных операций #

Векторные операторы могут эффективно работать только для выполнения фильтрации и агрегирования. Для других задач вы должны выполнять обычные SQL-запросы. Расширение vops предлагает для этого следующие варианты:

  • Использовать исходную таблицу, если она есть.

  • Настроить обёртку сторонних данных (FDW), используя postgres_fdw, для представления векторизованной таблицы в Postgres Pro в виде обычной таблицы со столбцами скалярных типов.

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

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

  2. Сведение запросов фильтрации и агрегирования к векторным операциям.

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

Таким образом, скрывая векторизованную проекцию за FDW, вы можете использовать стандартный SQL без специфических функций vops для выполнения последовательного сканирования и агрегирующих запросов, как если бы они обращались явно к таблице vops, и в то же время выполнять другие запросы к этим данным, включая операции JOIN, общие табличные выражения (CTE) и т. д. Запросы, которые могут быть эффективно обработаны векторными операциями, оптимизатор запросов Postgres Pro передаст обёртке vops, и они будут выполнены с применением векторных операций. С другими запросами данные выбираются из векторизованных таблицы как стандартные кортежи. Если определяется, что нужно применить векторные операции, vops задействует внедряемый на стадии после ANALYZE код, который заменяет скалярные булевы операции векторными:

Исходное выражениеПреобразованное выражение
NOT filter(o1)filter(vops_bool_not(o1))
filter(o1) AND filter(o2)filter(vops_bool_and(o1, o2))
filter(o1) OR filter(o2)filter(vops_bool_or(o1, o2))

Есть только одно различие между стандартным SQL и его векторизованным расширением — вы в любом случае должны применять явное приведение типа для строковых констант. Например, выражение l_shipdate <= '1998-12-01' не будет работать со столбцом l_shipdate векторного типа. В Postgres Pro Enterprise есть две перегруженные версии оператора <= для работы с соответствующим скалярным или векторным типом:

  • vops_date <= vops_date

  • vops_date <= date

Следующий пример иллюстрирует создание векторизованной таблицы через postgres_fdw и несколько запросов с ней:

CREATE FOREIGN TABLE lineitem_fdw (
   l_suppkey int4 not null,
   l_orderkey int4 not null,
   l_partkey int4 not null,
   l_shipdate date not null,
   l_quantity float8 not null,
   l_extendedprice float8 not null,
   l_discount float8 not null,
   l_tax      float8 not null,
   l_returnflag "char" not null,
   l_linestatus "char" not null
) SERVER vops_server OPTIONS (table_name 'vops_lineitem');

EXPLAIN SELECT
   sum(l_extendedprice*l_discount) AS revenue
FROM
   lineitem_fdw
WHERE
   l_shipdate BETWEEN '1996-01-01' AND '1997-01-01'
   AND l_discount BETWEEN 0.08 AND 0.1
   AND l_quantity < 24;
                       QUERY PLAN                        
---------------------------------------------------------
 Foreign Scan  (cost=1903.26..1664020.23 rows=1 width=4)
(1 row)

-- Фильтрация была передана FDW

EXPLAIN SELECT
    n_name,
    count(*),
    sum(l_extendedprice * (1-l_discount)) AS revenue
FROM
    customer_fdw JOIN orders_fdw ON c_custkey = o_custkey
    JOIN lineitem_fdw ON l_orderkey = o_orderkey
    JOIN supplier_fdw ON l_suppkey = s_suppkey
    JOIN nation ON c_nationkey = n_nationkey
    JOIN region ON n_regionkey = r_regionkey
WHERE
    c_nationkey = s_nationkey
    AND r_name = 'ASIA'
    AND o_orderdate >= '1996-01-01'
    AND o_orderdate < '1997-01-01'
GROUP BY
    n_name
ORDER BY
    revenue desc;
                                                              QUERY PLAN                                                              
--------------------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=2337312.28..2337312.78 rows=200 width=48)
   Sort Key: (sum((lineitem_fdw.l_extendedprice * ('1'::double precision - lineitem_fdw.l_discount)))) DESC
   ->  GroupAggregate  (cost=2336881.54..2337304.64 rows=200 width=48)
         Group Key: nation.n_name
         ->  Sort  (cost=2336881.54..2336951.73 rows=28073 width=40)
               Sort Key: nation.n_name
               ->  Hash Join  (cost=396050.65..2334807.39 rows=28073 width=40)
                     Hash Cond: ((orders_fdw.o_custkey = customer_fdw.c_custkey) AND (nation.n_nationkey = customer_fdw.c_nationkey))
                     ->  Hash Join  (cost=335084.53..2247223.46 rows=701672 width=52)
                           Hash Cond: (lineitem_fdw.l_orderkey = orders_fdw.o_orderkey)
                           ->  Hash Join  (cost=2887.07..1786058.18 rows=4607421 width=52)
                                 Hash Cond: (lineitem_fdw.l_suppkey = supplier_fdw.s_suppkey)
                                 ->  Foreign Scan on lineitem_fdw  (cost=0.00..1512151.52 rows=59986176 width=16)
                                 ->  Hash  (cost=2790.80..2790.80 rows=7702 width=44)
                                       ->  Hash Join  (cost=40.97..2790.80 rows=7702 width=44)
                                             Hash Cond: (supplier_fdw.s_nationkey = nation.n_nationkey)
                                             ->  Foreign Scan on supplier_fdw  (cost=0.00..2174.64 rows=100032 width=8)
                                             ->  Hash  (cost=40.79..40.79 rows=15 width=36)
                                                   ->  Hash Join  (cost=20.05..40.79 rows=15 width=36)
                                                         Hash Cond: (nation.n_regionkey = region.r_regionkey)
                                                         ->  Seq Scan on nation  (cost=0.00..17.70 rows=770 width=40)
                                                         ->  Hash  (cost=20.00..20.00 rows=4 width=4)
                                                               ->  Seq Scan on region  (cost=0.00..20.00 rows=4 width=4)
                                                                     Filter: ((r_name)::text = 'ASIA'::text)
                           ->  Hash  (cost=294718.76..294718.76 rows=2284376 width=8)
                                 ->  Foreign Scan on orders_fdw  (cost=0.00..294718.76 rows=2284376 width=8)
                     ->  Hash  (cost=32605.64..32605.64 rows=1500032 width=8)
                           ->  Foreign Scan on customer_fdw  (cost=0.00..32605.64 rows=1500032 width=8)

-- фильтрация по диапазону значений в orders передана FDW

F.83.5.4. Построение индексов для векторизованных таблиц #

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

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

  • Функции first() и last() предназначены для отсортированного набора данных. В этом случае первым и последним значением в плитке являются соответственно наименьшее и наибольшее значение в этой плитке.

  • Функции high() и low() предназначены для несортированных данных. Выполнение этих функций обходится дороже, так как они должны просмотреть все значения в плитке.

Используя возвращённые значения, вы можете построить функциональные индексы для векторизованных данных. Индекс BRIN представляется наиболее подходящим:

CREATE INDEX low_boundary ON trades
USING brin(first(day)); -- таблица trades упорядочивается по дням
CREATE INDEX high_boundary ON trades
USING brin(last(day)); -- таблица table упорядочивается по дням

Теперь вы можете использовать созданные индексы в запросах. Однако вам придётся перепроверять точное условие, так как подобный индекс даёт лишь приблизительный результат:

SELECT sum(price) FROM trades
WHERE first(day) >= '2015-01-01' AND last(day) <= '2016-01-01'
AND filter(betwixt(day, '2015-01-01', '2016-01-01'));

F.83.5.5. Использование автоматических проекций #

Расширение vops может автоматизировать создание и поддержание векторизованных проекций данных. Это позволяет не использовать postgres_fdw: vops может перенаправлять обычные SQL-запросы к таблицам-проекциям автоматически, если выполняются следующие условия:

  • В запросе отсутствуют соединения.

  • В запросе выполняется агрегирование векторных столбцов.

  • Все другие выражения в списке целевых столбцов, а также в предложениях ORDER BY и GROUP BY, обращаются только к скалярным столбцам.

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

  1. Создайте векторизованную проекцию таблицы с помощью функции create_projection().

    Например, чтобы создать проекцию показанной ранее таблицы lineitem, подобную vops_lineitem, выполните:

    SELECT create_projection('auto_vops_lineitem','lineitem',
                             array['l_shipdate','l_quantity','l_extendedprice','l_discount','l_tax'],
                             array['l_returnflag','l_linestatus']);

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

    Дополнительно вы можете передать необязательный параметр order_by для ускорения последующих операций обновления проекций. Значения столбцов, перечисленных в order_by, должны быть уникальными. Например, чтобы записи таблицы сортировались по столбцу l_shipdate, вы можете создать таблицу-проекцию следующим образом:

    SELECT create_projection('auto_vops_lineitem_ordered','lineitem',
                             array['l_shipdate','l_quantity','l_extendedprice','l_discount','l_tax'],
                             array['l_returnflag','l_linestatus'],
                             'l_shipdate');
  2. Установите для параметра vops.auto_substitute_projections значение on (вкл.).

    Теперь запросы, которые vops может перенаправить к таблице-проекции auto_vops_lineitem, будут выполняться с векторизованными данными.

Проекции не обновляются автоматически, поэтому после изменения данных в исходной таблице неявно перенаправленные запросы могут выдавать неточные данные. Чтобы избежать этого, необходимо обновлять проекции, прежде чем выполнять запросы к векторизованным данным. Для этого vops предоставляет функции проекция_refresh(), где проекция — имя проекции, которую нужно обновить. Например, чтобы обновить проекцию auto_vops_lineitem, выполните:

SELECT auto_vops_lineitem_refresh();

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

Если при создании проекции задаётся атрибут order_by, vops также создаёт следующие индексы:

  • Два функциональных индекса BRIN по функциям first() и last() с данным атрибутом. Например, если исходный запрос содержит предикаты вида (l_shipdate between '01-01-2017' and '01-01-2018'), vops, перенаправляя запрос, добавляет конъюнкцию (first(l_shipdate) >= '01-01-2017' and last(l_shipdate) >= '01-01-2018'), чтобы оптимизатор Postgres Pro мог быстро найти нужные страницы, используя индексы BRIN.

  • Индекс BRIN для скалярных столбцов, выбранных для группирования данных. Такой индекс позволяет эффективно выбирать группы и выполнять соединения по индексу.

F.83.6. Справка #

F.83.6.1. Векторные типы #

Расширение vops поддерживает все основные числовые типы Postgres Pro: 1-, 2-, 4-, 8-байтовые целые, а также 4- и 8-байтовые числа с плавающей точкой. Типы date и timestamp используют ту же реализацию, что и int4, и int8, соответственно. В следующей таблице перечислены имеющиеся векторные типы, в сопоставлении с соответствующими типами SQL и C.

Тип SQLТип CВекторный тип vops
boolboolvops_bool
"char"charvops_char
dateDateADTvops_date
int2int16vops_int2
int4int32vops_int4
int8int64vops_int8
float4float4vops_float4
float8float8vops_float8
timestampTimestampvops_timestamp
char(N), varchar(N)textvops_text(N)

Примечание

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

F.83.6.2. Векторные операторы #

Расширение vops поддерживает набор операторов для векторных типов.

F.83.6.2.1. Математические операторы #

Для математических операций vops перегружает обычные операторы SQL, чтобы они принимали операнды и векторного, и скалярного типа.

ОператорОписание
+Сложение
-Бинарное вычитание или унарная смена знака
*Умножение
/Деление
||Конкатенация. Этот оператор поддерживается только для типов char, int2 и int4. Он возвращает целочисленное значение удвоенного размера:
(char || char) -> int2
(int2 || int2) -> int4
(int4 || int4) -> int8
F.83.6.2.2. Операторы сравнения #

Для операций сравнения vops перегружает обычные операторы SQL, чтобы они принимали операнды и векторного, и скалярного типа.

ОператорОписание
=Равно
<>Не равно
<Меньше
<=Меньше или равно
>Больше
>=Больше или равно
F.83.6.2.3. Булевы операторы #

Булевы операторы AND, OR и NOT нельзя перегрузить, поэтому vops предоставляет собственную реализацию булевых операторов для векторных типов:

ОператорОписание
&Логическое И (AND)
|Логическое ИЛИ (OR)
!Логическое НЕ (NOT)

Векторные булевы операторы имеют следующие особенности:

  • Приоритеты векторных булевых операторов отличаются от AND, OR и NOT, поэтому их операнды нужно заключать в скобки. Например:

    (x=1) | (x=2)
    
  • Результат векторных булевых операций имеет тип vops_bool. Так как Postgres Pro требует, чтобы выражения предикатов имели обычный булев тип, вы должны привести результат к типу bool, используя функцию filter(). Эта функция, принимающая векторное булево выражение, вычисляет значение filter_mask, которое определяет результат данной булевой операции для каждого элемента вектора. Если все биты маски равны нулю (предикат не выполняется для всех элементов вектора), то функция filter() возвращает false и Postgres Pro пропускает эту запись. В противном случае возвращается true, и векторные операторы будут проверять значение filter_mask для отдельных элементов вектора в последующих операциях.

  • Применяя векторные булевы операторы, вы должны явно приводить строковые константы к требуемому типу данных.

F.83.6.3. Функции сравнения #

ФункцияОписание
bitwixt(x,low,high)То же, что и предикат x BETWEEN low AND high.
is_null(x)То же, что и предикат выражение IS NULL.
is_not_null(x)То же, что и предикат выражение IS NOT NULL.
ifnull(x,subst)То же, что и функция COALESCE.

F.83.6.4. Функции преобразования #

create_projection(projection_name text, source_table regclass, vector_columns text[], scalar_columns text[] DEFAULT null, order_by text DEFAULT null) #

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

Аргументы:

  • projection_name — имя таблицы-проекции.

  • source_table — исходная таблица Postgres Pro, из которой будет создаваться векторизованная проекция.

  • vector_columns — массив имён столбцов, содержимое которых будет сохранено в плитках vops.

  • scalar_columns — массив имён столбцов, тип которых останется скалярным. По значениям этих столбцов строки таблицы будут группироваться в плитки vops. По таким столбцам vops автоматически строит индекс BRIN.

  • order_by — необязательное указание векторного столбца, по которому могут сортироваться данные в таблице-проекции. Сортировка позволяет инкрементальным образом обновлять в проекции данные, изменённые в исходной странице, с использованием функции pname_refresh(). В этом столбце должны содержаться уникальные значения времени, например, даты продаж. Расширение vops автоматически строит индексы BRIN по функциям first() и last() со значениями этого столбца.

Примеры:

SELECT create_projection('vops_lineitem','lineitem',array['l_shipdate','l_quantity','l_extendedprice','l_discount','l_tax'],array['l_returnflag','l_linestatus']);
проекция_refresh() #

Обновляет таблицу-проекцию с именем проекция.

Примеры:

Обновление проекции vops_lineitem:

SELECT vops_lineitem_refresh();
drop_projection(projection_name text) #

Удаляет указанную проекцию и соответствующую функцию обновления, а также удаляет информацию об этой проекции из таблицы vops_projections.

Примеры:

Удаление проекции vops_lineitem:

SELECT drop_projection(vops_lineitem);
populate(destination regclass, source regclass, predicate cstring DEFAULT null, sort cstring DEFAULT null) #

Копирует данные из существующей таблицы в её векторизованную проекцию и возвращает число импортированных строк (bigint).

Аргументы:

  • destination — целевая таблица, в которую будут копироваться данные.

    Тип: regclass

  • source — исходная таблица, из которой будут копироваться данные.

    Тип: regclass

  • predicate — ограничивает множество импортируемых данных (необязательный аргумент). Используя этот аргумент, вы можете импортировать только самые последние записи.

    Тип: cstring

  • sort — определяет порядок сортировки данных, в котором они будут загружаться (необязательный аргумент).

    Тип: cstring

Примеры:

SELECT populate(destination := 'vops_lineitem'::regclass, source := 'lineitem'::regclass);
SELECT populate(destination := 'vops_lineitem_projection'::regclass, source := 'lineitem_projection'::regclass, sort := 'l_returnflag,l_linestatus');
import(destination regclass, csv_path cstring, separator cstring DEFAULT ',' skip integer DEFAULT 0) #

Загружает данные в таблицу vops непосредственно из файла CSV, пропуская этап создания обычной таблицы, и возвращает число импортированных строк (bigint). Используйте эту функцию, чтобы не хранить две копии одних и тех же данных.

Аргументы:

  • destination — целевая таблица, в которую будут копироваться данные.

    Тип: regclass

  • csv_path — путь к файлу CSV, из которого будут загружаться данные.

    Тип: cstring

  • separator — задаёт разделитель полей, используемый в файле CSV (необязательный аргумент). По умолчанию разделителем полей считается запятая.

    Тип: cstring

  • skip — задаёт количество строк в заголовке CSV, которые не нужно импортировать (необязательный аргумент).

    По умолчанию: 0

    Тип: cstring

Примеры:

SELECT import(destination := 'vops_lineitem'::regclass, csv_path := '/mnt/data/lineitem.csv', separator := '|');
vops_unnest(anyelement) #

Разбивает записи с типами vops на записи со скалярными типами.

F.83.6.5. Параметры конфигурации #

vops.auto_substitute_projections (boolean) #

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

По умолчанию: off

F.83.7. Авторы #

Postgres Professional, Москва, Россия