Insérer, sur la mise à jour en double dans PostgreSQL?
-
12-09-2019 - |
Question
Il y a quelques mois, j'appris d'une réponse sur Stack Overflow comment effectuer plusieurs mises à jour à la fois dans MySQL en utilisant la syntaxe suivante:
INSERT INTO table (id, field, field2) VALUES (1, A, X), (2, B, Y), (3, C, Z)
ON DUPLICATE KEY UPDATE field=VALUES(Col1), field2=VALUES(Col2);
Je l'ai maintenant basculé sur PostgreSQL et apparemment ce n'est pas correct. Il fait référence à toutes les tables correctes, donc je suppose que c'est une question de différents mots-clés utilisés mais je ne suis pas sûr où dans la documentation de PostgreSQL cette question est traitée.
Pour clarifier les choses, je veux insérer plusieurs choses et si elles existent déjà pour les mettre à jour.
La solution
PostgreSQL depuis la version 9.5 a UPSERT syntaxe, avec < strong> ON clause de conflit. avec la syntaxe suivante (similaire à MySQL)
INSERT INTO the_table (id, column_1, column_2)
VALUES (1, 'A', 'X'), (2, 'B', 'Y'), (3, 'C', 'Z')
ON CONFLICT (id) DO UPDATE
SET column_1 = excluded.column_1,
column_2 = excluded.column_2;
Recherche archives du groupe électronique de postgresql pour "upsert" conduit à la recherche un exemple de faire ce que vous voulez peut-être faire, dans le manuel :
Exemple 38-2. Exceptions avec UPDATE / INSERT
Cet exemple utilise la gestion des exceptions pour réaliser soit un UPDATE ou INSERT, selon le cas:
CREATE TABLE db (a INT PRIMARY KEY, b TEXT);
CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS
$$
BEGIN
LOOP
-- first try to update the key
-- note that "a" must be unique
UPDATE db SET b = data WHERE a = key;
IF found THEN
RETURN;
END IF;
-- not there, so try to insert the key
-- if someone else inserts the same key concurrently,
-- we could get a unique-key failure
BEGIN
INSERT INTO db(a,b) VALUES (key, data);
RETURN;
EXCEPTION WHEN unique_violation THEN
-- do nothing, and loop to try the UPDATE again
END;
END LOOP;
END;
$$
LANGUAGE plpgsql;
SELECT merge_db(1, 'david');
SELECT merge_db(1, 'dennis');
Il y a peut-être un exemple de la façon de le faire en vrac, en utilisant CTEs en 9.1 et au-dessus, dans la section noreferrer pirates liste de diffusion :
WITH foos AS (SELECT (UNNEST(%foo[])).*)
updated as (UPDATE foo SET foo.a = foos.a ... RETURNING foo.id)
INSERT INTO foo SELECT foos.* FROM foos LEFT JOIN updated USING(id)
WHERE updated.id IS NULL;
Voir de réponse a_horse_with_no_name pour un exemple plus clair.
Autres conseils
Attention:. Ce n'est pas sûre si elle est exécutée à partir de plusieurs sessions en même temps (voir mises en garde ci-dessous)
Une autre façon intelligente de faire une « UPSERT » dans postgresql est de faire deux instructions UPDATE / INSERT séquentielle qui sont chacun conçus pour réussir ou sans effet.
UPDATE table SET field='C', field2='Z' WHERE id=3;
INSERT INTO table (id, field, field2)
SELECT 3, 'C', 'Z'
WHERE NOT EXISTS (SELECT 1 FROM table WHERE id=3);
La mise à jour réussir si une ligne avec « id = 3 » existe déjà, sinon il n'a pas d'effet.
L'INSERT ne réussira que si la ligne avec "id = 3" n'existe pas.
Vous pouvez combiner ces deux en une seule chaîne et de les exécuter à la fois avec une seule instruction SQL exécuter à partir de votre application. les exécuter ensemble dans une seule transaction est fortement recommandée.
Cela fonctionne très bien lorsqu'il est exécuté de manière isolée ou sur une table verrouillée, mais est soumis à la race des conditions qui signifient qu'il pourrait encore échouer avec clé doublon si une ligne est insérée en même temps, ou peut mettre fin sans ligne insérée lorsqu'une ligne est supprimé en même temps. Une transaction SERIALIZABLE
sur PostgreSQL 9.1 ou supérieur traitera de manière fiable au prix d'un taux d'échec de sérialisation très élevé, ce qui signifie que vous devrez recommencer beaucoup. Voir pourquoi est upsert si compliqué , qui discute ce cas plus en détail.
Cette approche est soumis à des mises à jour perdus dans l'isolement de read committed
à moins que l'application vérifie les comptes de ligne concernés et vérifie que soit le insert
ou le update
affecté un de ligne.
Avec PostgreSQL 9.1 cela peut être réalisé au moyen d'un CTE inscriptible ( expression de table commune ) :
WITH new_values (id, field1, field2) as (
values
(1, 'A', 'X'),
(2, 'B', 'Y'),
(3, 'C', 'Z')
),
upsert as
(
update mytable m
set field1 = nv.field1,
field2 = nv.field2
FROM new_values nv
WHERE m.id = nv.id
RETURNING m.*
)
INSERT INTO mytable (id, field1, field2)
SELECT id, field1, field2
FROM new_values
WHERE NOT EXISTS (SELECT 1
FROM upsert up
WHERE up.id = new_values.id)
Voir ces entrées de blog:
Notez que cette solution ne pas éviter une violation de clé unique, mais il est vulnérable aux mises à jour perdues.
Voir suivi par Craig Ringer sur dba.stackexchange.com
Dans PostgreSQL 9.5 et plus récente, vous pouvez utiliser INSERT ... ON CONFLICT UPDATE
.
Voir la documentation .
Un INSERT ... ON DUPLICATE KEY UPDATE
MySQL peut être directement reformulé à un ON CONFLICT UPDATE
. Ni est la syntaxe SQL standard, ils sont les deux extensions spécifiques à la base de données. Il y a de bonnes raisons MERGE
n'a pas été utilisé pour cette , une nouvelle syntaxe n'a pas été créé juste pour le plaisir. (La syntaxe de MySQL a aussi des problèmes qui signifient qu'il n'a pas été adopté directement).
par exemple. configuration donnée:
CREATE TABLE tablename (a integer primary key, b integer, c integer);
INSERT INTO tablename (a, b, c) values (1, 2, 3);
la requête MySQL:
INSERT INTO tablename (a,b,c) VALUES (1,2,3)
ON DUPLICATE KEY UPDATE c=c+1;
devient:
INSERT INTO tablename (a, b, c) values (1, 2, 10)
ON CONFLICT (a) DO UPDATE SET c = tablename.c + 1;
Différences:
-
doit indiquer le nom de la colonne (ou le nom de contrainte unique) à utiliser pour le contrôle d'unicité. C'est le
ON CONFLICT (columnname) DO
-
Le
SET
mot-clé doit être utilisé, comme si cela était une déclaration deUPDATE
normale
Il a quelques fonctionnalités intéressantes aussi:
-
Vous pouvez avoir une clause de
WHERE
sur votreUPDATE
(vous permettant de transformer efficacementON CONFLICT UPDATE
enON CONFLICT IGNORE
pour certaines valeurs) -
Le proposée à des fins d'insertion de valeurs sont disponibles en tant que
EXCLUDED
de ligne variable, qui a la même structure que la table cible. Vous pouvez obtenir les valeurs d'origine dans le tableau en utilisant le nom de la table. Donc dans ce casEXCLUDED.c
sera10
(parce que ce que nous avons essayé d'insérer) et"table".c
sera3
parce que c'est la valeur actuelle dans le tableau. Vous pouvez utiliser une ou l'autre dans les expressionsSET
et clauseWHERE
.
Pour des informations sur upsert voir Comment upsert (Merge, INSERT ... MISE À JOUR SUR duplicate) dans PostgreSQL?
Je cherchais la même chose quand je suis venu ici, mais l'absence d'une fonction générique « upsert » me botherd un peu, donc je pensais que vous pouvez simplement passer la mise à jour et insérer sql comme arguments sur cette fonction constituent le manuel
qui ressemblerait à ceci:
CREATE FUNCTION upsert (sql_update TEXT, sql_insert TEXT)
RETURNS VOID
LANGUAGE plpgsql
AS $$
BEGIN
LOOP
-- first try to update
EXECUTE sql_update;
-- check if the row is found
IF FOUND THEN
RETURN;
END IF;
-- not found so insert the row
BEGIN
EXECUTE sql_insert;
RETURN;
EXCEPTION WHEN unique_violation THEN
-- do nothing and loop
END;
END LOOP;
END;
$$;
et peut-être faire ce que vous vouliez d'abord faire, batch « upsert », vous pouvez utiliser Tcl pour diviser les sql_update et boucle les mises à jour individuelles, le coup de preformance sera très faible, voir http://archives.postgresql.org/pgsql-performance/2006-04/msg00557.php
le coût le plus élevé est la requête en cours d'exécution de votre code, du côté de la base de données le coût d'exécution est beaucoup plus petit
Il n'y a pas de commande simple de le faire.
L'approche la plus correcte est d'utiliser la fonction, comme celle de docs .
Une autre solution (bien que pas sûr) est de faire la mise à jour avec le retour, vérifiez les lignes ont été mises à jour, et insérez le reste d'entre eux
Quelque chose le long des lignes de:
update table
set column = x.column
from (values (1,'aa'),(2,'bb'),(3,'cc')) as x (id, column)
where table.id = x.id
returning id;
id en supposant: 2 a été renvoyé:
insert into table (id, column) values (1, 'aa'), (3, 'cc');
Bien sûr, il écopera tôt ou tard (dans un environnement en même temps), car il est la condition de course claire ici, mais le plus souvent cela fonctionnera.
Voici un plus et l'article plus complet sur la sujet .
Personnellement, je l'ai mis en place une « règle » jointe à l'instruction d'insertion. Supposons que vous aviez une table « dns » qui a enregistré hits dns par client sur une base par temps:
CREATE TABLE dns (
"time" timestamp without time zone NOT NULL,
customer_id integer NOT NULL,
hits integer
);
Tu voulais être en mesure de re-insérer des lignes avec des valeurs mises à jour, ou les créer si elles n'existaient déjà. Détrompeur sur le customer_id et le temps. Quelque chose comme ceci:
CREATE RULE replace_dns AS
ON INSERT TO dns
WHERE (EXISTS (SELECT 1 FROM dns WHERE ((dns."time" = new."time")
AND (dns.customer_id = new.customer_id))))
DO INSTEAD UPDATE dns
SET hits = new.hits
WHERE ((dns."time" = new."time") AND (dns.customer_id = new.customer_id));
Mise à jour: Il a le potentiel d'échouer si inserts simultanés se produisent, car il génère des exceptions unique_violation. Cependant, la transaction non terminée continuera et réussir, et il vous suffit de répéter l'opération terminée.
Cependant, s'il y a des tonnes d'inserts qui se produisent tout le temps, vous voulez mettre un verrou de table autour des instructions d'insertion: verrouillage SHARE ROW EXCLUSIVE empêchera toute opération qui pourrait insérer, supprimer ou lignes de mise à jour dans votre table cible. Cependant, les mises à jour qui ne mettent pas à jour la clé unique sont en sécurité, donc si vous ne opération cela, utilisez les verrous consultatifs à la place.
En outre, la commande COPY ne pas utiliser les règles, donc si vous insérez avec copie, vous devrez utiliser des déclencheurs à la place.
I sur mesure "upsert" fonction ci-dessus, si vous voulez insérer et REPLACE:
`
CREATE OR REPLACE FUNCTION upsert(sql_insert text, sql_update text)
RETURNS void AS
$BODY$
BEGIN
-- first try to insert and after to update. Note : insert has pk and update not...
EXECUTE sql_insert;
RETURN;
EXCEPTION WHEN unique_violation THEN
EXECUTE sql_update;
IF FOUND THEN
RETURN;
END IF;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION upsert(text, text)
OWNER TO postgres;`
Et après l'exécution, faire quelque chose comme ceci:
SELECT upsert($$INSERT INTO ...$$,$$UPDATE... $$)
est important de mettre deux dollars par des virgules pour éviter les erreurs du compilateur
- vérifier la vitesse ...
Tout comme réponse la plus populaire, mais fonctionne un peu plus vite:
WITH upsert AS (UPDATE spider_count SET tally=1 WHERE date='today' RETURNING *)
INSERT INTO spider_count (spider, tally) SELECT 'Googlebot', 1 WHERE NOT EXISTS (SELECT * FROM upsert)
(source: http: //www.the-art-of- web.com/sql/upsert/ )
J'ai le même problème pour la gestion des paramètres de compte en tant que paires nom-valeur. Les critères de conception est que les différents clients peuvent avoir différents ensembles de paramètres.
Ma solution, similaire à GTM est d'effacer en vrac et remplacer, génération de l'enregistrement de fusion dans votre application.
est assez pare-balles, la plate-forme indépendante et comme il n'y a jamais plus de 20 paramètres par client, cela est seulement 3 charge assez bas appels db -. Probablement la méthode la plus rapide
L'alternative de la mise à jour des lignes individuelles - la vérification des exceptions, puis l'insertion - ou une combinaison est un code hideux, lent et se casse souvent parce que (comme mentionné ci-dessus) non standards gestion des exceptions SQL passe de db à db - ou même une version à .
#This is pseudo-code - within the application:
BEGIN TRANSACTION - get transaction lock
SELECT all current name value pairs where id = $id into a hash record
create a merge record from the current and update record
(set intersection where shared keys in new win, and empty values in new are deleted).
DELETE all name value pairs where id = $id
COPY/INSERT merged records
END TRANSACTION
Selon le documentation PostgreSQL de la déclaration INSERT
, la manipulation le cas de ON DUPLICATE KEY
n'est pas pris en charge. Cette partie de la syntaxe est une extension MySQL propriétaire.
CREATE OR REPLACE FUNCTION save_user(_id integer, _name character varying)
RETURNS boolean AS
$BODY$
BEGIN
UPDATE users SET name = _name WHERE id = _id;
IF FOUND THEN
RETURN true;
END IF;
BEGIN
INSERT INTO users (id, name) VALUES (_id, _name);
EXCEPTION WHEN OTHERS THEN
UPDATE users SET name = _name WHERE id = _id;
END;
RETURN TRUE;
END;
$BODY$
LANGUAGE plpgsql VOLATILE STRICT
J'utilise cette fusion de fonction
CREATE OR REPLACE FUNCTION merge_tabla(key INT, data TEXT)
RETURNS void AS
$BODY$
BEGIN
IF EXISTS(SELECT a FROM tabla WHERE a = key)
THEN
UPDATE tabla SET b = data WHERE a = key;
RETURN;
ELSE
INSERT INTO tabla(a,b) VALUES (key, data);
RETURN;
END IF;
END;
$BODY$
LANGUAGE plpgsql
Pour la fusion de petits ensembles, en utilisant la fonction ci-dessus est très bien. Toutefois, si vous fusionnez de grandes quantités de données, je vous suggère de regarder dans http: //mbk.projects.postgresql .org
La meilleure pratique actuelle que je suis au courant est:
- Copier / nouvelles données mises à jour dans la table temporaire (vous, ou vous pouvez faire INSERT si le coût est ok)
- Acquisition de verrouillage [facultatif] (conseil est préférable de verrous de table, OMI)
- Fusion. (La partie amusante)
UPDATE retourne le nombre de lignes modifiées. Si vous utilisez JDBC (Java), vous pouvez vérifier cette valeur contre 0 et, si aucune ligne n'a été affectée, le feu INSERT au lieu. Si vous utilisez un autre langage de programmation, peut-être le nombre de lignes modifiées peut encore être obtenu, la documentation de vérification.
Cela peut ne pas être aussi élégant, mais vous avez beaucoup plus simple SQL qui est plus trivial d'utiliser du code d'appel. D'autres termes, si vous écrivez le script de dix lignes en PL / PSQL, vous devriez probablement avoir un test unitaire d'un ou d'une autre nature juste pour elle seule.
Modifier Cela ne fonctionne pas comme prévu. Contrairement à la réponse acceptée, cela produit des violations clés uniques lorsque deux processus appellent à plusieurs reprises upsert_foo
simultanément.
Eureka! Je me suis trouvé un moyen de le faire dans une requête: utiliser UPDATE ... RETURNING
pour tester si des lignes ont été affectées:
CREATE TABLE foo (k INT PRIMARY KEY, v TEXT);
CREATE FUNCTION update_foo(k INT, v TEXT)
RETURNS SETOF INT AS $$
UPDATE foo SET v = $2 WHERE k = $1 RETURNING $1
$$ LANGUAGE sql;
CREATE FUNCTION upsert_foo(k INT, v TEXT)
RETURNS VOID AS $$
INSERT INTO foo
SELECT $1, $2
WHERE NOT EXISTS (SELECT update_foo($1, $2))
$$ LANGUAGE sql;
Le UPDATE
doit être fait dans une procédure distincte parce que, malheureusement, cela est une erreur de syntaxe:
... WHERE NOT EXISTS (UPDATE ...)
Maintenant, il fonctionne comme vous le souhaitez:
SELECT upsert_foo(1, 'hi');
SELECT upsert_foo(1, 'bye');
SELECT upsert_foo(3, 'hi');
SELECT upsert_foo(3, 'bye');