Postgres 中匹配条件的行数上限

Kev*_*rke 7 postgresql index constraint

假设我有一个像这样的数据库模式

user_id text
photo_id text
date_created timestamp
Run Code Online (Sandbox Code Playgroud)

我想强制执行“一个用户只能拥有 30 张照片”的约束。实现这一点的一个天真的方法是

SELECT COUNT(*) FROM user_photos WHERE user_id='usr_123'
Run Code Online (Sandbox Code Playgroud)

将结果与 30 进行比较,如果较低,则进行插入。天真,因为如果您可以在短时间内触发多个请求,您就可以赢得一场比赛并最终写入 30 多个记录。

有没有办法在数据库中强制执行此约束?例如创建某种索引“只能插入 30 行”,或者什么?有没有办法让一个count列为每个 userId 单独递增,并且可以将其范围限制为0-29

如果您使用事务,会锁定您读取的行吗?

Erw*_*ter 6

类似于@Eelke@Kassandry的建议。但您不需要计数器或触发器。

CREATE TABLE t1 (
  user_id text NOT NULL  -- should probably be integer
, photo_nr int NOT NULL
, photo_id text NOT NULL
, date_created timestamptz NOT NULL DEFAULT now()
, CONSTRAINT t1_pkey PRIMARY KEY (user_id, photo_nr)
, CONSTRAINT max_30_photos CHECK (photo_nr BETWEEN 1 AND 30)
);
Run Code Online (Sandbox Code Playgroud)

关键问题是如何廉价且安全地获得下一个空闲时段。我建议:

   SELECT photo_nr
   FROM   generate_series (1,30) photo_nr
   EXCEPT (SELECT photo_nr FROM t1 WHERE user_id = 'usr123') -- your user_id here
   ORDER  BY 1
   LIMIT  1;
Run Code Online (Sandbox Code Playgroud)

细节:

photo_nr这将返回给定用户的下一个空闲时间。您的INSERT陈述以此为基础:

INSERT INTO t1 (user_id, photo_nr, photo_id)
SELECT 'usr123', photo_nr, 'my_new_photo_id'  -- provide values here
FROM  (
   SELECT photo_nr
   FROM   generate_series (1,30) photo_nr
   EXCEPT (SELECT photo_nr FROM t1 WHERE user_id = 'usr123')  -- given user_id
   ORDER  BY 1
   LIMIT  1
   ) sub;
Run Code Online (Sandbox Code Playgroud)

如果没有空闲插槽,则不会插入任何内容(如命令标签所报告)。

这对于并发请求来说并不安全。对于所有并发事务,下一次释放photo_nr都是相同的。它可能会导致并发的独特违规INSERT。但在那些非常罕见的情况下,捕获该错误并重试通常比锁定整个表(无法锁定 Postgres 中尚不存在的行)或独占锁定给定用户的所有行要便宜。

您可以将 包装INSERT在 PL/pgSQL 函数中以重试唯一的违规行为。代码示例:

如果这是一个问题,一个廉价的替代方案是锁定表中的父行users以排除任何竞争条件。看:


小智 4

您的程序可以手动为每个用户的记录编号,然后您可以对 user_id 和 recordnumber 的组合施加唯一约束(或使其成为主键),并对 recordnumber 添加检查约束,检查其是否在 1 到 30 之间。