私は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
_
したがって、たとえば、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;
_
または、私が考慮していないより良いオプションはありますか?
通常、一意の_team_id
_列を持つteam
テーブル(または類似のテーブル)があります。
FK制約は次のことを示しています:... REFERENCES teams(id)
-teams(id)
で作業します。
次に、同時書き込み負荷の下での複雑さ(競合状態またはデッドロック)を回避するために、team
で親行の書き込みロックを取得し、同じトランザクションで子を書き込むのが最も簡単で最も安価です。 _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;
_
single行INSERT
の例。セット全体を一度に処理するには、さらに処理を行う必要があります。下記参照。
SELECT
のコーナーケースの問題が疑われます。 _team_id = 3
_のno行がまだある場合はどうなりますか? WHERE
句はINSERT
をキャンセルしませんか?
そうではありません。なぜなら、HAVING
句はこれをalwaysが正確に1行を返すセット全体の集約にするためです(20以上ある場合は削除されます)。指定された_team_id
_について)-exactly必要な動作 マニュアル:
クエリに集約関数呼び出しが含まれているが、_
GROUP BY
_句がない場合でも、グループ化は行われます。結果は単一グループ行になります(または、単一行がHAVING
によって削除された場合、おそらく行がまったくありません)。。HAVING
句が含まれている場合も同様です集計関数呼び出しやGROUP BY句がない場合でも。
大胆な強調鉱山。
親行が見つからない場合も問題ありません。 FK制約は、とにかく参照整合性を適用します。 _team_id
_が親テーブルにない場合、トランザクションはいずれにせよ外部キー違反で終了します。
All_team_joins
_で競合する可能性のある書き込み操作は、同じプロトコルに従う必要があります。
UPDATE
の場合、_team_id
_を変更すると、ソースおよびターゲットチームがロックされます。
ロックはトランザクションの終了時に解放されます。この密接に関連する回答の詳細な説明:
Postgres 9.4以降では、新しい、より弱い_FOR NO KEY UPDATE
_が望ましいかもしれません。また、仕事が減り、ブロッキングが減り、コストが下がる可能性があります。 マニュアル:
取得されるロックが弱いことを除いて、_
FOR UPDATE
_と同様に動作します。このロックは、同じ行でロックを取得しようとする_SELECT FOR KEY SHARE
_コマンドをブロックしません。このロックモードは、_FOR UPDATE
_ロックを取得しないUPDATE
によっても取得されます。
アップグレードを検討する別のインセンティブ...
_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
_
SELECT
リストのセットを返す関数は標準SQLに準拠していませんが、Postgresでは完全に有効です。
Postgres 10より前のSELECT
リストで複数のセットを返す関数を組み合わせないでください。これにより、予期しない動作が最終的に修正されました。
同じことを行う、よりクリーンで冗長な標準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);
_
all or nothingです。ブラックジャックゲームのように、1つが多すぎて、INSERT
全体が出ています。
四捨五入するために、これらすべてをVARIADIC
PL/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;
_
呼び出し(値のlistを使用した単純な構文に注意してください):
_SELECT f_add_players(3, 5, 7, 66);
_
または、実際の配列を渡すには、VARIADIC
キーのWordに再度注意してください。
_SELECT f_add_players(3, VARIADIC '{5,7,66}');
_
関連:
私はあなたのコメントに答えています
今のところ、常に単一の行を挿入していますが、将来的には、セット全体を一度に挿入することが必要/望ましいでしょう。
プレイヤーがチームに参加したかどうかを保存する方法がわかりません。だから私はそれらを「newplayer」と呼びますチームに参加するのを待っている多くの「newplayers」がある場合、私は今あなたが作成しなければならないチームの数にこの種のクエリを提案するでしょう:
SELECT DISTINCT ((ROW_NUMBER() OVER () -1)/20) +1)
FROM newplayer
1から必要な最大数までの数のリストを返します。チームのない55人のプレイヤーがいる場合、「1」、「2」、「3」を返します。次に、このリクエストに参加して、3つのチームを一度に挿入できます。
あなたのteam_joinsにとって、このようなもの:
WITH match AS (SELECT ((ROW_NUMBER() OVER () -1)/20) +1) AS teamId, newplayers.id as playerId)
INSERT INTO team_joins (team_id, player_id)
match.teamId, match.playerId
FROM match
「20」を「team.limit」に変更し、各チームにすでに挿入されている結合の数を差し引くまでは、あなた次第です。
これは簡単にアトミックに実行できます。
INSERT INTO team_joins (team_id)
SELECT team_id
FROM team_joins
GROUP BY team_id
HAVING count(*) < 20;
ただし、同時実行の問題があるため、最善の方法であるかどうかはわかりません。 SERIALIAZABLE
がなくてもそれは可能ですが、通常のワークロードではextremely可能性が低いため、INSERT
が開始する前に選択が完了します。並行処理の問題を解決するための私のお気に入りの方法は、必要な場合を除いて、INSERT
またはDELETE
を同時に実行しないことです。代わりに、チームを追加してチームメンバーを割り当てるときに事前挿入します。 SERIALIZABLE
がなければ、多くのEdgeケースがあります。 SERIALIZABLE
ではトランザクションを再生する必要があります。これらのソリューションはどちらもより具体的ですが、はるかに複雑です。
一般的なルールとして、行をロック/保護し、トランザクション中の変更を保証することは簡単で自然です。 INSERTからテーブルを保護するのは複雑です。
私の他の回答が興味深いかもしれません 同時グループ予約の戦略?