web-dev-qa-db-fra.com

Comment effectuer une insertion conditionnelle basée sur le nombre de lignes?

J'utilise Postgres 9.3 et je dois empêcher les insertions dans une table sur la base d'un nombre de lignes spécifiques déjà dans la table. Voici le tableau:

                                      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

Ainsi, par exemple, si une équipe avec l'ID 3 n'autorise que 20 joueurs et que SELECT COUNT(*) FROM team_joins WHERE team_id = 3 est égal à 20, aucun joueur ne devrait pouvoir rejoindre l'équipe 3. Quelle est la meilleure façon de gérer cela et d'éviter les problèmes de concurrence ? Dois-je utiliser une transaction SERIALIZABLE pour insérer, ou puis-je simplement utiliser une clause WHERE comme celle-ci dans l'instruction d'insertion?

INSERT INTO team_joins (team_id)
VALUES (3)
WHERE (
  SELECT COUNT(*) FROM team_joins WHERE team_id = 3
) < 20;

Ou y a-t-il une meilleure option que je n'envisage pas?

6
Rob Johansen

En règle générale, vous disposez d'une table team (ou similaire) avec une colonne team_id Unique.
Votre contrainte FK indique autant: ... REFERENCES teams(id) - je vais donc travailler avec teams(id).

Ensuite, pour éviter les complications (conditions de concurrence critique ou blocages) lors d'une charge d'écriture simultanée, il est généralement plus simple et moins coûteux de prendre un verrou d'écriture sur la ligne parent dans team puis, dans la même transaction, écrivez la ou les lignes enfants dans 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;

Exemple pour single ligne INSERT. Pour traiter un ensemble entier à la fois, vous devez en faire plus; voir ci-dessous.

On pourrait suspecter un problème de cas d'angle dans le SELECT. Et s'il y a encore non ligne avec team_id = 3? La clause WHERE n'annulerait-elle pas le INSERT?
Ce ne serait pas le cas, car la clause HAVING en fait une agrégation sur l'ensemble entier qui toujours renvoie exactement une ligne (qui est éliminée s'il y en a 20 ou plus pour le team_id déjà donné) - exactement le comportement que vous voulez. Le manuel:

Si une requête contient des appels de fonction agrégés, mais pas de clause GROUP BY, Le regroupement se produit toujours: le résultat est une ligne de groupe unique (ou peut-être aucune ligne du tout, si la ligne unique est ensuite éliminé par HAVING) . La même chose est vraie si elle contient une clause HAVING, même sans appel de fonction d'agrégation ou clause GROUP BY .

Accentuation sur moi.

Le cas où aucune ligne parent n'est trouvée n'est pas un problème non plus. Votre contrainte FK applique de toute façon l'intégrité référentielle. Si team_id N'est pas dans la table parent, la transaction se termine avec une violation de clé étrangère dans les deux cas.

Tous les opérations d'écriture éventuellement en concurrence sur team_joins Doivent suivre le même protocole.

Dans le cas UPDATE, si vous modifiez le team_id, Vous verrouillez la source et l'équipe cible.

Les verrous sont libérés à la fin de la transaction. Explication détaillée dans cette réponse étroitement liée:

Dans Postgres 9.4 ou version ultérieure, le nouveau, plus faible FOR NO KEY UPDATE peut être préférable. Fait aussi le travail, moins de blocage, potentiellement moins cher. Le manuel:

Se comporte de la même manière que FOR UPDATE, Sauf que le verrou acquis est plus faible: ce verrou ne bloquera pas les commandes SELECT FOR KEY SHARE Qui tentent d'acquérir un verrou sur les mêmes lignes. Ce mode de verrouillage est également acquis par tout UPDATE qui n'acquiert pas de verrou FOR UPDATE.

Une autre incitation à envisager la mise à niveau ...

Insérez plusieurs joueurs de la même équipe

En supposant utilement que vous avez une colonne player_id integer NOT NULL. Même verrouillage que ci-dessus, plus ...

Syntaxe courte:

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

La fonction set-return dans la liste SELECT n'est pas conforme au SQL standard, mais elle est parfaitement valide dans Postgres.
Ne combinez simplement pas plusieurs fonctions de retour d'ensemble dans la liste SELECT avant Postgres 10, ce qui a finalement corrigé un comportement inattendu.

SQL standard plus propre et plus détaillé faisant de même:

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

C'est tout ou rien. Comme dans un jeu de Blackjack: un de trop et l'ensemble INSERT est sorti.

Une fonction

Pour terminer, tout cela pourrait être commodément encapsulé dans une fonction 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;

À propos de FOUND.

Appel (notez la syntaxe simple avec une liste de valeurs):

SELECT f_add_players(3, 5, 7, 66);

Ou, pour passer un réel tableau - notez à nouveau la clé VARIADIC:

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

En relation:

10
Erwin Brandstetter

Je réponds à votre commentaire

Pour l'instant, j'insère toujours des lignes simples, mais à l'avenir, il sera probablement nécessaire/souhaitable d'insérer un ensemble entier à la fois

Je ne sais pas comment vous stockez un joueur a rejoint une équipe ou non. Je vais donc les appeler "newplayer" Si vous avez beaucoup de "newplayers" en attente de rejoindre une équipe, je suggère ce genre de requête pour savoir combien d’équipes vous devez créer:

SELECT DISTINCT ((ROW_NUMBER() OVER () -1)/20) +1) 
FROM newplayer

Il retourne une liste de nombres de 1 au maximum nécessaire. Si vous avez 55 joueurs sans équipe, il retournera "1", "2", "3". Ensuite, vous pouvez vous joindre à cette demande pour insérer vos 3 équipes à la fois.

Pour vous team_joins, quelque chose comme ceci:

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 

À vous de changer "20" en "team.limit" et de soustraire le nombre de jointures déjà insérées pour chaque équipe.

1
Julien Feniou

Vous pouvez facilement accomplir cela atomiquement ..

INSERT INTO team_joins (team_id)
SELECT team_id
FROM team_joins
GROUP BY team_id
HAVING count(*) < 20;

Cependant, je ne suis pas sûr que la meilleure façon en raison de problèmes de concurrence. Sans SERIALIAZABLE, il est toujours possible mais extrêmement peu probable sous des charges de travail normales, que la sélection se termine avant le début de INSERT. Ma façon préférée de résoudre le problème de concurrence n'est pas de INSERT ou DELETE simultanément, sauf si vous devez le faire. Au lieu de cela, pour pré-insérer lorsque vous ajoutez l'équipe et allouez les membres de l'équipe. Sans SERIALIZABLE, il existe de nombreux cas Edge. Avec SERIALIZABLE, vous devez rejouer les transactions - ces deux solutions sont plus concrètes et mais beaucoup plus complexes.

En règle générale, il est facile et naturel de verrouiller/protéger des lignes et de garantir contre toute modification lors d'une transaction. Il est complexe de protéger les tables contre les INSERT.

Vous pouvez trouver mon autre réponse intéressante Stratégie pour les réservations de groupe simultanées?

0
Evan Carroll