Insérer, lors d'une mise à jour en double dans PostgreSQL?

645

Il y a plusieurs mois, j'ai 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 suis maintenant passé à PostgreSQL et apparemment, ce n'est pas correct. Cela 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 sais pas où dans la documentation PostgreSQL cela est couvert.

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

Teifion
la source
38
Quiconque trouve cette question devrait lire l'article de Depesz "Pourquoi l'upsert est-il si compliqué?" . Il explique très bien le problème et les solutions possibles.
Craig Ringer
8
UPSERT sera ajouté dans Postgres 9.5: wiki.postgresql.org/wiki/…
tommed le
4
@tommed - c'est fait: stackoverflow.com/a/34639631/4418
warren

Réponses:

515

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

La recherche dans les archives du groupe de messagerie de postgresql de "upsert" conduit à trouver un exemple de ce que vous voulez faire, dans le manuel :

Exemple 38-2. Exceptions avec UPDATE / INSERT

Cet exemple utilise la gestion des exceptions pour effectuer 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 masse, en utilisant les CTE dans 9.1 et au-dessus, dans la liste de diffusion des pirates :

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.

Stephen Denne
la source
7
La seule chose que je n'aime pas à ce sujet, c'est que ce serait beaucoup plus lent, car chaque upsert serait son propre appel individuel dans la base de données.
baash05
@ baash05 il pourrait y avoir un moyen de le faire en masse, voir ma réponse mise à jour.
Stephen Denne
2
La seule chose que je ferais différemment est d'utiliser FOR 1..2 LOOP au lieu de simplement LOOP afin que si une autre contrainte unique est violée, elle ne tournera pas indéfiniment.
olamork
2
À quoi fait excludedréférence la première solution ici?
ichbinallen
2
@ichbinallen dans la documentation Les clauses SET et WHERE de ON CONFLICT DO UPDATE ont accès à la ligne existante en utilisant le nom de la table (ou un alias), et aux lignes proposées pour l'insertion à l'aide de la table spéciale exclue . Dans ce cas, le excludedtableau spécial vous donne accès aux valeurs que vous tentiez d'insérer en premier lieu.
TMichel
429

Avertissement: ce n'est pas sûr s'il est exécuté à partir de plusieurs sessions en même temps (voir les mises en garde ci-dessous).


Une autre façon intelligente de faire un "UPSERT" dans postgresql est de faire deux instructions UPDATE / INSERT séquentielles conçues chacune pour réussir ou pour 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);

La MISE À JOUR réussira si une ligne avec "id = 3" existe déjà, sinon elle n'a 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 tous les deux avec une seule instruction SQL exécutée à partir de votre application. Les exécuter ensemble en une seule transaction est fortement recommandé.

Cela fonctionne très bien lorsqu'il est exécuté de manière isolée ou sur une table verrouillée, mais est soumis à des conditions de concurrence qui signifie qu'il peut toujours échouer avec une erreur de clé en double 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 SERIALIZABLEtransaction sur PostgreSQL 9.1 ou supérieur la traitera de manière fiable au prix d'un taux d'échec de sérialisation très élevé, ce qui signifie que vous devrez réessayer beaucoup. Voyez pourquoi est upsert si compliqué , qui traite de ce cas plus en détail.

Cette approche est également sujette à des mises à jour perdues read committedisolément, à moins que l'application ne vérifie le nombre de lignes affectées et ne vérifie que la insertou la ligne updateaffectée .

