Ограничение - одна логическая строка истинна, все остальные строки ложны

13

У меня есть колонка: standard BOOLEAN NOT NULL

Я хотел бы обеспечить один ряд True, а все остальные False. Это не FK или что-либо еще в зависимости от этого ограничения. Я знаю, что могу сделать это с помощью plpgsql, но это похоже на кувалду. Я предпочел бы что - то вроде CHECKили UNIQUEограничения. Чем проще, тем лучше.

Одна строка должна быть True, они не могут быть ложными (поэтому первая вставленная строка должна быть True).

Строка должна быть обновлена, что означает, что мне нужно ждать, чтобы проверить ограничения, пока не будут выполнены обновления, поскольку все строки могут быть установлены в False в первую очередь, а одна строка в True после.

Существует FK между products.tax_rate_idи tax_rate.id, но это не имеет ничего общего с неисполнением или стандартной ставки налога, который выбирается пользователем , чтобы облегчить создание новых продуктов ..

PostgreSQL 9.5, если это имеет значение.

Фон

В таблице указана ставка налога. По умолчанию standardиспользуется одна из налоговых ставок ( поскольку по умолчанию используется команда Postgres). При добавлении нового продукта к нему применяется стандартная налоговая ставка. Если нет standard, база данных должна либо сделать предположение, либо выполнить все виды ненужных проверок. Я подумал, что простым решением было убедиться, что оно есть standard.

Под «по умолчанию» выше я подразумеваю уровень представления (UI). Существует пользовательская опция для изменения ставки налога по умолчанию. Мне нужно либо добавить дополнительные проверки, чтобы гарантировать, что GUI / пользователь не пытается установить tax_rate_id в NULL, либо просто установить ставку налога по умолчанию.

theGtknerd
источник
Так у тебя есть ответ?
Эрвин Брандштеттер
Да, у меня есть ответ, большое спасибо за ваш вклад, @ErwinBrandstetter. Я сейчас склоняюсь к курку. Это проект с открытым исходным кодом в свободное время. Когда я на самом деле его реализую, я отмечу принятый ответ.
theGtknerd

Ответы:

15

Вариант 1

Поскольку все, что вам нужно, это один столбец с standard = true, установите стандартное значение NULL во всех других строках. Тогда работает простое UNIQUEограничение, поскольку значения NULL не нарушают его:

CREATE TABLE taxrate (
   taxrate int PRIMARY KEY
 , standard bool DEFAULT true
 , CONSTRAINT standard_true_or_null CHECK (standard) -- yes, that's the whole constraint
 , CONSTRAINT standard_only_1_true UNIQUE (standard)
);

DEFAULTявляется необязательным напоминанием, что первая введенная строка должна стать значением по умолчанию. Это ничего не навязывает . Хотя вы не можете установить более одной строки standard = true, вы все равно можете установить все строки NULL. Нет четкого способа предотвратить это, используя только ограничения в одной таблице. CHECKограничения не учитывают другие строки (без подвохов).

Связанные с:

Обновить:

BEGIN;
UPDATE taxrate SET standard = NULL WHERE standard;
UPDATE taxrate SET standard = TRUE WHERE taxrate = 2;
COMMIT;

Чтобы разрешить команду типа (где ограничение выполняется только в конце оператора):

WITH kingdead AS (
   UPDATE taxrate
   SET standard = NULL
   WHERE standard
   )
UPDATE taxrate
SET standard = TRUE
WHERE taxrate = 1;

.. UNIQUEограничение должно быть DEFERRABLE. Видеть:

dbfiddle здесь

Вариант 2

Есть вторая таблица с одной строкой, как:

Создайте это как суперпользователь:

CREATE TABLE taxrate (
   taxrate int PRIMARY KEY
);

CREATE TABLE taxrate_standard (
   taxrate int PRIMARY KEY REFERENCES taxrate
);

CREATE UNIQUE INDEX taxrate_standard_singleton ON taxrate_standard ((true));  -- singleton

REVOKE DELETE ON TABLE taxrate_standard FROM public;  -- can't delete

INSERT INTO taxrate (taxrate) VALUES (42);
INSERT INTO taxrate_standard (taxrate) VALUES (42);

Теперь всегда есть одна строка, указывающая на стандарт (в этом простом случае, также представляющая стандартную ставку напрямую). Только суперпользователь может сломать его. Вы также можете запретить это с помощью триггера BEFORE DELETE.

