SQL 每个用户最多两个条目的唯一约束

SQL unique constraint of a max of two entries per user

是否可以在 SQL 中生成一个唯一约束,允许单个用户 (user_id) 最多启用两个条目 (enabled)?举例如下

user_id  | enabled
------------------
123      | true
123      | true
123      | false
456      | true

以上是有效的,但添加另一个 user_id = 123enabled = true 会失败,因为会有三个条目。另外添加 user_id = 123enabled = false 是有效的,因为 table 仍然满足规则。

您不能允许 "enabled" 有两个值。但这里有一个无需使用触发器即可接近您想要的解决方案。这个想法是将值编码为数字并对其中两个值强制执行唯一性:

create table t (
    user_id int,
    enabled_code int,
    is_enabled boolean as (enabled_code <> 0),
    check (enabled_code in (0, 1, 2))
);

create unique index unq_t_enabled_code_1
    on t(user_id, enabled_code)
    where enabled_code = 1;

create unique index unq_t_enabled_code_2
    on t(user_id, enabled_code)
    where enabled_code = 2;

插入新值有点棘手,因为您需要检查值是在插槽“1”还是“2”中。但是,您可以使用is_enabled作为查询的布尔值。

您可以通过将另一个 boolean 列添加到 UNIQUEPRIMARY KEY 约束(或 UNIQUE 索引)来使其工作:

CREATE TABLE tbl (
   user_id int
 , enabled bool
 , enabled_first bool DEFAULT true
 , PRIMARY KEY (user_id, enabled, enabled_first)
);

enabled_firsttrue 标记每个实例的第一个。我将 DEFAULT true 设置为允许每个 user_id 对第一个 enabled 进行简单插入 - 没有提及添加的 enabled_first。需要显式 enabled_first = false 才能插入第二个实例。

NULL 被我使用的 PK 约束自动排除。请注意,一个简单的 UNIQUE 约束仍然允许 NULL 值,绕过您想要的约束。您必须另外定义所有三列 NOT NULL。参见:

  • Allow null in unique column

db<>fiddle here

当然,现在true/false这两个值内部不同,需要调整写操作。这可能是也可能不是acceptable。甚至可能是可取的。

受欢迎的副作用:由于每个索引元组的最小有效负载(实际数据大小)为 8 个字节,而布尔值占用 1 个字节而不需要对齐填充,索引仍然与 [=28] 的最小大小相同=].
与table类似:添加的boolean不会增加物理存储。 (可能不适用于具有更多列的 tables。)参见:

已经解释过,仅约束或唯一索引无法强制执行您想要的逻辑。

另一种方法是使用物化视图。逻辑是使用 window 函数在视图中创建一个附加列,该列重置每两行具有相同 (user_id, enabled) 的行。然后,您可以在该列上放置一个唯一的部分索引。最后,您可以创建一个触发器,在每次插入或更新记录时刷新视图,从而有效地实施唯一约束。

-- table set-up
create table mytable(user_id int, enabled boolean);

-- materialized view set-up
create materialized view myview as 
select 
    user_id, 
    enabled, 
    (row_number() over(partition by user_id, enabled) - 1) % 2 rn 
from mytable;

-- unique partial index that enforces integrity
create unique index on myview(user_id, rn) where(enabled);

-- trigger code
create or replace function refresh_myview()
returns trigger language plpgsql
as $$
begin
    refresh materialized view myview;
    return null;
end$$;

create trigger refresh_myview
after insert or update
on mytable for each row
execute procedure refresh_myview();

完成此设置后,让我们插入初始内容:

insert into mytable values
    (123, true),
    (123, true),
    (234, false),
    (234, true);

这有效,现在视图的内容是:

user_id | enabled | rn
------: | :------ | -:
    123 | t       |  0
    123 | t       |  1
    234 | f       |  0
    234 | t       |  0

现在,如果我们尝试插入违反约束的行,则会引发错误,并且 insert 会被拒绝。

insert into mytable values(123, true);
-- ERROR:  could not create unique index "myview_user_id_rn_idx"
-- DETAIL:  Key (user_id, rn)=(123, 0) is duplicated.
-- CONTEXT:  SQL statement "refresh materialized view myview"
-- PL/pgSQL function refresh_myview() line 3 at SQL statement

Demo on DB Fiddle