制約-1つのブール行はtrue、他のすべての行はfalse


13

列があります: standard BOOLEAN NOT NULL

1つの行をTrueに、その他の行をすべてFalseに強制したいと思います。この制約に応じて、FKやその他のものはありません。私はplpgsqlでそれを達成できることを知っていますが、これは大槌のようです。CHECKまたはUNIQUE制約のようなものを好みます。シンプルなほど良い。

1つの行はTrueでなければなりませんが、すべてFalseにすることはできません(したがって、最初に挿入された行はTrueである必要があります)。

行を更新する必要があります。つまり、すべての行が最初にFalseに設定され、その後に1つの行がTrueに設定される可能性があるため、更新が完了するまで制約を確認するのを待つ必要があります。

そこの間にFKがあるproducts.tax_rate_idtax_rate.id、それはデフォルトまたは新製品を作成する容易にするために、ユーザが選択可能で、標準税率、とは何の関係もありません。..

重要な場合はPostgreSQL 9.5。

バックグラウンド

表は税率です。税率の1つがデフォルトです(standardデフォルトはPostgresコマンドであるため)。新しい商品が追加されると、標準の税率が商品に適用されます。がないstandard場合、データベースは推測またはすべての種類の不要なチェックを実行する必要があります。簡単な解決策は、があることを確認することだと思いましたstandard

上記の「デフォルト」とは、プレゼンテーション層(UI)を意味します。デフォルトの税率を変更するためのユーザーオプションがあります。GUI /ユーザーがtax_rate_idをNULLに設定しないようにするために追加のチェックを追加するか、デフォルトの税率を設定する必要があります。


答えはありますか?
Erwin Brandstetter 2018年

はい、私の回答があります。@ ErwinBrandstetterの入力に感謝します。とりあえずトリガーに寄りかかっています。これは私自身の時代のオープンソースプロジェクトです。実際に実装するときは、使用する回答を受け入れ済みとしてマークします。
theGtknerd 2018年

回答:


15

バリアント1

必要なのはを含む単一の列だけなのでstandard = true、他のすべての行でstandardをNULLに設定します。次に、UNIQUENULL値は違反しないため、単純な制約が機能します。

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

次のような単一の行を持つ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

関連:

バリアント1VIEWと同じように表示するには、a を追加します。

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との間にFKがありますtax_rate.id

貧乏人の実装変形形態2のだけに行を追加することでproducts、標準税率を指す(または任意の同様のテーブル)。「標準税率」と呼ぶかもしれないダミー製品-セットアップで許可されている場合。

FK制約は、参照整合性を適用します。それを完了するにtax_rate_id IS NOT NULLは、行を強制します(それが一般的に列に当てはまらない場合)。そして、その削除を禁止します。どちらもトリガーに入れることができます。余分なテーブルはありませんが、エレガントさが低く、信頼性が低くなります。


2
2つのテーブルによるアプローチを強くお勧めします。また、そのバリエーションにサンプルクエリを追加して、OPがCROSS JOIN標準に対する方法LEFT JOIN、特定の方法、そしてCOALESCE2つの方法を比較できるようにすることもお勧めします。
jpmc26 2018

2
+1、私は余分なテーブルについて同じ考えを持っていましたが、答えを適切に書く時間はありませんでした。最初のテーブルとについてCONSTRAINT standard_only_1_true UNIQUE (standard):テーブルは大きくないのでそれほど重要ではないと思いますが、制約によってテーブル全体のインデックスが定義されるため、WHERE (standard)使用するスペースが少ない部分的な一意のインデックスではないでしょうか?
ypercubeᵀᴹ

@ypercubeᵀᴹ:はい、テーブル全体のインデックスは大きくなります。これは、このバリアントの欠点です。しかし、あなたが言ったように、それは明らかに小さなテーブルなので、ほとんど問題になりません。制約のみの最も簡単な標準ソリューションを目指していました。コンセプトの証明。個人的に、私はjpmc26と思いますし、強くバリアント2を好む
アーウィンBrandstetter

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


しかし、言ったように、最初の行はtrueでなければならないので、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、t)が含まれます。

select * from test;
id | foo
-:| :-
 1 | t  
 2 | f  
 3 | f  
 4 | f  
delete from test where id = 1;

ここ dbfiddle


これを解決するには、BEFORE DELETEトリガーを追加して、最初の行(fooが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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.