Уникальные ограничения — классическая функция реляционной базы данных, которая обеспечивает уникальность столбца или группы столбцов во время ввода данных или построения индекса. Они могут быть указаны с помощью ключевых слов UNIQUE / PRIMARY KEY. Уникальные индексы — сущности, которые их поддерживают. Хотя такие ограничения всегда можно было указать в heap-таблицах, они не поддерживались в append optimized таблицах (AO/CO). 

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

Синтаксис

-- Adding unique constraints while creating a table
CREATE TABLE foo(i int UNIQUE) USING ao_row;
CREATE TABLE bar(i int PRIMARY KEY) USING ao_column;
CREATE TABLE foo2(i int, CONSTRAINT iuniq UNIQUE(i));
CREATE TABLE bar2(i int, CONSTRAINT ipk PRIMARY KEY(i));

-- Adding unique constraints post-table creation
CREATE TABLE baz(i int) with (appendonly=true);
CREATE UNIQUE INDEX on baz(i);

CREATE TABLE foobar(i int) USING ao_row;
ALTER TABLE foobar ADD CONSTRAINT UNIQUE (i);
OR
ALTER TABLE foobar ADD PRIMARY KEY (i);

-- We will now be able to transform a heap table with
-- unique index -> AO/CO table with unique index
CREATE TABLE foobaz(i int unique) using heap;
ALTER TABLE foobaz set access method ao_row; 

Разрешение конфликтов во время построения индекса

Если уникальные индексы создаются на основе таблицы, уже содержащей данные, механизм предельно прост. ShareLock применяется к таблице на протяжении всей операции, предотвращающая одновременное изменение. Выполняется полное последовательное сканирование таблицы, которое информирует модуль B-Tree о кортежах, которые необходимо проиндексировать (мертвые кортежи не индексируются). Для создания записей индекса значения ключей индекса, полученные в результате сканирования, помещаются в файл сортировки. Конфликт обнаруживается во время последующих сравнений сортировки: если есть какие-либо одинаковые ключи, выдаётся сообщение об ошибке.

CREATE TABLE foo(i int) USING ao_row;
INSERT INTO foo VALUES(1);
INSERT INTO foo VALUES(1);

CREATE UNIQUE INDEX on foo(i);
ERROR:  could not create unique index "foo_i_idx"
DETAIL:  Key (i)=(1) is duplicated.

«DWH на основе GreenPlum»

Разрешение конфликтов при приёме данных

Ключевые сценарии

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

CREATE TABLE foo(i int UNIQUE) USING ao_row;
-- reveals unique index created on foo
\d+ foo
                                    Table "public.foo"
 Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
--------+---------+-----------+----------+---------+---------+--------------+-------------
 i      | integer |           |          |         | plain   |              | 
Compression Type: None
Compression Level: 0
Block Size: 32768
Checksum: t
Indexes:
    "foo_i_key" UNIQUE CONSTRAINT, btree (i)
Distributed by: (i)
Access method: ao_row
-- Case 0: no conflict as key doesn't exist in the table
-- succeeds
INSERT INTO foo VALUES(0);
-- Case 1: conflict against committed tuple
INSERT INTO foo VALUES(1);
-- raises conflict
INSERT INTO foo VALUES(1);
ERROR:  duplicate key value violates unique constraint "foo_i_key"  (seg1 ...)
DETAIL:  Key (i)=(1) already exists.
-- Case 2: conflict against aborted tuple
BEGIN;
INSERT INTO foo VALUES(2);
ABORT;

-- should succeed
INSERT INTO foo VALUES(2);
-- Case 3: conflict against row inserted by in-progress transaction

-- Session 1:
BEGIN;
INSERT INTO foo VALUES(3);

-- Session 2:
-- will block until transaction in session 1 commits/aborts
INSERT INTO foo VALUES(3);

-- Session 1:
COMMIT;

-- Session 2:
-- Conflict raised.
ERROR:  duplicate key value violates unique constraint "foo_i_key"  (seg0 ...)
DETAIL:  Key (i)=(3) already exists.
-- Case 4: Conflict against a deleted tuple
DELETE FROM foo WHERE i = 1;
-- should succeed
INSERT INTO foo VALUES(1);

Общий механизм