dbfiddle здесь

Связанные с:

Вы можете добавить, VIEWчтобы увидеть то же самое, что и в варианте 1 :

CREATE VIEW taxrate_combined AS
SELECT t.*, (ts.taxrate = t.taxrate) AS standard
FROM   taxrate t
LEFT   JOIN taxrate_standard ts USING (taxrate);

В запросах, где все, что вам нужно, это стандартная ставка, используйте (только) taxrate_standard.taxrateнапрямую.


Вы позже добавили:

Есть ФК между products.tax_rate_idиtax_rate.id

А реализация бедняка варианта 2 будет просто добавить строку в products(или любую аналогичную таблицу) , указывающую на стандартные ставки налога; фиктивный продукт, который вы могли бы назвать «Стандартная налоговая ставка» - если ваша настройка позволяет это.

Ограничения FK обеспечивают ссылочную целостность. Чтобы завершить это, примените tax_rate_id IS NOT NULLдля строки (если это не относится к столбцу в целом). И запретить его удаление. Оба могут быть включены в триггеры. Никакого дополнительного стола, но менее элегантный и не такой надежный.

Эрвин Брандштеттер
источник
2
Настоятельно рекомендую подход за двумя столами. Я также предложил бы добавить пример запроса к этому варианту, чтобы ОП мог видеть, как CROSS JOINпротивостоять стандарту, LEFT JOINконкретному, а затем COALESCEмежду ними.
jpmc26
2
+1, у меня было такое же представление о дополнительной таблице, но не было времени, чтобы правильно написать ответ. Что касается первой таблицы и CONSTRAINT standard_only_1_true UNIQUE (standard): я предполагаю, что таблица не будет большой, так что это не имеет большого значения, но поскольку ограничение будет определять индекс для всей таблицы, разве частичный уникальный индекс не будет WHERE (standard)использовать меньше места?
ypercubeᵀᴹ
@ ypercubeᵀᴹ: Да, индекс для всей таблицы больше, это недостаток для этого варианта. Но, как вы сказали: это, очевидно, крошечный столик, так что это вряд ли имеет значение. Я стремился к простейшему стандартному решению с единственными ограничениями. Доказательство концепции. Лично я с jpmc26 и решительно поддерживаю вариант 2.
Эрвин Брандштеттер,
9

Вы можете использовать фильтрованный индекс

create table test
(
    id int primary key,
    foo bool
);
CREATE UNIQUE INDEX only_one_row_with_column_true_uix 
    ON test (foo) WHERE (foo);  --> where foo is true
insert into test values (1, false);
insert into test values (2, true);
insert into test values (3, false);
insert into test values (4, false);
insert into test values (5, true);
ОШИБКА: двойное значение ключа нарушает уникальное ограничение "only_one_row_with_column_true_uix"
ДЕТАЛИ: Ключ (foo) = (t) уже существует.

dbfiddle здесь


Но, как вы сказали, первая строка должна быть истинной, тогда вы можете использовать ограничение CHECK, но даже используя функцию, вы можете удалить первую строку позже.

create function check_one_true(new_foo bool)
returns int as
$$
begin
    return 
    (
        select count(*) + (case new_foo when true then 1 else 0 end)
        from test 
        where foo = true
    );
end
$$
language plpgsql stable;
alter table test 
    add constraint ck_one_true check(check_one_true(foo) = 1); 
insert into test values (1, true);
insert into test values (2, false);
insert into test values (3, false);
insert into test values (4, false);
insert into test values (5, true);
ОШИБКА: новая строка для отношения "test" нарушает проверочное ограничение "ck_one_true"
ДЕТАЛИ: Недостающий ряд содержит (5, т).

select * from test;
id | Foo
-: | : -
 1 | T  
 2 | е  
 3 | е  
 4 | е  
delete from test where id = 1;

dbfiddle здесь


Вы можете решить эту проблему, добавив триггер BEFORE DELETE, чтобы убедиться, что первая строка (foo is true) никогда не удаляется.

create function dont_delete_foo_true()
returns trigger as
$x$
begin
    if old.foo then
        raise exception 'Can''t delete row where foo is true.';
    end if;
    return old;
end;
$x$ language plpgsql;
create trigger trg_test_delete
before delete on test
for each row 
execute procedure dont_delete_foo_true();
delete from test where id = 1;

ОШИБКА: Невозможно удалить строку, где foo имеет значение true.

dbfiddle здесь

McNets
источник