web-dev-qa-db-ja.com

行数に基づいて条件付き挿入を実行するにはどうすればよいですか?

私は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;
_

または、私が考慮していないより良いオプションはありますか?

6
Rob Johansen

通常、一意の_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;
_

singleINSERTの例。セット全体を一度に処理するには、さらに処理を行う必要があります。下記参照。

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;
_

FOUNDについて

呼び出し(値のlistを使用した単純な構文に注意してください):

_SELECT f_add_players(3, 5, 7, 66);
_

または、実際の配列を渡すには、VARIADICキーのWordに再度注意してください。

_SELECT f_add_players(3, VARIADIC '{5,7,66}');
_

関連:

10

私はあなたのコメントに答えています

今のところ、常に単一の行を挿入していますが、将来的には、セット全体を一度に挿入することが必要/望ましいでしょう。

プレイヤーがチームに参加したかどうかを保存する方法がわかりません。だから私はそれらを「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」に変更し、各チームにすでに挿入されている結合の数を差し引くまでは、あなた次第です。

1
Julien Feniou

これは簡単にアトミックに実行できます。

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からテーブルを保護するのは複雑です。

私の他の回答が興味深いかもしれません 同時グループ予約の戦略?

0
Evan Carroll