Для каждого кортежа, подлежащего операции INSERT или подобной INSERT, он сначала вставляется в таблицу, а затем сразу после этого проверяется на уникальность. Если тест на уникальность пройдён — отлично. В противном случае возникнет ошибка нарушения уникальности ERROR, что приведёт к прерыванию транзакции, и кортеж станет невидимым благодаря MVCC.

Первый шаг проверки уникальности — посмотреть, есть ли у нас вообще запись индекса для ключа рассматриваемого кортежа. Если кортеж с таким ключевым значением никогда не вставлялся (кейс 0), то шансов для конфликта нет. Если мы найдём запись индекса, но это не обязательно означает наличие конфликтующего кортежа — мы можем легко получить кейс 2, 3 или 4 (приведены выше). В этих случаях записи в реальном индексе указывают на невидимые кортежи, поскольку очистка индекса откладывается до vacuum-time. Второй шаг — выполнить проверку видимости кортежа. Если найдём запись индекса, то получим TID (идентификатор кортежа) из записи индекса, а затем выполним проверку видимости этого tid. Если кортеж виден, в зависимости от степени видимости, у нас есть потенциальный конфликт (кейс 3) или существующий конфликт (кейс 1). Если кортеж не виден (кейсы 2 и 4), то конфликта нет.

Используем «грязный» снапшот, чтобы «увидеть» во время вставки кортежа, есть ли какие-либо кортежи, соответствующие его ключу. Например:

  • вставлены совершённой транзакцией (кейс 1);

  • вставлены прерванной транзакцией (кейс 2);

  • вставлены незавершенной транзакцией (кейс 3)

  • удалены в результате совершённой транзакции (кейс 4)

Есть и другие кейсы, но они либо не применяются к таблицам AO/CO, либо тривиальны для обсуждения. 

Heap tables

Проверка видимости кортежей в heap-таблицах проста, поскольку вся информация о видимости хранится в самом кортеже (xmin и xmax). Всё, что нам нужно сделать, это «выбрать» кортеж по его tid и сравнить xmin/xmax со снапшотом.

-- Setup
CREATE EXTENSION pageinspect;
-- Case 1: conflict against committed tuple
CREATE TABLE foo(i int UNIQUE) USING heap DISTRIBUTED REPLICATED;
INSERT INTO foo VALUES(1);

-- In utility mode on seg0, run inspection functions:

SELECT * FROM bt_page_items('foo_i_key', 1);
 itemoffset | ctid  | itemlen | nulls | vars |          data           
------------+-------+---------+-------+------+-------------------------
          1 | (0,1) |      16 | f     | f    | 01 00 00 00 00 00 00 00
(1 row)

-- Since we have a live index entry for i = 1 pointing to tid `(0,1)`
-- any insert for key i = 1 will have to determine tuple visibility.

SELECT
    ctid, i,
    txid_status(xmin::text::bigint) AS inserting_xid_status,
    txid_status(xmax::text::bigint) AS deleting_xid_status
FROM foo;
 ctid  | i | inserting_xid_status | deleting_xid_status 
-------+---+----------------------+---------------------
 (0,1) | 1 | committed            | 
(1 row)

-- Since the tuple has its xmin committed, it is visible and the following
-- will raise a conflict.
INSERT INTO foo VALUES(1);
ERROR:  duplicate key value violates unique constraint "foo_i_key"  (seg1 ...)
DETAIL:  Key (i)=(1) already exists.
-- Case 2: conflict against aborted tuple
CREATE TABLE foo(i int UNIQUE) USING heap DISTRIBUTED REPLICATED;
BEGIN;
INSERT INTO foo VALUES(2);
ABORT;

-- In utility mode on seg0, run inspection functions:

SELECT * FROM bt_page_items('foo_i_key', 1);
 itemoffset | ctid  | itemlen | nulls | vars |          data           
------------+-------+---------+-------+------+-------------------------
          1 | (0,1) |      16 | f     | f    | 02 00 00 00 00 00 00 00
(1 row)

-- Since we have a live index entry for i = 1 pointing to tid `(0,2)`
-- any insert for key i = 1 will have to determine tuple visibility.

SET gp_select_invisible TO ON;
SELECT
    ctid, i,
    txid_status(xmin::text::bigint) AS inserting_xid_status,
    txid_status(xmax::text::bigint) AS deleting_xid_status
FROM foo;
 ctid  | i | inserting_xid_status | deleting_xid_status 
-------+---+----------------------+---------------------
 (0,1) | 2 | aborted              | 
(1 row)

-- Since the tuple has its xmin aborted, it is invisible and the following
-- will succeed.
INSERT INTO foo VALUES(2);
-- Case 3: conflict against row inserted by in-progress transaction
CREATE TABLE foo(i int UNIQUE) USING heap DISTRIBUTED REPLICATED;

-- Session 1:
BEGIN;
INSERT INTO foo VALUES(3);

-- Session 3: (for snooping) - in utility mode on segment 0 
SELECT * FROM bt_page_items('foo_i_key', 1);
 itemoffset | ctid  | itemlen | nulls | vars |          data           
------------+-------+---------+-------+------+-------------------------
          1 | (0,1) |      16 | f     | f    | 03 00 00 00 00 00 00 00
(1 row)

-- Since we have a live index entry for i = 3 pointing to tid `(0,1)`
-- any insert for key i = 3 will have to determine tuple visibility.

SELECT
    ctid, i,
    txid_status(xmin::text::bigint) AS inserting_xid_status,
    txid_status(xmax::text::bigint) AS deleting_xid_status
FROM foo;
SET
 ctid  | i | inserting_xid_status | deleting_xid_status 
-------+---+----------------------+---------------------
 (0,1) | 3 | in progress          | 
(1 row)

-- Session 2:
-- Since the tuple has its xmin in-progress, meaning its inserting
-- transaction is still running, the backend will go into an xwait
-- state to wait for the inserting transaction to complete.
INSERT INTO foo VALUES(3); -- will block

-- Session 1:
COMMIT;

-- Session 2:
-- Since Session 1 committed, Session 2 will wake up and retry the
-- insert and will raise a conflict.
ERROR:  duplicate key value violates unique constraint "foo_i_key"  (seg0 ...)
DETAIL:  Key (i)=(3) already exists.
-- Case 4: Conflict against a deleted tuple
CREATE TABLE foo(i int UNIQUE) USING heap DISTRIBUTED REPLICATED;
INSERT INTO foo VALUES(1);
DELETE FROM foo WHERE i = 1;

-- In utility mode on seg0, run inspection functions:

SELECT * FROM bt_page_items('foo_i_key', 1);
 itemoffset | ctid  | itemlen | nulls | vars |          data           
------------+-------+---------+-------+------+-------------------------
          1 | (0,1) |      16 | f     | f    | 01 00 00 00 00 00 00 00
(1 row)

-- Since we have a live index entry for i = 1 pointing to tid `(0,1)`
-- any insert for key i = 1 will have to determine tuple visibility.

SELECT
    ctid, i,
    txid_status(xmin::text::bigint) AS inserting_xid_status,
    txid_status(xmax::text::bigint) AS deleting_xid_status
FROM foo;
SET
 ctid  | i | inserting_xid_status | deleting_xid_status 
-------+---+----------------------+---------------------
 (0,1) | 1 | committed            | committed
(1 row)

-- Since the tuple has its xmax committed, it is deleted and invisible,
-- and thus the following will succeed.
INSERT INTO foo VALUES(1);

AO/CO-таблицы

Кортежи AO/CO не размещают метаданные видимости совместно со своими фактическими данными. Это значит, что они не хранят внутри себя поля xmin/xmax, что затрудняет проверку видимости. Чтобы проверить видимость, нам нужно посмотреть на их вспомогательные связи (каталог блоков и карту видимости), которые сами являются heap-таблицами.

Block directory

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

Одна строка каталога блоков содержит информацию для диапазона кортежей данных. xmin кортежа каталога блока эквивалентен xmin кортежа данных. Мы можем определить видимость кортежа (для кейсов 1,2,3), просто взглянув на поля xmin и xmax строки каталога блоков. По сути, нам даже не нужно сканировать кортеж данных с диска — если каталог блоков сообщает нам, что в каком-то varblock или segfile есть кортеж, нам этого достаточно. 

-- Case 1: conflict against committed tuple
CREATE TABLE foo(i int UNIQUE) USING ao_row DISTRIBUTED REPLICATED;
INSERT INTO foo VALUES(1);

-- In utility mode on seg0, run inspection functions:

SELECT * FROM bt_page_items('foo_i_key', 1);
 itemoffset | ctid  | itemlen | nulls | vars |          data           
------------+-------+---------+-------+------+-------------------------
          1 | (0,2) |      16 | f     | f    | 01 00 00 00 00 00 00 00
(1 row)


-- Since we have a live index entry for i = 1 pointing to tid `(0,2)`
-- any insert for key i = 1 will have to determine tuple visibility.

-- First obtain the name of the block directory relation
SELECT oid::regclass FROM pg_class WHERE oid =
    (SELECT blkdirrelid FROM pg_appendonly WHERE relid = 'foo'::regclass);
             oid             
-----------------------------
 pg_aoseg.pg_aoblkdir_442697
(1 row)

-- Then inspect the transaction status of the block directory row that
-- covers the data row.
SELECT
    ctid, segno, first_row_no, 
    txid_status(xmin::text::bigint) AS inserting_xid_status,
    txid_status(xmax::text::bigint) AS deleting_xid_status
FROM pg_aoseg.pg_aoblkdir_442697;
 ctid  | segno | first_row_no | inserting_xid_status | deleting_xid_status 
-------+-------+--------------+----------------------+---------------------
 (0,2) |     0 |            1 | committed            | 
(1 row)

-- Since the block directory row has its xmin committed, it is visible and
-- by extension so is the data tuple. So, this will raise a conflict.
INSERT INTO foo VALUES(1);
ERROR:  duplicate key value violates unique constraint "foo_i_key"  (seg1 ...)
DETAIL:  Key (i)=(1) already exists.

Аналогично для кейсов 2 и 3 кортеж каталога блоков будет иметь статусы «прервано» и «в процессе» соответственно. И мы получим поведение, аналогичное heap-кортежам.

Block directory internals

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

Как показано на диаграмме, для команды вставки оптимального размера мы можем получить несколько строк каталога блоков. Из значений first_row_no можем вывести диапазон строк, охватываемых строкой каталога блоков, в сегменте файла segno. Если мы увеличим масштаб строки каталога блоков, то увидим, что в ней есть двоичный столбец: minipage. Мини-страница хранит максимум 161 запись каталога блоков. Каждая запись сообщает нам диапазон, который она охватывает, и смещение.

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

  • в конце вставки (не в конце транзакции!)

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

Итак, есть окна, благодаря которым кортежи попали в таблицу, а их строки каталога блоков — нет. Что бы произошло, если в этом окне была выполнена проверка уникального индекса? Он не нашел бы ни одной строки записи каталога блоков для кортежа и ошибочно завершил бы операцию (правильным поведением было бы дождаться завершения транзакции вставки для прерывания / фиксации). 

Здесь на помощь приходит механизм placeholder entry (ввода плейсхолдера).

Placeholder block directory entries

В начале команды вставки создаём запись-заполнитель для файла segno. Затем сохраняем строку, содержащую эту запись.

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

Карта видимости

Мы ещё не говорили об удалении. Когда кортеж удаляется из таблицы AO/CO, подобно heap-таблицам, он физически не удаляется из рефайла. Физическое удаление откладывается до VACUUM. Физическое удаление записи индекса для кортежа также откладывается до VACUUM. Строка карты видимости записывает метаданные, сообщающие нам, что кортеж был удалён. Запись каталога блоков для этой строки не удаляется. Как и строки каталога блоков, одна строка visimap охватывает несколько строк данных. Итак, если у нас есть видимая строка visimap, покрывающая данную строку данных, значит, строка данных удалена (обратите внимание на противоположную семантику).

Таким образом, при выполнении проверки уникального индекса, даже если у нас есть видимая строка каталога блоков, необязательно возникает конфликт. Строка могла быть удалена. Итак, нам нужно выполнить дополнительный поиск по visimap.

-- Case 4: Conflict against a deleted tuple
CREATE TABLE foo(i int UNIQUE) USING ao_row DISTRIBUTED REPLICATED;
INSERT INTO foo VALUES(1);
DELETE FROM foo WHERE i = 1;

-- In utility mode on seg0, run inspection functions:

SELECT * FROM bt_page_items('foo_i_key', 1);
 itemoffset | ctid  | itemlen | nulls | vars |          data           
------------+-------+---------+-------+------+-------------------------
          1 | (0,1) |      16 | f     | f    | 01 00 00 00 00 00 00 00
(1 row)

-- Since we have a live index entry for i = 1 pointing to tid `(0,1)`
-- any insert for key i = 1 will have to determine tuple visibility.

SELECT
    ctid, *,
    txid_status(xmin::text::bigint) AS inserting_xid_status,
    txid_status(xmax::text::bigint) AS deleting_xid_status
FROM pg_aoseg.pg_aovisimap_442709;
 ctid  | segno | first_row_no |        visimap         | inserting_xid_status | deleting_xid_status 
-------+-------+--------------+------------------------+----------------------+---------------------
 (0,1) |     0 |            0 | \x01000000000102000000 | committed            | 
(1 row)

-- Since the visimap tuple has its xmin committed, it is visible and thus
-- the data tuple it is covering is deleted. So the following succeeds.
INSERT INTO foo VALUES(1);

Взаимодействие с «ленивым вакуумом»

Lazy VACUUM (VACUUM без ключевого слова FULL) для таблиц AO/CO сильно отличается от heap-таблиц. В отличие от heap-таблиц, в таблицах AO/CO у кортежей изменены их ctid. Это связано с тем, что текущие кортежи фактически перемещаются из seg-файла в другой («вставляются» в новый seg-файл). Мёртвые кортежи остаются в исходном segfile, который в дальнейшем будет «сжат».

Конфликты в «живых» кортежах

Поскольку «ленивый» VACUUM может выполняться одновременно со вставками, нам нужно каким-то образом разрешать конфликты.

Но для начала разделим ленивый VACUUM на три фазы:

  • текущие кортежи перемещаются из одного сегментного файла в другой – при этом повторно используется механизм вставки (что означает, что будут сформированы новые кортежи данных, а также новые индексные кортежи, как при обычной вставке);

  • вставка завершается, и перемещённые кортежи сохраняют свои записи каталога блоков на диске;

  • старые индексные кортежи, указывающие на перемещённые кортежи, удаляются.

Проверки уникальности в рамках сеанса VACUUM излишни, так как у нас есть гарантия, что внутри перемещаемых «живых» кортежей нет конфликтов.

Если есть какие-либо конфликтующие вставки вплоть до третей фазы, старые записи индекса будут использоваться для поиска (они всегда будут иметь приоритет над новыми записями индекса из-за характера кода сортировки B-Tree). Это значит, что здесь не нужен placeholder-механизм. После второго этапа новые записи индекса и каталога блоков будут доступны для поиска конфликтов. 

Производительность

Влияние на загрузку данных

Наша цель в этом разделе — проверить производительность загрузки данных для следующих типов таблиц:

  • heap-таблица с неуникальным индексом;

  • heap-таблица с уникальным индексом;

  • AO с неуникальным индексом;

  • AO с уникальным индексом;

  • AOCO с неуникальным индексом;

  • AOCO с уникальным индексом.

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

Кейс 2: вставляем данные, которые конфликтуют с прерванными строками.

Кейс 4: вставляем данные, которые конфликтуют с удалёнными строками.

Мы хотим узнать, насколько сильно проверка уникальности влияет на приём данных. Нас не интересуют кейсы 1 и 3, поскольку они приводят к немедленному завершению и сценарию xwait соответственно, что делает их тривиальными с точки зрения производительности.

Кейс 0: вставляем ключи [1,100000000] в пустую таблицу с целочисленным столбцом, служащим уникальным ключом. 

Кейс 2: вставляем ключи [1,100000000] в таблицу с целочисленным столбцом, служащим уникальным ключом, в которой уже есть ключи [1,100000000], вставленные из прерванной транзакции. 

Кейс 4: вставляем ключи [1,100000000] в таблицу с целочисленным столбцом, служащим уникальным ключом, в которой уже есть ключи [1,100000000], удалённые из совершенной транзакции.

Анализ

Кейс 0 [лучший кейс]

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

Кейс 2 [худший кейс]

Здесь можем увидеть более выраженные различия. Если мы сравним со случаем 0, обнаружим, что существуют нетривиальные накладные расходы на обслуживание индекса (даже для неуникальных индексов). Почему? Потому что теперь в коде btree есть накладные расходы из-за ранее существовавших записей индекса — мы больше не можем идти по быстрому пути btree и добавлять ключи по порядку.

Видна существенная разница между неуникальными и уникальными индексами по всем направлениям. Это связано с дополнительными проверками видимости — теперь мы должны проверить кортежи. Проверки видимости для heap более эффективны, поскольку xmin совмещен с данными, тогда как для AO/CO это не так, и нам нужно искать строки каталога блоков. В этом случае таблицы AO/CO с уникальными индексами примерно в 1,3 раза дороже, чем heap-таблицы с уникальными индексами.

