Уникальные ограничения — классическая функция реляционной базы данных, которая обеспечивает уникальность столбца или группы столбцов во время ввода данных или построения индекса. Они могут быть указаны с помощью ключевых слов UNIQUE / PRIMARY KEY. Уникальные индексы — сущности, которые их поддерживают. Хотя такие ограничения всегда можно было указать в heap-таблицах, они не поддерживались в append optimized таблицах (AO/CO).  В статье разберём, как уникальные индексы помогают принимать более эффективные решения по планированию. А также рассмотрим примеры базовых сценариев и объясним, как они обрабатываются.  Если уникальные индексы создаются на основе таблицы, уже содержащей данные, механизм предельно прост. ShareLock применяется к таблице на протяжении всей операции, предотвращающая одновременное изменение. Выполняется полное последовательное сканирование таблицы, которое информирует модуль B-Tree о кортежах, которые необходимо проиндексировать (мертвые кортежи не индексируются). Для создания записей индекса значения ключей индекса, полученные в результате сканирования, помещаются в файл сортировки. Конфликт обнаруживается во время последующих сравнений сортировки: если есть какие-либо одинаковые ключи, выдаётся сообщение об ошибке. «DWH на основе GreenPlum» Ниже приведены сценарии для иллюстрации ключевых функциональных возможностей, внутренних компонентов и производительности. Для каждого кортежа, подлежащего операции INSERT или подобной INSERT, он сначала вставляется в таблицу, а затем сразу после этого проверяется на уникальность. Если тест на уникальность пройдён — отлично. В противном случае возникнет ошибка нарушения уникальности ERROR, что приведёт к прерыванию транзакции, и кортеж станет невидимым благодаря MVCC. Первый шаг проверки уникальности — посмотреть, есть ли у нас вообще запись индекса для ключа рассматриваемого кортежа. Если кортеж с таким ключевым значением никогда не вставлялся (кейс 0), то шансов для конфликта нет. Если мы найдём запись индекса, но это не обязательно означает наличие конфликтующего кортежа — мы можем легко получить кейс 2, 3 или 4 (приведены выше). В этих случаях записи в реальном индексе указывают на невидимые кортежи, поскольку очистка индекса откладывается до vacuum-time. Второй шаг — выполнить проверку видимости кортежа. Если найдём запись индекса, то получим TID (идентификатор кортежа) из записи индекса, а затем выполним проверку видимости этого tid. Если кортеж виден, в зависимости от степени видимости, у нас есть потенциальный конфликт (кейс 3) или существующий конфликт (кейс 1). Если кортеж не виден (кейсы 2 и 4), то конфликта нет. Используем «грязный» снапшот, чтобы «увидеть» во время вставки кортежа, есть ли какие-либо кортежи, соответствующие его ключу. Например: вставлены совершённой транзакцией (кейс 1); вставлены прерванной транзакцией (кейс 2); вставлены незавершенной транзакцией (кейс 3) удалены в результате совершённой транзакции (кейс 4) Есть и другие кейсы, но они либо не применяются к таблицам AO/CO, либо тривиальны для обсуждения.  Проверка видимости кортежей в heap-таблицах проста, поскольку вся информация о видимости хранится в самом кортеже (xmin и xmax). Всё, что нам нужно сделать, это «выбрать» кортеж по его tid и сравнить xmin/xmax со снапшотом. Кортежи AO/CO не размещают метаданные видимости совместно со своими фактическими данными. Это значит, что они не хранят внутри себя поля xmin/xmax, что затрудняет проверку видимости. Чтобы проверить видимость, нам нужно посмотреть на их вспомогательные связи (каталог блоков и карту видимости), которые сами являются heap-таблицами. Поиск по индексу в таблицах AO/CO реализован с дополнительным уровнем косвенности. TID из записи индекса разбиваются на номер файла сегмента и номер строки, которые используются для поиска по индексу в отношении каталога блока. Результатом поиска является строка каталога блока, которая сообщает о смещении блока, содержащего кортеж. После этого мы можем извлечь кортеж, просканировав блок. Одна строка каталога блоков содержит информацию для диапазона кортежей данных. xmin кортежа каталога блока эквивалентен xmin кортежа данных. Мы можем определить видимость кортежа (для кейсов 1,2,3), просто взглянув на поля xmin и xmax строки каталога блоков. По сути, нам даже не нужно сканировать кортеж данных с диска — если каталог блоков сообщает нам, что в каком-то varblock или segfile есть кортеж, нам этого достаточно.  Аналогично для кейсов 2 и 3 кортеж каталога блоков будет иметь статусы «прервано» и «в процессе» соответственно. И мы получим поведение, аналогичное heap-кортежам. Прежде чем переходить к усложнению, давайте обсудим, как структурированы строки блочного каталога на диске. Как показано на диаграмме, для команды вставки оптимального размера мы можем получить несколько строк каталога блоков. Из значений first_row_no можем вывести диапазон строк, охватываемых строкой каталога блоков, в сегменте файла segno. Если мы увеличим масштаб строки каталога блоков, то увидим, что в ней есть двоичный столбец: minipage. Мини-страница хранит максимум 161 запись каталога блоков. Каждая запись сообщает нам диапазон, который она охватывает, и смещение. Теперь перейдём к усложнению — записи каталога блоков записываются в зависимости каталога блоков задолго до кортежа данных. Обычно они записаны: в конце вставки (не в конце транзакции!) несколько раз в течение команды для вставки оптимального размера (поскольку строки каталога блоков могут охватывать ограниченный диапазон кортежей, а varblocks — хранить ограниченное количество строк).  Итак, есть окна, благодаря которым кортежи попали в таблицу, а их строки каталога блоков — нет. Что бы произошло, если в этом окне была выполнена проверка уникального индекса? Он не нашел бы ни одной строки записи каталога блоков для кортежа и ошибочно завершил бы операцию (правильным поведением было бы дождаться завершения транзакции вставки для прерывания / фиксации).  Здесь на помощь приходит механизм placeholder entry (ввода плейсхолдера). В начале команды вставки создаём запись-заполнитель для файла segno. Затем сохраняем строку, содержащую эту запись. Это значит, что для данного segno все будущие строки, которые будут отображаться в этом segno из текущей команды в таких окнах, будут покрыты этой записью. Соответственно любые одновременные проверки уникальности, выполняемые в таких окнах, будут направляться в эту строку. И затем можно использовать её метаданные транзакций. Диаграммы показывают, как во время этих окон выполняется разрешение конфликтов. Мы ещё не говорили об удалении. Когда кортеж удаляется из таблицы AO/CO, подобно heap-таблицам, он физически не удаляется из рефайла. Физическое удаление откладывается до VACUUM. Физическое удаление записи индекса для кортежа также откладывается до VACUUM. Строка карты видимости записывает метаданные, сообщающие нам, что кортеж был удалён. Запись каталога блоков для этой строки не удаляется. Как и строки каталога блоков, одна строка visimap охватывает несколько строк данных. Итак, если у нас есть видимая строка visimap, покрывающая данную строку данных, значит, строка данных удалена (обратите внимание на противоположную семантику). Таким образом, при выполнении проверки уникального индекса, даже если у нас есть видимая строка каталога блоков, необязательно возникает конфликт. Строка могла быть удалена. Итак, нам нужно выполнить дополнительный поиск по visimap. 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, обнаружим, что существуют нетривиальные накладные расходы на обслуживание индекса (даже для неуникальных индексов). Почему? Потому что теперь в коде btree есть накладные расходы из-за ранее существовавших записей индекса — мы больше не можем идти по быстрому пути btree и добавлять ключи по порядку. Видна существенная разница между неуникальными и уникальными индексами по всем направлениям. Это связано с дополнительными проверками видимости — теперь мы должны проверить кортежи. Проверки видимости для heap более эффективны, поскольку xmin совмещен с данными, тогда как для AO/CO это не так, и нам нужно искать строки каталога блоков. В этом случае таблицы AO/CO с уникальными индексами примерно в 1,3 раза дороже, чем heap-таблицы с уникальными индексами. У таблиц AO и AOC примерно одинаковая производительность, поскольку при поиске по уникальному индексу используется только 1 столбец для выполнения поиска по каталогу блоков. Здесь мы также можем увидеть выраженные различия. Если мы сравним со случаем 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: Стремиться к кейсу 0 и делать всё, чтобы избежать кейсов 2 и 4. Кейс 1 приведёт к отказоустойчивости транзакции вставки. Регулярный VACUUM уничтожает записи индекса, указывающие на мертвые строки как для таблиц AO/CO, так и для heap-таблиц. При этом кейсы 2 и 4 превращаются в кейс 0. Очистка таблиц AO/CO имеет дополнительное преимущество, заключающееся в очистке вспомогательных отношений. Раздутые вспомогательные таблицы могут только увеличить время, необходимое для выполнения проверки уникальности. Для таблиц AO/CO, если задания загрузки данных разделены таким образом, чтобы они не конфликтовали по ключу, мы можем избежать кейсов 2, 3 и 4. xwaits из-за проверки индекса можно отслеживать с помощью: «DWH на основе GreenPlum»  Редактор в MobileUp