Проверки уникальности индексов
PostgreSQL применяет ограничения уникальности SQL с помощью уникальных индексов - индексов, которые не допускают наличия нескольких записей с одинаковыми ключами. Метод доступа, поддерживающий эту функцию, устанавливает значение amcanunique
в true
. (В настоящее время только b-tree поддерживает эту функцию.) Столбцы, перечисленные в предложении INCLUDE, не учитываются при обеспечении уникальности.
Из-за MVCC всегда необходимо допускать физическое существование дубликатов записей в индексе: записи могут относиться к последовательным версиям одного логического ряда. На самом деле мы хотим добиться того, чтобы ни один снимок MVCC не включал две строки с одинаковыми индексными ключами. Это разбивается на следующие случаи, которые необходимо проверять при вставке новой строки в уникальный индекс:
- Если конфликтующий действительный ряд был удален текущей транзакцией, все в порядке. (В частности, поскольку
UPDATE
всегда удаляет старую версию ряда перед вставкой новой в ерсии, это позволит выполнитьUPDATE
ряда без изменения ключа). - Если конфликтующая строка была вставлена транзакцией, которая еще не зафиксирована, будущий «вставщик» должен подождать, пока эта транзакция не зафиксируется. Если она откатится назад, то конфликта не будет. Если же она зафиксируется без повторного удаления конфликтующего ряда, то имеет место нарушение уникальности. (На практике мы просто ждем окончания другой транзакции, а затем заново выполняем проверку видимости).
- Аналогично, если конфликтующий действительный ряд был удален еще не зафиксированной транзакцией, потенциальный «вставщик» должен дождаться фиксации или прерывания этой транзакции, а затем повторить проверку.
Кроме того, непосредственно перед тем, как сообщить о нарушении уникальности в соответствии с вышеуказанными правилами, метод доступа должен перепроверить актуальность вставляемой строки. Если он зафиксирован как мертвый, то о нарушении сообщать не следует. (Этот случай не может произойти в обычном сценарии вставки строки, которая только что была создана текущей транзакцией. Однако это может произойти во время CREATE UNIQUE INDEX CONCURRENTLY
).
Мы требуем, чтобы метод доступа к индексу сам применял эти тесты, а это значит, что он должен залезть в кучу, чтобы проверить статус фиксации любого ряда, который, судя по содержимому индекса, имеет дублирующийся ключ. Это, несомненно, уродливо и немодульно, но позволяет сэкономить лишнюю работу: если бы мы делали отдельный зонд, то поиск индекса для конфликтующего ряда по сути повторялся бы, пока мы находили бы место для вставки записи индекса нового ряда. Более того, нет очевидного способа избежать условий гонки, если проверка конфликта не является неотъемлемой частью вставки новой индексной записи.
Если ограничение уникальности откладывается, возникает дополнительная сложность: нам нужно иметь возможность вставить индексную запись для нового ряда, но отложить любую ошибку нарушения уникальности до конца оператора или даже позже.
Чтобы избежать ненужных повторных поисков в индексе, метод доступа к индексу должен выполнять предварительную проверку уникальности при первоначальной вставке. Если она покажет, что конфликтующих кортежей точно нет, мы закончили. В противном случае мы запланируем повторную проверку, когда придет время применить ограничение. Если во время перепроверки и вставленный кортеж, и какой-то другой кортеж с тем же ключом окажутся живыми, то об ошибке нужно сообщить. (Обратите внимание, что для этой цели «живой» на самом деле означает «любой кортеж в цепочке HOT записи индекса является живым»). Чтобы реализовать это, функции aminsert
передается параметр checkUnique
, имеющий одно из следующих значений:
-
UNIQUE_CHECK_NO
указывает, что проверка уникальности не должна производиться (это не уникальный индекс). -
UNIQUE_CHECK_YES
указывает, что это неоткладываемый уникальный индекс и проверку уникальности нужно выполнить немедленно, как описано выше. -
UNIQUE_CHECK_PARTIAL
указывает на то, что ограничение уникальности является отложенным. PostgreSQL будет использовать этот режим для вставки записи индекса каждого ряда. Метод доступа должен допускать дублирование записей в индексе и сообщать о возможных дубликатах, возвращаяfalse
из aminsert. Для каждого ряда, для которого возвращаетсяfalse
, будет зап ланирована отложенная перепроверка.Метод доступа должен выявить все строки, которые могут нарушать ограничение уникальности, но не будет ошибкой, если он сообщит о ложных срабатываниях. Это позволяет выполнить проверку, не дожидаясь завершения других транзакций; конфликты, о которых сообщается в этом случае, не рассматриваются как ошибки и будут перепроверены позже, и к тому времени они уже могут перестать быть конфликтами.
-
UNIQUE_CHECK_EXISTING
указывает, что это отложенная перепроверка строки, о которой было сообщено как о потенциальном нарушении уникальности. Хотя это реализуется вызовомaminsert
, метод доступа не должен вставлять новую индексную запись в этом случае. Индексная запись уже присутствует. Скорее, метод доступа должен проверить, есть ли еще одна действующая индексная запись. Если да, и если целевая строка также еще жива, сообщите об ошибке.Рекомендуется, чтобы при вызове
UNIQUE_CHECK_EXISTING
метод доступа дополнительно проверял, что целевая строка действительно имеет существующую запись в индексе, и сообщал об ошибке, если это не так. Это хорошая идея, потому чт о значения кортежей индекса, переданные вaminsert
, будут вычислены заново. Если определение индекса включает функции, которые на самом деле не являются неизменяемыми, мы можем проверять не ту область индекса. Проверка того, что целевая строка найдена при повторной проверке, подтверждает, что мы сканируем те же значения кортежей, которые использовались при первоначальной вставке.