Дана таблица с тремя столбцами:
CREATE TABLE tbl
column_scope text
, col1 int
, col2 int
);
Я хочу обеспечить, чтобы значения в column_scope, col1, разделенные по col2, были уникальными в обоих столбцах, за исключением дубликатов в одной и той же строке.
То есть в пределах области никакие два значения в col1 и col2 не могут быть одинаковыми, за исключением NULL или того, чтобы они были одной и той же строкой.
Я попытался добавить уникальный индекс (column_scope, col1, col2), однако это не удалось, поскольку не учитывались недопустимые случаи, такие как:
row_1 -> (scope_1, x, NULL);
row_2 -> (scope_1, NULL, x);
или
row_1 -> (scope_1, x, y);
row_2 -> (scope_1, y, z);
Я также попытался добавить ограничение в таблицу, однако мне не удалось получить правильные условия, гарантирующие, что col1 и col2 не имеют общих значений для данного column_scope.
Это отличается от других вопросов, поскольку меня не волнует конкретная комбинация значений, меня волнует только то, чтобы значения не повторялись в разных столбцах, и все это в пределах третьего столбца.


Пристегнитесь. Это продвинутое решение.
Сначала установите два дополнительных модуля (по одному на каждую базу данных): intarray и btree_gist . Тогда мы сможем добиться этой цели с помощью единственного ограничения исключения:
CREATE EXTENSION intarray; -- required!
CREATE EXTENSION btree_gist; -- required!
CREATE TABLE tbl (
column_scope text NOT NULL
, col1 int -- can be null
, col2 int -- can be null
);
-- Add THIS exclusion constraint !!!
ALTER TABLE tbl ADD CONSTRAINT tbl_cross_col_unique
EXCLUDE USING gist (column_scope WITH =
, array_remove(ARRAY[col1, col2], null) gist__int_ops WITH &&);
По сути, ограничение исключения не позволяет «перекрывать» массивы, построенные из col1 и col2 для одного и того же column_scope.
Обратите внимание, что я предполагаю, что наберу integer вместо col1 и col2. Все просто. Для других типов нужно сделать больше...
Вы хотите разрешить нулевые значения в col1 и col2. Поэтому исключите их из ограничения исключения. Удачное совпадение: нулевые значения в любом случае не допускаются в сгенерированный массив.
Руководство по array_remove():
Сравнения выполняются с использованием семантики
IS NOT DISTINCT FROM, поэтому можно удалитьNULL.
Все становится на свои места.
Здесь есть много сложных деталей, касающихся типов данных, индексного метода, класса оператора, нулевых значений, оптимизации индекса... Но это выходит за рамки простого вопроса и входит в сферу платного консультирования.
Близкий случай с более подробной информацией:
Альтернативно вы можете объединить col1 и col2 в один столбец отдельной таблицы в отношении 1:n (и добавить тег для определения типа). Либо замените то, что у вас есть сейчас, либо дополнительно, просто чтобы обеспечить соблюдение вашего ограничения. Затем можно применить простое ограничение UNIQUE. Видеть:
Этот индекс выражения реализует индекс UNIQUE, где (1,2) и (2,1) для (col1, col2) считаются равными:
CREATE UNIQUE INDEX tbl_uni_idx ON tbl
(column_scope, GREATEST(col1, col2), LEAST(col1, col2));
Видеть:
Нулевые значения не считаются равными, если вы не добавите предложение NULLS NOT DISTINCT, для которого требуется Postgres 15 или более поздняя версия. Видеть:
Проблема в том, что если в строке есть, например, (1, 2), а в другой — (3, 1), я бы также хотел, чтобы она была недействительной, поскольку 1 распределяется между столбцами :( Кроме того, спасибо за помощь !
@Daniel: Думаю, я наконец понял, что тебе нужно. Я отредактировал вопрос, чтобы уточнить его (и снова открыл его), и добавил ответ. Не могли бы вы подтвердить, правильно ли я понял вопрос и ответ?
да!! это именно та проблема, которую я пытаюсь решить. Спасибо большое за решение и объяснение. Единственная проблема, которая у меня осталась, заключается в том, что bot col1 и col2 являются текстовыми типами, а не целыми числами. Есть ли эквивалент для строк, которые можно было бы использовать? Я пробовал использовать pg_trgm и gist_trgm_ops, но, похоже, это не сработало. В любом случае большое спасибо за решение!
Основываясь на другом решении, которое вы прокомментировали, я также попробовал выполнить array_remove(ARRAY[hashtext(col1), hashtext(col2)], null) gist__int_ops With &&); Однако, похоже, ограничение соблюдается неправильно.
Неважно, это сработало с пустой базой данных! Я думаю, что моя проблема связана с тем, что это миграция Rails, и ActiveRecord автоматически типизирует вещи, которые не должны быть типизированы. Большое спасибо за помощь @Erwin!
@Дэниел! Большой! Я добавил больше альтернатив.
Здесь мы рискуем. Я действительно не совсем уверен, что понял правила, которые вы пытаетесь обеспечить...