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_joins(INSERT/ 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)
调用(注意带有值列表的简单语法):
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)
有关的: