约束 - 一个布尔行为真,所有其他行为假

the*_*erd 21 postgresql constraint referential-integrity postgresql-9.5

我有一个专栏: standard BOOLEAN NOT NULL

我想强制执行一行 True,而所有其他行都是 False。根据此约束,没有 FK 或其他任何东西。我知道我可以用 plpgsql 完成它,但这似乎是一个大锤。我更喜欢 a CHECKorUNIQUE约束之类的东西。越简单越好。

一行必须为 True,它们不能全部为 False(因此插入的第一行必须为 True)。

该行将需要更新,这意味着我必须等待检查约束,直到更新完成,因为所有行可能先设置为 False,然后再将一行设置为 True。

products.tax_rate_id和之间有一个 FK tax_rate.id,但它与默认或标准税率无关,用户可选择以轻松创建新产品。

PostgreSQL 9.5 如果重要的话。

背景

表是税率。其中一种税率是默认值(standard因为默认值是 Postgres 命令)。添加新产品时,标准税率适用于该产品。如果没有standard,则数据库必须进行猜测或进行各种不需要的检查。我想,简单的解决方案是确保有一个standard.

上面的“默认”是指表示层(UI)。用户可以选择更改默认税率。我要么需要添加额外的检查以确保 GUI/用户不会尝试将 tax_rate_id 设置为 NULL,或者只是设置默认税率。

Erw*_*ter 18

变体 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)
);
Run Code Online (Sandbox Code Playgroud)

DEFAULT是一个可选的提醒,输入的第一行应成为默认值。它没有强制执行任何事情。虽然您不能将多行设置为standard = true,但您仍然可以将所有行设置为 NULL。没有干净的方法可以仅通过单个表中的约束来防止这种情况。CHECK约束不考虑其他行(没有肮脏的技巧)。

有关的:

更新:

BEGIN;
UPDATE taxrate SET standard = NULL WHERE standard;
UPDATE taxrate SET standard = TRUE WHERE taxrate = 2;
COMMIT;
Run Code Online (Sandbox Code Playgroud)

允许这样的命令(仅在语句末尾满足约束):

WITH kingdead AS (
   UPDATE taxrate
   SET standard = NULL
   WHERE standard
   )
UPDATE taxrate
SET standard = TRUE
WHERE taxrate = 1;
Run Code Online (Sandbox Code Playgroud)

..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);
Run Code Online (Sandbox Code Playgroud)

现在总是有一行指向标准(在这个简单的例子中也直接代表标准费率)。只有超级用户才能打破它。您也可以使用 trigger 禁止这样做BEFORE DELETE

dbfiddle在这里

有关的:

您可以添加 aVIEW以查看与变体 1相同的内容:

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

在您只需要标准费率的查询中,taxrate_standard.taxrate直接使用(仅)。


你后来补充说:

products.tax_rate_id和之间有一个 FKtax_rate.id

一个穷人对变体 2的实现只是在products(或任何类似的表)中添加一行指向标准税率;如果您的设置允许,您可能会称之为“标准税率”的虚拟产品。

FK 约束强制执行参照完整性。要完成它,请tax_rate_id IS NOT NULL对行强制执行(如果一般列不是这种情况)。并禁止删除。两者都可以放入触发器中。没有额外的桌子,但不那么优雅,也不那么可靠。

  • **强烈**推荐使用两张表的方法。我还建议向该变体添加一个示例查询,以便 OP 可以查看如何针对标准进行“CROSS JOIN”、针对特定的“LEFT JOIN”以及两者之间的“COALESCE”。 (2认同)
  • +1,我对额外的表格有相同的想法,但没有时间正确地写出答案。关于第一个表和`CONSTRAINT standard_only_1_true UNIQUE (standard)`:我想这个表不会很大所以没什么关系但是因为约束将在整个表上定义一个索引,所以不是部分唯一的带有“WHERE(标准)”的索引使用更少的空间? (2认同)

McN*_*ets 14

您可以使用过滤索引

create table test
(
    id int primary key,
    foo bool
);
Run Code Online (Sandbox Code Playgroud)
CREATE UNIQUE INDEX only_one_row_with_column_true_uix 
    ON test (foo) WHERE (foo);  --> where foo is true
Run Code Online (Sandbox Code Playgroud)
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);
Run Code Online (Sandbox Code Playgroud)
错误:重复键值违反唯一约束“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;
Run Code Online (Sandbox Code Playgroud)
alter table test 
    add constraint ck_one_true check(check_one_true(foo) = 1); 
Run Code Online (Sandbox Code Playgroud)
insert into test values (1, true);
insert into test values (2, false);
insert into test values (3, false);
insert into test values (4, false);
Run Code Online (Sandbox Code Playgroud)
insert into test values (5, true);
Run Code Online (Sandbox Code Playgroud)
错误:关系“test”的新行违反检​​查约束“ck_one_true”
详细信息:失败的行包含 (5, t)。

select * from test;
Run Code Online (Sandbox Code Playgroud)
身份证 | 富
-: | :--
 1 | 吨  
 2 | F  
 3 | F  
 4 | F  
delete from test where id = 1;
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)
create trigger trg_test_delete
before delete on test
for each row 
execute procedure dont_delete_foo_true();
Run Code Online (Sandbox Code Playgroud)
delete from test where id = 1;
Run Code Online (Sandbox Code Playgroud)

错误:无法删除 foo 为真的行。

dbfiddle在这里