J'ai une fonction d'insertion en bloc set_interactions(arg_rows text)
qui ressemble à ceci:
with inserts as (
insert into interaction (
thing_id,
associate_id, created_time)
select t->>'thing_id', t->>'associate_id', now() from
json_array_elements(arg_rows::json) t
ON CONFLICT (thing_id, associate_id) DO NOTHING
RETURNING thing_id, associate_id
) select into insert_count count(*) from inserts;
-- Followed by an insert in an unrelated table that has two triggers, neither of which touch any of the tables here (also not by any of their triggers, etc.)
(Je l'enroule de cette façon car j'ai besoin d'obtenir un nombre d'insertions réelles, sans l'astuce "fausses mises à jour de lignes".)
La table interaction
a:
Le déclencheur fait ceci:
DECLARE associateId text;
BEGIN
-- Go out and get the associate_id for this thing_id
BEGIN
SELECT thing.associate_id INTO STRICT associateId FROM thing WHERE thing.id = NEW.thing_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE EXCEPTION 'Could not map the thing to an associate!';
WHEN TOO_MANY_ROWS THEN
RAISE EXCEPTION 'Could not map the thing to a SINGLE associate!'; -- thing PK should prevent this
END;
-- We don't want to add an association between an associate interacting with their own things
IF associateId != NEW.associate_id THEN
-- Insert the new association, if it doesn't yet exist
INSERT INTO associations ("thing_owner", "associate")
VALUES (associateId, NEW.associate_id)
ON CONFLICT DO NOTHING;
END IF;
RETURN NULL;
END;
interactions
et associations
n'ont pas plus de colonnes que vous ne le voyez dans les instructions ci-dessus.
Parfois, j'obtiens une erreur deadlock detected
De PostgreSQL 9.6.5 lorsque l'application appelle set_interactions()
. Il peut l'appeler avec 1 à 100 lignes de données, non triées; les lots "en conflit" peuvent ou non avoir une entrée identique (au niveau du lot entier ou pour chaque ligne en conflit).
Détails de l'erreur:
deadlock detected while inserting index Tuple (37605,46) in relation "associations" SQL statement INSERT INTO associations ("thing_owner", "associate") VALUES (associateId, NEW.associate_id) ON CONFLICT DO NOTHING; PL/pgSQL function aud.addfriendship() line 19 at SQL statement SQL statement "with inserts as ( insert into interaction ( thing_id, associate_id, created_time) select t->>'thing_id', t->>'associate_id', now() from json_array_elements(arg_rows::json) t ON CONFLICT (thing_id, associate_id) DO NOTHING RETURNING thing_id, associate_id ) select count(*) from inserts" PL/pgSQL function setinteractions(text) line 7 at SQL statement Process 31370 waits for ShareLock on transaction 111519214; blocked by process 31418. Process 31418 waits for ShareLock on transaction 111519211; blocked by process 31370. error: deadlock detected
Je pensais que peut-être la fonction était parfois appelée avec des données en double en un seul appel. Pas le cas: cela entraîne à la place une erreur garantie, ON CONFLICT DO UPDATE command cannot affect row a second time
.
Je ne suis pas en mesure de reproduire le blocage, même en essayant 1 000 appels à set_interactions()
à la fois avec des paramètres identiques, ou même avec des paires de lignes identiques ayant (différentes dans la paire) thing_id
Et associate_id
Mais d'autres valeurs aussi, donc elles ne sont pas optimisées avant de frapper PostgreSQL (elles ne devraient pas non plus être optimisées par la base de données, car la fonction est marquée volatile
.) provient d'une extrémité arrière à filetage unique; mais en même temps, l'application elle-même n'exécute qu'un seul back-end en production, où l'impasse se produit. J'ai même essayé d'exécuter ces 1 000 appels sur une copie complète de la base de données de production, et même sous la charge d'un second back-end, et en plus de pgAdmin via une requête très longue qui sélectionne parmi interactions
. Ils réussissent sans se plaindre.
https://rcoh.svbtle.com/postgres-unique-constraints-can-cause-deadlock mentionne essayer d'éviter de s'appuyer sur un index unique (ce à quoi correspond le PK, si je comprends bien) ) lors de l'insertion de doublons. Cependant, c'était avant ON CONFLICT DO UPDATE
, Ce qui, je pense, résoudrait ce problème.
Comment cette requête est-elle bloquée de manière "aléatoire" et comment puis-je la résoudre? (Aussi, pourquoi ne puis-je pas le reproduire avec la méthode ci-dessus?)
La clause ON CONFLICT
Peut empêcher les erreurs clé en double. Il peut toujours y avoir des frictions avec des transactions simultanées essayant d'entrer les mêmes clés ou de mettre à jour les mêmes lignes. Ce n'est donc pas une assurance contre les blocages.
Plus important encore, ajoutez un ordre cohérent pour saisir les lignes avec ORDER BY
. Pour m'assurer que la commande est exécutée, j'utilise un CTE, qui matérialise le résultat. (I pensez cela devrait aussi fonctionner avec une sous-requête; juste pour être sûr.) Sinon, des insertions mutuellement enchevêtrées essayant d'entrer des tuples d'index identiques dans l'index unique peuvent conduire à l'impasse que vous avez observée. Le manuel:
La meilleure défense contre les blocages est généralement de les éviter en étant certain que toutes les applications utilisant une base de données acquièrent des verrous sur plusieurs objets dans un ordre cohérent.
De plus, puisque set_interactions()
est une fonction PL/pgSQL, c'est plus simple et moins cher:
WITH data AS (
SELECT t->>'thing_id' AS t_id, t->>'associate_id' AS a_id
-- Or, if not type text, cast right away:
-- SELECT (t->>'thing_id')::int AS t_id, (t->>'associate_id')::int AS a_id
FROM json_array_elements(arg_rows::json) t
ORDER BY 1, 2 -- deterministic, stable order (!!)
)
INSERT INTO interaction (thing_id, associate_id, created_time)
SELECT t_id, a_id, now()
FROM data
ON CONFLICT (thing_id, associate_id) DO NOTHING;
GET DIAGNOSTICS insert_count = ROW_COUNT;
Pas besoin d'un autre CTE, RETURNING
et un autre count(*)
. Plus:
La fonction de déclenchement semble également gonflée. Pas besoin d'un bloc imbriqué, car vous n'attrapez pas d'erreurs, ne soulevant que des exceptions qui annulent la transaction entière dans tous les cas. Et les exceptions sont également inutiles.
Le 1er EXCEPTION
sur NO_DATA_FOUND
Ne peut jamais se produire dans une conception plusieurs-à-plusieurs appropriée avec des contraintes FK imposant l'intégrité référentielle.
Le 2e est également inutile - vous vous en doutez autant:
- PK devrait empêcher cela
La fonction de déclenchement se résume à:
BEGIN
-- Insert the new association, if it doesn't yet exist
INSERT INTO associations (thing_owner, associate)
SELECT t.associate_id, NEW.associate_id
FROM thing t
WHERE t.id = NEW.thing_id -- -- PK guarantees 0 or 1 result
AND t.associate_id <> NEW.associate_id -- exclude association to self
ON CONFLICT DO NOTHING;
RETURN NULL;
END
Vous pouvez supprimer complètement le déclencheur et la fonction set_interactions()
et simplement exécuter cette requête, en faisant tout ce que je peux voir dans la question:
WITH data AS (
SELECT (t->>'thing_id')::int AS t_id, (t->>'associate_id')::int AS a_id -- asuming int
FROM json_array_elements(arg_rows::json) t
ORDER BY 1, 2 -- (!!)
)
, ins_inter AS (
INSERT INTO interaction (thing_id, associate_id, created_time)
SELECT t_id, a_id, now()
FROM data
ON CONFLICT (thing_id, associate_id) DO NOTHING
RETURNING thing_id, associate_id
)
, ins_ass AS (
INSERT INTO associations (thing_owner, associate)
SELECT t.associate_id, i.associate_id
FROM ins_inter i
JOIN thing t ON t.id = i.thing_id
AND t.associate_id <> i.associate_id -- exclude association to self
ON CONFLICT DO NOTHING
)
SELECT count(*) FROM ins_inter;
Maintenant, je ne vois plus aucune chance de blocage. Bien sûr, toutes les autres transactions pouvant également écrire simultanément dans la même table doivent respecter le même ordre de lignes.
Si ce n'est pas possible et que vous envisagez toujours SKIP LOCKED
, Consultez: