Создать ограничение PostgreSQL для предотвращения уникальных комбинаций строк

9

Представьте, что у вас есть простая таблица:

name | is_active
----------------
A    | 0
A    | 0
B    | 0
C    | 1
...  | ...

Мне нужно создать специальное уникальное ограничение, которое не выполняется в следующей ситуации: разные is_activeзначения не могут сосуществовать для одного и того же nameзначения.

Пример допустимого условия:

Примечание: простой многостолбцовый уникальный индекс не допускает такой комбинации.

A    | 0
A    | 0
B    | 0

Пример допустимого условия:

A    | 0
B    | 1

Пример сбойного условия:

A    | 0
A    | 1
-- should be prevented, because `A 0` exists
-- same name, but different `is_active`

В идеале мне нужно уникальное ограничение или уникальный частичный индекс. Триггеры для меня более проблематичны.

Двойной A,0разрешен, но (A,0) (A,1)нет.

Андрей Скалюк
источник

Ответы:

17

Вы можете использовать исключение ограничение с btree_gist,

-- This is needed
CREATE EXTENSION btree_gist;

Затем мы добавляем ограничение, которое говорит:

«Мы не можем иметь 2 строки, которые имеют одинаковые nameи разные is_active» :

ALTER TABLE table_name
  ADD CONSTRAINT only_one_is_active_value_per_name
    EXCLUDE  USING gist
    ( name WITH =, 
      is_active WITH <>      -- if boolean, use instead:
                             -- (is_active::int) WITH <>
    );

Некоторые заметки:

  • is_activeможет быть целым или логическим, не имеет значения для ограничения исключения. (на самом деле это так, если столбец логический, вам нужно использовать (is_active::int) WITH <>.)
  • Строки, где nameили is_activeравно нулю, будут игнорироваться ограничением и, таким образом, разрешены.
  • Ограничение имеет смысл, только если в таблице больше столбцов. В противном случае, если в таблице есть только эти 2 столбца, UNIQUEограничение на (name)одиночку будет проще и целесообразнее. Я не вижу причин для хранения нескольких одинаковых строк.
  • Дизайн нарушает 2НФ. Хотя ограничение исключения спасет нас от аномалий обновления, оно не может быть связано с проблемами производительности. Если у вас есть, например, 1000 строк с name = 'A'и вы хотите обновить статус is_active с 0 до 3, все 1000 должны быть обновлены. Вы должны проверить, будет ли нормализация дизайна более эффективной. (Нормализующее значение в этом случае, чтобы удалить статус is_active из таблицы и добавить таблицу из 2 столбцов с именем, is_active и уникальным ограничением (name). Если is_activeэто логическое значение, оно может быть полностью удалено, а дополнительная таблица - просто таблица из одного столбца, сохраняя только "активные" имена.)
ypercubeᵀᴹ
источник
is_active не может быть логическим,ERROR: data type boolean has no default operator class for access method "gist"
Эван Кэрролл
1
@EvanCarroll Я не могу вспомнить, насколько хорошо я это проверял, когда писал. Но это работает с intи smallint.
ypercubeᵀᴹ
Также работает, используя, EXCLUDE USING gist (name WITH =, (is_active::int) WITH <>)если это логическое значение. И вопрос есть 0и 1, нет, trueи falseпоэтому довольно маловероятно, что я тестировал с
булевыми
Хорошо, я использовал ограничение исключения для dba.stackexchange.com/a/175922/2639, и у меня возникла проблема с использованием логических значений, поэтому я начал поиск. Я думал, что btree_gist покрывает bools, но это не так.
Эван Кэрролл
3

Это не тот случай, когда вы можете использовать уникальный индекс. Вы можете проверить условие в триггере, например:

create or replace function a_table_trigger()
returns trigger language plpgsql as $$
declare
    active int;
begin
    select is_active into active
    from a_table
    where name = new.name;

    if found and active is distinct from new.is_active then
        raise exception 'The value of is_active for "%" should be %', new.name, active;
    end if;
    return new;
end $$;

Проверьте это здесь.

Клину
источник