Как обеспечить уникальность двух столбцов в пределах третьего?

Дана таблица с тремя столбцами:

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.

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

ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
0
0
61
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Пристегнитесь. Это продвинутое решение.
Сначала установите два дополнительных модуля (по одному на каждую базу данных): 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 или более поздняя версия. Видеть:

Здесь мы рискуем. Я действительно не совсем уверен, что понял правила, которые вы пытаетесь обеспечить...

Erwin Brandstetter 24.05.2024 01:04

Проблема в том, что если в строке есть, например, (1, 2), а в другой — (3, 1), я бы также хотел, чтобы она была недействительной, поскольку 1 распределяется между столбцами :( Кроме того, спасибо за помощь !

Daniel 24.05.2024 21:33

@Daniel: Думаю, я наконец понял, что тебе нужно. Я отредактировал вопрос, чтобы уточнить его (и снова открыл его), и добавил ответ. Не могли бы вы подтвердить, правильно ли я понял вопрос и ответ?

Erwin Brandstetter 25.05.2024 03:37

да!! это именно та проблема, которую я пытаюсь решить. Спасибо большое за решение и объяснение. Единственная проблема, которая у меня осталась, заключается в том, что bot col1 и col2 являются текстовыми типами, а не целыми числами. Есть ли эквивалент для строк, которые можно было бы использовать? Я пробовал использовать pg_trgm и gist_trgm_ops, но, похоже, это не сработало. В любом случае большое спасибо за решение!

Daniel 27.05.2024 18:13

Основываясь на другом решении, которое вы прокомментировали, я также попробовал выполнить array_remove(ARRAY[hashtext(col1), hashtext(col2)], null) gist__int_ops With &&); Однако, похоже, ограничение соблюдается неправильно.

Daniel 27.05.2024 18:25

Неважно, это сработало с пустой базой данных! Я думаю, что моя проблема связана с тем, что это миграция Rails, и ActiveRecord автоматически типизирует вещи, которые не должны быть типизированы. Большое спасибо за помощь @Erwin!

Daniel 27.05.2024 19:03

@Дэниел! Большой! Я добавил больше альтернатив.

Erwin Brandstetter 27.05.2024 22:48

Другие вопросы по теме