bovine
la source
6
Réponse courte: si l'enregistrement existe, l'INSERT ne fait rien. Réponse longue: SELECT dans INSERT renverra autant de résultats qu'il y a de correspondances avec la clause where. C'est au plus un (si le numéro un n'est pas dans le résultat de la sous-sélection), sinon zéro. L'INSERT ajoutera donc une ou zéro ligne.
Peter Becker
3
la partie «où» peut être simplifiée en utilisant existe:... where not exists (select 1 from table where id = 3);
Endy Tjahjono
1
cela devrait être la bonne réponse .. avec quelques ajustements mineurs, il pourrait être utilisé pour faire une mise à jour de masse .. Humm .. Je me demande si une table temporaire pourrait être utilisée ..
baash05
1
@keaplogik, cette limitation 9.1 concerne les CTE inscriptibles (expressions de table communes) qui sont décrites dans une autre des réponses. La syntaxe utilisée dans cette réponse est très basique et a longtemps été prise en charge.
bovin
8
Attention, cela peut faire l'objet de mises à jour read committedisolées, sauf si votre application vérifie que le insertou le nombre de updatelignes est différent de zéro. Voir dba.stackexchange.com/q/78510/7788
Craig Ringer
227

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

un cheval sans nom
la source
1
@ FrançoisBeausoleil: les chances d'une condition de course sont beaucoup plus faibles qu'avec l'approche "exception try / handle"
a_horse_with_no_name
2
@a_horse_with_no_name Comment voulez-vous dire exactement que la chance sur les conditions de course est beaucoup plus petite? Lorsque j'exécute cette requête simultanément avec les mêmes enregistrements, j'obtiens l'erreur "la valeur de la clé en double viole la contrainte unique" 100% des fois jusqu'à ce que la requête détecte que l'enregistrement a été inséré. Est-ce un exemple complet?
Jeroen van Dijk
4
@a_horse_with_no_name Votre solution semble fonctionner dans des situations simultanées lorsque vous encapsulez l'instruction upsert avec le verrou suivant: BEGIN WORK; VERROUILLER LA TABLE mytable EN MODE EXCLUSIF DE PARTAGE DE RANGÉES; <UPSERT ICI>; COMMIT WORK;
Jeroen van Dijk
2
@JeroenvanDijk: merci. Ce que je voulais dire par "beaucoup plus petit", c'est que si plusieurs transactions (et commettent le changement!), L'intervalle de temps entre la mise à jour et l'insertion est plus petit, car tout n'est qu'une seule instruction. Vous pouvez toujours générer une violation de pk par deux instructions INSERT indépendantes. Si vous verrouillez la table entière, vous sérialisez efficacement tous les accès à celle-ci (quelque chose que vous pourriez également réaliser avec le niveau d'isolement sérialisable).
a_horse_with_no_name
12
Cette solution est sujette à des mises à jour perdues si la transaction d'insertion est annulée; il n'y a aucune vérification pour faire en sorte que les UPDATElignes concernées soient affectées.
Craig Ringer
132

Dans PostgreSQL 9.5 et plus récent, vous pouvez utiliser INSERT ... ON CONFLICT UPDATE.

Consultez la documentation .

Un MySQL INSERT ... ON DUPLICATE KEY UPDATEpeut être directement reformulé en a ON CONFLICT UPDATE. La syntaxe standard SQL n'est pas non plus, ce sont deux extensions spécifiques à la base de données. Il y a de bonnes raisons pour lesquelles cela MERGEn'a pas été utilisé , une nouvelle syntaxe n'a pas été créée juste 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 colonne (ou le nom de contrainte unique) à utiliser pour la vérification d'unicité. C'est leON CONFLICT (columnname) DO

  • Le mot-clé SETdoit être utilisé, comme s'il s'agissait d'une UPDATEinstruction normale

Il a aussi de belles fonctionnalités:

  • Vous pouvez avoir une WHEREclause sur votre UPDATE(vous permettant de transformer efficacement ON CONFLICT UPDATEen ON CONFLICT IGNOREcertaines valeurs)

  • Les valeurs proposées pour l'insertion sont disponibles en tant que 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, ce EXCLUDED.csera 10(parce que c'est ce que nous avons essayé d'insérer) et ce "table".csera 3parce que c'est la valeur actuelle dans le tableau. Vous pouvez utiliser l'un ou les deux dans les SETexpressions et la WHEREclause.

Pour des informations sur upsert, voir Comment UPSERT (MERGE, INSERT ... ON DUPLICATE UPDATE) dans PostgreSQL?

