如何根据行数执行条件插入?

Rob*_*sen 9 postgresql database-design insert concurrency

我正在使用 Postgres 9.3,我需要根据表中已有的特定行数来防止插入到表中。这是表:

                                      Table "public.team_joins"
     Column      |           Type           |                            Modifiers                             
-----------------+--------------------------+---------------------------------------------------------
 id              | integer                  | not null default nextval('team_joins_id_seq'::regclass)
 team_id         | integer                  | not null
Indexes:
    "team_joins_pkey" PRIMARY KEY, btree (id)
    "team_joins_team_id" btree (team_id)
Foreign-key constraints:
    "team_id_refs_teams_id" FOREIGN KEY (team_id) REFERENCES teams(id) DEFERRABLE INITIALLY DEFERRED
Run Code Online (Sandbox Code Playgroud)

因此,例如,如果一个 id 为 3 的团队只允许 20 名玩家,并且SELECT COUNT(*) FROM team_joins WHERE team_id = 3等于 20,那么没有玩家应该能够加入团队 3。处理这种情况并避免并发问题的最佳方法是什么?我应该使用SERIALIZABLE事务来插入,还是可以WHERE在插入语句中使用这样的子句?

INSERT INTO team_joins (team_id)
VALUES (3)
WHERE (
  SELECT COUNT(*) FROM team_joins WHERE team_id = 3
) < 20;
Run Code Online (Sandbox Code Playgroud)

或者有没有我没有考虑的更好的选择?

Erw*_*ter 12

通常,您有一个team带有唯一team_id列的表(或类似表)。
您的 FK 约束表明:... REFERENCES teams(id)- 所以我将使用teams(id).

然后,避免并发症的发生(比赛条件或死锁)并行写入负载下,这是典型的最简单,最便宜的也要采取了对母行写锁定team,然后在同一个事务中,写子行(S) team_joinsINSERT/ UPDATE/ DELETE)。

BEGIN;

SELECT * FROM teams WHERE id = 3 FOR UPDATE;  -- write lock

INSERT INTO team_joins (team_id)
SELECT 3                -- inserting single row
FROM   team_joins
WHERE  team_id = 3
HAVING count(*) < 20;

COMMIT;
Run Code Online (Sandbox Code Playgroud)

例如,对于INSERT。要一次处理一整套,你需要做更多的事情;见下文。

人们可能会怀疑SELECT. 如果没有 ,team_id = 3怎么办?该WHERE条款不会取消INSERT?
它不会,因为该HAVING子句使它成为整个集合的聚合,它总是只返回一行(如果给定的team_id已经有 20 行或更多行,则该行被消除) -正是您想要的行为。手册:

如果查询包含聚合函数调用,但没有GROUP BY子句,则仍会发生分组:结果是单个组行(或者可能根本没有行,如果该单行随后被 消除HAVING。如果它包含一个HAVING子句,即使没有任何聚合函数调用或GROUP BY子句也是如此

大胆强调我的。

找不到父行的情况也没有问题。无论如何,您的 FK 约束都会强制执行参照完整性。如果team_id不在父表中,则事务会因外键违规而终止。

所有可能存在竞争的写操作team_joins都必须遵循相同的协议。

在这种UPDATE情况下,如果您更改team_id,您将锁定源目标团队。

在事务结束时释放锁。这个密切相关的答案中的详细解释:

在 Postgres 9.4或更高版本中,新的、较弱的FOR NO KEY UPDATE可能更可取。也可以完成这项工作,减少阻塞,可能更便宜。手册:

行为与 类似FOR UPDATE,不同之处在于获取的锁较弱:此锁不会阻止SELECT FOR KEY SHARE尝试获取相同行上的锁的命令。这种锁定模式也被任何UPDATE没有获得FOR UPDATE锁定的人获得。

考虑升级的另一个动机......

插入同一队的多名球员

假设您有一个 column 很有用player_id integer NOT NULL。与上述相同的锁定,加上...

简短的语法:

INSERT INTO team_joins (team_id, player_id)
SELECT 3, unnest('{5,7,66}'::int[])
FROM   team_joins
WHERE  team_id = 3
HAVING count(*) < (21 - 3);  -- 3 being the number of rows to insert
Run Code Online (Sandbox Code Playgroud)

SELECT列表中的 set-returning 函数不符合标准 SQL,但它在 Postgres 中完全有效。
只是不要SELECT在 Postgres 10 之前在列表中组合多个返回集合的函数,这最终修复了那里的一些意外行为。

更简洁、更详细的标准 SQL 执行相同操作:

INSERT INTO team_joins (team_id, player_id)
SELECT team_id, player_id
FROM  (
   SELECT 3 AS team_id
   FROM   team_joins
   WHERE  team_id = 3
   HAVING count(*) < (21 - 3)
   ) t
CROSS JOIN (
   VALUES (5), (7), (66)
   ) p(player_id);
Run Code Online (Sandbox Code Playgroud)

这是全有或全无。就像在二十一点游戏中一样:一个太多了,整个INSERT就出局了。

功能

最后,所有这些都可以方便地封装在VARIADICPL/pgSQL 函数中:

CREATE OR REPLACE FUNCTION f_add_players(team_id int, VARIADIC player_ids int[])
  RETURNS bool AS
$func$
BEGIN
   SELECT * FROM teams WHERE id = 3 FOR UPDATE;         -- lock team
-- SELECT * FROM teams WHERE id = 3 FOR NO KEY UPDATE;  -- in pg 9.4+

   INSERT INTO team_joins (team_id, player_id)
   SELECT $1, unnest($2)                                -- use $1, not team_id
   FROM   team_joins t
   WHERE  t.team_id = $1                                -- table-qualify to disambiguate
   HAVING count(*) < 21 - array_length($2, 1);
   -- HAVING count(*) < 21 - cardinality($2);           -- in pg 9.4+

   RETURN FOUND;                                        -- true if INSERT
END
$func$  LANGUAGE plpgsql;
Run Code Online (Sandbox Code Playgroud)

关于FOUND.

调用(注意带有值列表的简单语法):

SELECT f_add_players(3, 5, 7, 66);
Run Code Online (Sandbox Code Playgroud)

或者,传递一个实际数组-VARIADIC再次注意关键字:

SELECT f_add_players(3, VARIADIC '{5,7,66}');
Run Code Online (Sandbox Code Playgroud)

有关的: