web-dev-qa-db-fra.com

Insérer, lors de la mise à jour en double dans PostgreSQL?

Il y a plusieurs mois, une réponse à Stack Overflow m'a appris à effectuer plusieurs mises à jour simultanément dans MySQL à l'aide de 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 suis maintenant passé à PostgreSQL et ce n'est apparemment pas correct. Il fait référence à toutes les tables correctes, donc je suppose que c'est une question de mots-clés différents utilisés, mais je ne suis pas sûr de savoir dans quelle partie de la documentation PostgreSQL cela est couvert.

Pour clarifier, je veux insérer plusieurs choses et si elles existent déjà pour les mettre à jour.

593
Teifion

PostgreSQL depuis la version 9.5 a la syntaxe UPSERT , avec la clause ON CONFLICT . avec ce qui suit syntaxe (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;

La recherche dans les archives du groupe de courrier électronique de postgresql pour "upsert" mène à la recherche n exemple de ce que vous voulez éventuellement faire, dans le manuel :

Exemple 38-2. Exceptions avec UPDATE/INSERT

Cet exemple utilise la gestion des exceptions pour exécuter 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 existe peut-être un exemple montrant comment procéder en bloc, en utilisant les CTE de la version 9.1 et ultérieure, dans la liste de diffusion hackers :

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 la réponse de a_horse_with_no_name pour un exemple plus clair.

449
Stephen Denne

Avertissement: ceci n'est pas sûr si elle est exécutée à partir de plusieurs sessions simultanément (voir mises en garde ci-dessous).


Un autre moyen astucieux de réaliser un "UPSERT" dans postgresql consiste à effectuer deux instructions séquentielles UPDATE/INSERT conçues chacune pour réussir ou n’avoir aucun 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);

UPDATE réussira si une ligne avec "id = 3" existe déjà, sinon elle n'aura aucun effet.

L'INSERT ne réussira que si la ligne avec "id = 3" n'existe pas déjà.

Vous pouvez combiner ces deux éléments en une seule chaîne et les exécuter avec une seule instruction SQL exécutée à partir de votre application. Les exécuter ensemble dans une seule transaction est fortement recommandé.

Cela fonctionne très bien lorsqu'il est exécuté isolément ou sur une table verrouillée, mais est soumis à des conditions de concurrence qui signifient qu'il peut toujours échouer avec une erreur de clé dupliquée si une ligne est insérée simultanément ou peut se terminer sans aucune ligne insérée lorsqu'une ligne est supprimée simultanément . Une transaction SERIALIZABLE sur PostgreSQL 9.1 ou une version ultérieure la traitera de manière fiable au prix d'un très grand nombre d'échecs de sérialisation, ce qui signifie que vous devrez réessayer beaucoup. Voir pourquoi l'upert est-il si compliqué , qui traite de ce cas plus en détail.

Cette approche est également sous réserve des mises à jour perdues dans l'isolement read committed, sauf si l'application vérifie le nombre de lignes affectées et vérifie que la insert ou la update a affecté une ligne .

419
bovine

Avec PostgreSQL 9.1, cela peut être réalisé en utilisant un CTE en écriture ( 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 n'empêche pas une violation de clé unique, mais elle n'est pas vulnérable aux mises à jour perdues.
Voir le suivi de Craig Ringer sur dba.stackexchange.com

223

Dans PostgreSQL 9.5 et versions ultérieures, vous pouvez utiliser INSERT ... ON CONFLICT UPDATE.

Voir la documentation .

Un MySQL INSERT ... ON DUPLICATE KEY UPDATE peut être directement reformulé en un ON CONFLICT UPDATE. La syntaxe n'est pas non plus la norme SQL, ce sont deux extensions spécifiques à la base de données. Il y a de bonnes raisons que MERGE n'ait pas été utilisé pour cela , une nouvelle syntaxe n'a pas été créée pour le plaisir. (La syntaxe de MySQL a également des problèmes qui signifient qu'elle n'a pas été adoptée 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:

  • Vous devez spécifier le nom de la colonne (ou le nom de contrainte unique) à utiliser pour la vérification de l'unicité. C'est la ON CONFLICT (columnname) DO

  • Le mot clé SET doit être utilisé, comme s'il s'agissait d'une instruction normale UPDATE.

Il a aussi quelques fonctionnalités intéressantes:

  • Vous pouvez avoir une clause WHERE sur votre UPDATE (vous permettant de transformer efficacement ON CONFLICT UPDATE en ON CONFLICT IGNORE pour certaines valeurs)

  • Les valeurs proposées pour l'insertion sont disponibles dans la variable de ligne EXCLUDED, qui a la même structure que la table cible. Vous pouvez obtenir les valeurs d'origine dans la table en utilisant le nom de la table. Donc, dans ce cas, EXCLUDED.c sera 10 (car c'est ce que nous avons essayé d'insérer) et "table".c sera 3, car c'est la valeur actuelle de la table. Vous pouvez utiliser les expressions SET et la clause WHERE, ou les deux.

Pour plus d'informations sur upsert, voir Comment UPSERT (FUSIONNER, INSÉRER ... SUR UNE MISE À JOUR DOUBLE) dans PostgreSQL?

120
Craig Ringer

Je cherchais la même chose quand je suis arrivé ici, mais l'absence d'une fonction générique "upsert" m'a un peu gêné, alors j'ai pensé que vous pouviez simplement passer la mise à jour et insérer SQL comme argument de cette fonction dans le manuel.

cela 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 que pour faire ce que vous vouliez initialement, lot "upsert", vous pouvez utiliser Tcl pour scinder le sql_update et mettre en boucle les mises à jour individuelles, le hit de préformance sera très petit, voyez http: //archives.postgresql. org/pgsql-performance/2006-04/msg00557.php

le coût le plus élevé est l'exécution de la requête à partir de votre code; du côté de la base de données, le coût d'exécution est beaucoup plus petit

17
Paul Scheltema

Il n'y a pas de commande simple pour le faire.

L’approche la plus correcte consiste à utiliser une fonction, comme celle de docs .

Une autre solution (bien que pas si sûre) consiste à mettre à jour avec le renvoi, vérifier quelles lignes étaient des mises à jour et insérer le reste

Quelque chose dans le genre 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;

en supposant que l'id: 2 a été renvoyé:

insert into table (id, column) values (1, 'aa'), (3, 'cc');

Bien sûr, il va se sauver tôt ou tard (dans un environnement concurrentiel), car la situation de concurrence est claire, mais cela fonctionnera généralement.

Voici un article plus long et plus complet sur le sujet .

13
user80168

Personnellement, j'ai mis en place une "règle" attachée à la déclaration insert. Supposons que vous disposiez d'une table "DNS" enregistrant les hits DNS par client à la fois:

CREATE TABLE dns (
    "time" timestamp without time zone NOT NULL,
    customer_id integer NOT NULL,
    hits integer
);

Vous vouliez pouvoir ré-insérer des lignes avec des valeurs mises à jour ou les créer si elles n'existaient pas déjà. Entrez le customer_id et l'heure. Quelque chose comme ça:

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: Cela peut échouer si des insertions simultanées se produisent, car cela générera des exceptions unique_violation. Toutefois, la transaction non terminée continuera et réussira et il vous suffira de répéter la transaction terminée.

Toutefois, si des tonnes d'insertions se produisent tout le temps, vous voudrez peut-être verrouiller les instructions d'insertion avec un verrou de table: le verrouillage SHARE ROW EXCLUSIVE empêchera toute opération susceptible d'insérer, de supprimer ou de mettre à jour des lignes dans votre table cible. Toutefois, les mises à jour qui ne mettent pas à jour la clé unique sont sécurisées. Par conséquent, si aucune opération ne le permet, utilisez plutôt des verrous d'avertissement.

De plus, la commande COPY n'utilise pas de règles. Par conséquent, si vous insérez avec COPY, vous devrez utiliser des déclencheurs.

9
Ch'marr

Je personnalise la fonction "upsert" ci-dessus, si vous voulez INSÉRER ET REMPLACER:

`

 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, faites quelque chose comme ça:

SELECT upsert($$INSERT INTO ...$$,$$UPDATE... $$)

Il est important de mettre une double virgule pour éviter les erreurs de compilation

  • vérifier la vitesse ...
8
Felipe FMMobile

Semblable à la réponse la plus aimée, mais fonctionne légèrement 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/ )

7
alexkovelsky

J'ai le même problème pour la gestion des paramètres de compte que les paires nom-valeur. Les critères de conception sont que différents clients peuvent avoir différents ensembles de paramètres.

Ma solution, similaire à JWP, consiste à effacer et à remplacer en bloc, en générant l'enregistrement de fusion au sein de votre application.

C’est une bonne solution, indépendante de la plate-forme, et comme il n’ya jamais plus de 20 paramètres par client, il ne s’agit que de 3 appels de base de données à charge relativement faible, probablement la méthode la plus rapide.

L'alternative consiste à mettre à jour des lignes individuelles - vérifier les exceptions puis insérer - ou une combinaison de code hideux, lent et souvent interrompu, car (comme mentionné ci-dessus) la gestion des exceptions non standard SQL passant d'une base à une autre - voire d'une version à l'autre.

 #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
6
benno

UPDATE renverra le nombre de lignes modifiées. Si vous utilisez JDBC (Java), vous pouvez alors comparer cette valeur à 0 et, si aucune ligne n'a été affectée, déclencher INSERT à la place. Si vous utilisez un autre langage de programmation, vous pouvez peut-être obtenir le nombre de lignes modifiées. Consultez la documentation.

Cela n’est peut-être pas aussi élégant mais vous avez un code SQL beaucoup plus simple et plus simple à utiliser à partir du code appelant. En revanche, si vous écrivez le script de dix lignes dans PL/PSQL, vous devriez probablement avoir un test unitaire d'un type ou d'un autre, rien que pour cela.

5
h22

Pour la fusion de petits ensembles, l’utilisation de la fonction ci-dessus convient. Cependant, si vous fusionnez de grandes quantités de données, je suggérerais de regarder dans http://mbk.projects.postgresql.org

La meilleure pratique actuelle que je connaisse est la suivante:

  1. COPIER les données nouvelles/mises à jour dans la table temporaire (bien sûr, ou vous pouvez faire INSERT si le coût est correct)
  2. Acquire Lock [facultatif] (l'avis est préférable aux verrous de table, IMO)
  3. Fusionner. (la partie amusante)
5
jwp

Selon la documentation PostgreSQL de l'instruction INSERT) , le traitement de la casse ON DUPLICATE KEY n'est pas pris en charge. Cette partie de la syntaxe est une extension propriétaire de MySQL.

5
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
5
Ahmad

J'utilise cette fonction merge

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
5
Mise

Edit: Cela ne fonctionne pas comme prévu. Contrairement à la réponse acceptée, cela produit des violations de clé uniques lorsque deux processus appellent à plusieurs reprises upsert_foo simultanément.

Eureka! J'ai trouvé un moyen de le faire en une requête: utilisez UPDATE ... RETURNING pour tester si des lignes étaient 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;

La UPDATE doit être effectuée dans une procédure séparée car, malheureusement, il s'agit d'une erreur de syntaxe:

... WHERE NOT EXISTS (UPDATE ...)

Maintenant cela 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');
4
Joey Adams