Craig Ringer
la source
J'ai examiné la solution 9.5 de PostgreSQL comme vous l'avez décrit ci-dessus car je rencontrais des lacunes dans le champ d'incrémentation automatique sous MySQL ON DUPLICATE KEY UPDATE. J'ai téléchargé Postgres 9.5 et implémenté votre code mais étrangement le même problème se produit sous Postgres: le champ série de la clé primaire n'est pas consécutif (il y a des écarts entre les insertions et les mises à jour.). Une idée de ce qui se passe ici? Est-ce normal? Une idée comment éviter ce comportement? Je vous remercie.
WM
@WM C'est à peu près inhérent à une opération upsert. Vous devez évaluer la fonction qui génère la séquence avant de tenter l'insertion. Étant donné que ces séquences sont conçues pour fonctionner simultanément, elles sont exemptées de la sémantique de transaction normale, mais même si elles n'étaient pas la génération n'est pas appelée dans une sous-transaction et annulée, elle se termine normalement et s'engage avec le reste de l'opération. Cela se produirait donc même avec des implémentations de séquences "sans espace". La seule façon dont la base de données pourrait éviter cela serait de retarder l'évaluation de la génération de séquence jusqu'à la fin de la vérification des clés.
Craig Ringer
1
@WM qui créerait ses propres problèmes. Fondamentalement, vous êtes coincé. Mais si vous comptez sur le fait que serial / auto_increment est sans espace, vous avez déjà des bugs. Vous pouvez avoir des lacunes de séquence en raison de restaurations, y compris des erreurs transitoires - redémarrages sous charge, erreurs client en cours de transaction, plantages, etc. Vous ne devez jamais, jamais compter sur SERIAL/ SEQUENCEou AUTO_INCREMENTne pas avoir de lacunes. Si vous avez besoin de séquences sans espace, elles sont plus complexes; vous devez généralement utiliser une table de comptoir. Google vous en dira plus. Mais sachez que les séquences sans espace empêchent toutes les insertions simultanées.
Craig Ringer
@WM Si vous avez absolument besoin de séquences sans interruption et de upsert, vous pouvez utiliser l'approche upsert basée sur les fonctions discutée dans le manuel avec une implémentation de séquence sans interruption qui utilise une table de comptage. Étant donné que les BEGIN ... EXCEPTION ...exécutions dans une sous-transaction qui est annulée en cas d'erreur, votre incrément de séquence serait annulé en cas d' INSERTéchec.
Craig Ringer
Merci beaucoup @Craig Ringer, c'était assez instructif. J'ai réalisé que je pouvais tout simplement renoncer à avoir cette clé primaire d'incrémentation automatique. J'ai fait un primaire composite de 3 champs et pour mon besoin actuel particulier, il n'y a vraiment pas besoin d'un champ d'incrémentation automatique sans espace. Merci encore, les informations que vous avez fournies me feraient gagner du temps à l'avenir en essayant d'empêcher un comportement DB naturel et sain. Je le comprends mieux maintenant.
WM
17

Je cherchais la même chose quand je suis venu ici, mais le manque d'une fonction générique "upsert" m'a un peu dérangé alors j'ai pensé que vous pouviez simplement passer la mise à jour et insérer sql comme arguments sur cette fonction du 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 pour faire ce que vous vouliez initialement faire, batch "upsert", vous pouvez utiliser Tcl pour diviser sql_update et boucler les mises à jour individuelles, le hit de pré-performance sera très petit voir 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 faible

Paul Scheltema
la source
3
Vous devez toujours l'exécuter dans une boucle de nouvelle tentative et il est sujet à des courses avec un concurrent à DELETEmoins que vous ne verrouilliez la table ou que vous soyez en SERIALIZABLEisolation de transaction sur PostgreSQL 9.1 ou supérieur.
Craig Ringer
13

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

L'approche la plus correcte consiste à utiliser la fonction, comme celle des documents .

Une autre solution (bien que moins sûre) consiste à effectuer une mise à jour avec retour, à vérifier quelles lignes étaient des mises à jour et à insérer les autres

Quelque chose dans le sens 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 id: 2 a été renvoyé:

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

Bien sûr, il renflouera tôt ou tard (dans un environnement simultané), car il y a une condition de course claire ici, mais généralement cela fonctionnera.

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

Craig Ringer
la source
1
Si vous utilisez cette option, assurez-vous de vérifier que l'ID est retourné même si la mise à jour ne fait rien. J'ai vu des bases de données optimiser des requêtes comme "Mettre à jour la table foo set bar = 4 where bar = 4".
thelem
10

Personnellement, j'ai mis en place une "règle" attachée à l'instruction d'insertion. Supposons que vous disposiez d'une table "DNS" qui enregistre les hits DNS par client sur une base de temps:

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à. Saisissez l'identifiant client 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 de violation unique. Cependant, la transaction non terminée se poursuivra et réussira, et il vous suffit de répéter la transaction terminée.

Cependant, s'il y a des tonnes d'insertions tout le temps, vous souhaiterez mettre un verrou de table autour des instructions d'insertion: le verrouillage SHARE ROW EXCLUSIVE empêchera toutes les opérations qui pourraient insérer, supprimer ou mettre à jour des lignes dans votre table cible. Cependant, les mises à jour qui ne mettent pas à jour la clé unique sont sûres. Par conséquent, si aucune opération ne le fait, utilisez plutôt des verrous consultatifs.

De plus, la commande COPY n'utilise pas de RULES, donc si vous insérez avec COPY, vous devrez utiliser des déclencheurs à la place.

Ch'marr
la source
9

J'utilise cette fonction de fusion

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
Mise
la source
1
Il est plus efficace de simplement faire la updatepremière, puis de vérifier le nombre de lignes mises à jour. (Voir la réponse d'Ahmad)
a_horse_with_no_name
8

J'ai personnalisé 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 ceci:

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

Est important de mettre une double virgule pour éviter les erreurs du compilateur

  • vérifier la vitesse ...
Felipe FMMobile
la source
7

Semblable à la réponse la plus appréciée, mais fonctionne légèrement plus rapidement:

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

alexkovelsky
la source
3
Cela échouera s'il est exécuté simultanément dans deux sessions, car aucune mise à jour ne verra une ligne existante, les deux mises à jour atteindront donc zéro ligne, donc les deux requêtes émettront une insertion.
Craig Ringer
6

J'ai le même problème de gestion des paramètres de compte que les paires de valeurs de nom. Le critère de conception est que différents clients peuvent avoir des ensembles de paramètres différents.

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

C'est assez à l'épreuve des balles, indépendant de la plate-forme et comme il n'y a jamais plus de 20 paramètres par client, ce ne sont que 3 appels db à charge assez faible - probablement la méthode la plus rapide.

L'alternative de mettre à jour des lignes individuelles - vérifier les exceptions puis insérer - ou une combinaison de codes hideux, lents et se casse souvent parce que (comme mentionné ci-dessus) la gestion des exceptions SQL non standard passant de db à db - ou même de version en 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
Benno
la source
Bienvenue chez SO. Belle introduction! :-)
Don Question
1
Cela ressemble plus à REPLACE INTOque INSERT INTO ... ON DUPLICATE KEY UPDATE, ce qui peut provoquer un problème si vous utilisez des déclencheurs. Vous finirez par exécuter la suppression et l'insertion de déclencheurs / règles, plutôt que de mettre à jour ceux.
cHao
5

Selon la documentation PostgreSQL de l' INSERTinstruction , la gestion du ON DUPLICATE KEYcas n'est pas prise en charge. Cette partie de la syntaxe est une extension MySQL propriétaire.

Christian Hang-Hicks
la source
@Lucian MERGEest aussi vraiment plus une opération OLAP; voir stackoverflow.com/q/17267417/398670 pour des explications. Il ne définit pas la sémantique de concurrence et la plupart des gens qui l'utilisent pour upsert ne font que créer des bugs.
Craig Ringer
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
Ahmad
la source
5

Pour fusionner de petits ensembles, l'utilisation de la fonction ci-dessus est très bien. Cependant, si vous fusionnez de grandes quantités de données, je vous suggère de consulter http://mbk.projects.postgresql.org

La meilleure pratique actuelle que je connaisse est:

  1. COPIEZ les données nouvelles / mises à jour dans la table temporaire (bien sûr, ou vous pouvez l'insérer si le coût est correct)
  2. Acquérir le verrou [facultatif] (le conseil est préférable aux verrous de table, IMO)
  3. Fusionner. (la partie amusante)
jwp
la source
5

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, peut-être que le nombre de lignes modifiées peut encore être obtenu, consultez la documentation.

Cela peut ne pas être aussi élégant, mais vous disposez d'un SQL beaucoup plus simple et plus simple à utiliser à partir du code appelant. Différemment, si vous écrivez le script de dix lignes en PL / PSQL, vous devriez probablement avoir un test unitaire de l'un ou l'autre type juste pour lui seul.

Audrius Meskauskas
la source
4

Modifier: 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_foosimultanément.

Eureka! J'ai trouvé un moyen de le faire dans une seule requête: utilisez UPDATE ... RETURNINGpour 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 UPDATEdoit être fait dans une procédure distincte parce que, malheureusement, cela est 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');
Joey Adams
la source
1
Vous pouvez les combiner en une seule instruction si vous utilisez un CTE inscriptible. Mais comme la plupart des solutions publiées ici, celle-ci est erronée et échouera en présence de mises à jour simultanées.
Craig Ringer