У таблиц AO и AOC примерно одинаковая производительность, поскольку при поиске по уникальному индексу используется только 1 столбец для выполнения поиска по каталогу блоков.

Кейс 4 [худший кейс]

Здесь мы также можем увидеть выраженные различия. Если мы сравним со случаем 0, обнаружим, что существуют нетривиальные накладные расходы на обслуживание индекса даже для неуникальных индексов. 

Опять же, здесь видна существенная разница между неуникальными и уникальными индексами по всем направлениям. Нет никакой разницы между кейсом 2 и кейсом 4 для heap-таблицы, так как в кейс 2 — это прерванный кейс xmin, а кейс 4 – совершённый кейс xmax.

Однако для таблиц AO/CO ещё больше накладных расходов по сравнению с кейсом 2. Это может быть связано с дополнительными поисками по visimap для определения, был ли удален кортеж или нет. В этом случае таблицы AO/CO с уникальными индексами примерно в 2 раза дороже, чем heap-таблицы с уникальными индексами.

Уникальные индексы и оптимизатор

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

  • ​​ndistinct = no_of_rows;

  • бессмысленно хранить MCV;

  • количество групп = no_of_rows для GROUP BY unique_index_column;

  • более точная избирательность соединения;

  • join elimination.

Они уже доступны для heap и готовы к работе с таблицами AO/CO, поскольку эти оптимизации не зависят от таблицы AM. Вот пример join elimination:

CREATE TABLE customer(first_name text, address_id int unique) USING ao_row;
CREATE TABLE address (address_id int unique, address text) USING ao_row;

EXPLAIN SELECT first_name
FROM customer c
LEFT JOIN address a ON c.address_id = a.address_id;

                                    QUERY PLAN                                     
-----------------------------------------------------------------------------------
 Gather Motion 3:1  (slice1; segments: 3)  (cost=0.00..860.67 rows=49600 width=32)
   ->  Seq Scan on customer c  (cost=0.00..199.33 rows=16533 width=32)
 Optimizer: Postgres query optimizer
(3 rows)

Вместо заключения: лучшие практики

  • Стремиться к кейсу 0 и делать всё, чтобы избежать кейсов 2 и 4. Кейс 1 приведёт к отказоустойчивости транзакции вставки.

  • Регулярный VACUUM уничтожает записи индекса, указывающие на мертвые строки как для таблиц AO/CO, так и для heap-таблиц. При этом кейсы 2 и 4 превращаются в кейс 0.

  • Очистка таблиц AO/CO имеет дополнительное преимущество, заключающееся в очистке вспомогательных отношений. Раздутые вспомогательные таблицы могут только увеличить время, необходимое для выполнения проверки уникальности.

  • Для таблиц AO/CO, если задания загрузки данных разделены таким образом, чтобы они не конфликтовали по ключу, мы можем избежать кейсов 2, 3 и 4.

  • xwaits из-за проверки индекса можно отслеживать с помощью:

-- ps display (on the segment hosts):
1294654 … postgres:  7003, ... sdw1(33842) con25 seg1 cmd6 MPPEXEC INSERT waiting

-- all backends on all segments that are stuck on a xwait
SELECT gp_execution_segment(), pid, sess_id, wait_event_type, wait_event, state,
backend_xid, query FROM gp_dist_random('pg_stat_activity')
    WHERE wait_event = 'transactionid' and wait_event_type = 'Lock';

-[ RECORD 1 ]--------+-------------------------
gp_execution_segment | 1
pid                  | 3099493
sess_id              | 747
wait_event_type      | Lock
wait_event           | transactionid
state                | active
backend_xid          | 17616
query                | insert into h values(1);


-- backend on coordinator running the query that is doing
-- xwait on some segment(s)
SELECT pid, sess_id, wait_event_type, wait_event, state,
query FROM pg_stat_activity WHERE sess_id = 
    (SELECT sess_id FROM gp_dist_random('pg_stat_activity') WHERE 
        wait_event = 'transactionid' and wait_event_type = 'Lock');
        
-[ RECORD 1 ]---+-------------------------
pid             | 3099446
sess_id         | 747
wait_event_type | IPC
wait_event      | Dispatch/Result
state           | active
query           | insert into h values(1);

«DWH на основе GreenPlum»

Комментарии (1)


  1. Ivan22
    07.04.2023 14:41
    +1

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

    Однако во что "join elimination" превратился