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 = 123
和 enabled = true
会失败,因为会有三个条目。另外添加 user_id = 123
和 enabled = 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
列添加到 UNIQUE
或 PRIMARY KEY
约束(或 UNIQUE
索引)来使其工作:
CREATE TABLE tbl (
user_id int
, enabled bool
, enabled_first bool DEFAULT true
, PRIMARY KEY (user_id, enabled, enabled_first)
);
enabled_first
用 true
标记每个实例的第一个。我将 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。)参见:
- Calculating and saving space in PostgreSQL
- Is a composite index also good for queries on the first field?
已经解释过,仅约束或唯一索引无法强制执行您想要的逻辑。
另一种方法是使用物化视图。逻辑是使用 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
是否可以在 SQL 中生成一个唯一约束,允许单个用户 (user_id
) 最多启用两个条目 (enabled
)?举例如下
user_id | enabled
------------------
123 | true
123 | true
123 | false
456 | true
以上是有效的,但添加另一个 user_id = 123
和 enabled = true
会失败,因为会有三个条目。另外添加 user_id = 123
和 enabled = 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
列添加到 UNIQUE
或 PRIMARY KEY
约束(或 UNIQUE
索引)来使其工作:
CREATE TABLE tbl (
user_id int
, enabled bool
, enabled_first bool DEFAULT true
, PRIMARY KEY (user_id, enabled, enabled_first)
);
enabled_first
用 true
标记每个实例的第一个。我将 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。)参见:
- Calculating and saving space in PostgreSQL
- Is a composite index also good for queries on the first field?
已经解释过,仅约束或唯一索引无法强制执行您想要的逻辑。
另一种方法是使用物化视图。逻辑是使用 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