Oracle: comment UPSERT (mettre à jour ou insérer dans une table?)

293

L'opération UPSERT met à jour ou insère une ligne dans une table, selon que la table a déjà une ligne qui correspond aux données:

if table t has a row exists that has key X:
    update t set mystuff... where mykey=X
else
    insert into t mystuff...

Étant donné qu'Oracle ne dispose pas d'une instruction UPSERT spécifique, quelle est la meilleure façon de procéder?

Mark Harrison
la source

Réponses:

60

Une alternative à MERGE ("à l'ancienne"):

begin
   insert into t (mykey, mystuff) 
      values ('X', 123);
exception
   when dup_val_on_index then
      update t 
      set    mystuff = 123 
      where  mykey = 'X';
end;   
Tony Andrews
la source
3
@chotchki: vraiment? Une explication serait utile.
Tony Andrews
15
Le problème est que vous avez une fenêtre entre l'insertion et la mise à jour où un autre processus peut réussir à supprimer une suppression. J'ai cependant utilisé ce modèle sur une table qui n'a jamais été supprimée.
chotchki
2
OK je suis d'accord. Je ne sais pas pourquoi ce n'était pas évident pour moi.
Tony Andrews
4
Je ne suis pas d'accord avec Chotchki. "Durée de verrouillage: tous les verrous acquis par des instructions dans une transaction sont conservés pendant la durée de la transaction, empêchant les interférences destructrices, y compris les lectures incorrectes, les mises à jour perdues et les opérations DDL destructrices des transactions simultanées." Souce: lien
yohannc
5
@yohannc: Je pense que le fait est que nous n'avons acquis aucun verrou simplement en essayant et en échouant d'insérer une ligne.
Tony Andrews,
211

L' instruction MERGE fusionne les données entre deux tables. L'utilisation de DUAL nous permet d'utiliser cette commande. Notez que ce n'est pas protégé contre l'accès simultané.

create or replace
procedure ups(xa number)
as
begin
    merge into mergetest m using dual on (a = xa)
         when not matched then insert (a,b) values (xa,1)
             when matched then update set b = b+1;
end ups;
/
drop table mergetest;
create table mergetest(a number, b number);
call ups(10);
call ups(10);
call ups(20);
select * from mergetest;

A                      B
---------------------- ----------------------
10                     2
20                     1
Mark Harrison
la source
57
Apparemment, l'instruction "fusionner dans" n'est pas atomique. Il peut en résulter «ORA-0001: contrainte unique» lorsqu'il est utilisé simultanément. La vérification de l'existence d'une correspondance et l'insertion d'un nouvel enregistrement ne sont pas protégées par un verrou, il existe donc une condition de concurrence. Pour ce faire de manière fiable, vous devez intercepter cette exception et réexécuter la fusion ou effectuer une simple mise à jour à la place. Dans Oracle 10, vous pouvez utiliser la clause "log errors" pour la faire continuer avec le reste des lignes lorsqu'une erreur se produit et enregistrer la ligne incriminée dans une autre table, plutôt que de simplement arrêter.
Tim Sylvester
1
Salut, j'ai essayé d'utiliser le même modèle de requête dans ma requête, mais ma requête insère en quelque sorte des lignes en double. Je ne trouve pas plus d'informations sur la table DUAL. Quelqu'un peut-il me dire où puis-je obtenir des informations sur DUAL et également sur la syntaxe de fusion?
Shekhar
5
@Shekhar Dual est une table factice avec une seule ligne et une seule colonne adp-gmbh.ch/ora/misc/dual.html
YogoZuno
7
@TimSylvester - Oracle utilise les transactions, ce qui garantit que l'instantané des données au début d'une transaction est cohérent tout au long de la transaction, sauf les modifications apportées en son sein. Les appels simultanés à la base de données utilisent la pile d'annulation; Oracle gérera donc l'état final en fonction de l'ordre de début / fin des transactions simultanées. Ainsi, vous n'aurez jamais de condition de concurrence critique si une vérification des contraintes est effectuée avant l'insertion, quel que soit le nombre d'appels simultanés effectués vers le même code SQL. Dans le pire des cas, vous pouvez obtenir beaucoup de conflits et Oracle prendra beaucoup plus de temps pour atteindre un état final.
Neo
2
@RandyMagruder Est-il vrai que son 2015 et nous ne pouvons toujours pas faire un upsert de manière fiable dans Oracle! Connaissez-vous une solution sûre simultanée?
dan b
105

Le double exemple ci-dessus qui est en PL / SQL était génial car je voulais faire quelque chose de similaire, mais je le voulais côté client ... alors voici le SQL que j'ai utilisé pour envoyer une instruction similaire directement à partir de C #

MERGE INTO Employee USING dual ON ( "id"=2097153 )
WHEN MATCHED THEN UPDATE SET "last"="smith" , "name"="john"
WHEN NOT MATCHED THEN INSERT ("id","last","name") 
    VALUES ( 2097153,"smith", "john" )

Cependant, d'un point de vue C #, cela est plus lent que de faire la mise à jour et de voir si les lignes affectées étaient égales à 0 et de faire l'insertion si c'était le cas.

MyDeveloperDay
la source
10
Je suis revenu ici pour vérifier à nouveau ce modèle. Il échoue silencieusement lorsque des insertions simultanées sont tentées. Une insertion prend effet, la seconde ne fusionne ni insertions ni mises à jour. Cependant, l'approche plus rapide consistant à effectuer deux instructions distinctes est sûre.
Synesso
3
les débutants de l'oralcle comme moi peuvent demander ce qu'est cette double table voir ceci: stackoverflow.com/q/73751/808698
Hajo Thelen
5
Dommage qu'avec ce modèle nous ayons besoin d' écrire deux fois les données (John, Smith ...). Dans ce cas, je gagne rien en utilisant MERGE, et je préfère utiliser beaucoup plus simple DELETEalors INSERT.
Nicolas Barbulesco
@NicolasBarbulesco cette réponse n'a pas besoin d'écrire les données deux fois: stackoverflow.com/a/4015315/8307814
pourquoi
@NicolasBarbulescoMERGE INTO mytable d USING (SELECT 1 id, 'x' name from dual) s ON (d.id = s.id) WHEN MATCHED THEN UPDATE SET d.name = s.name WHEN NOT MATCHED THEN INSERT (id, name) VALUES (s.id, s.name);
Whyer
46

Une autre alternative sans vérification d'exception:

UPDATE tablename
    SET val1 = in_val1,
        val2 = in_val2
    WHERE val3 = in_val3;

IF ( sql%rowcount = 0 )
    THEN
    INSERT INTO tablename
        VALUES (in_val1, in_val2, in_val3);
END IF;
Brian Schmitt
la source
Votre solution fournie ne fonctionne pas pour moi. Est-ce que% rowcount fonctionne uniquement avec des curseurs explicites?
Synesso
Que se passe-t-il si la mise à jour renvoie 0 lignes modifiées car l'enregistrement est déjà là et les valeurs sont les mêmes?
Adriano Varoli Piazza
10
@Adriano: sql% rowcount renverra toujours> 0 si la clause WHERE correspond à des lignes, même si la mise à jour ne modifie en fait aucune donnée sur ces lignes.
Tony Andrews
Ne fonctionne pas: PLS-00207: l'identifiant 'COUNT', appliqué au curseur implicite SQL, n'est pas un attribut de curseur légal
Patrik Beck
Erreurs de syntaxe ici :(
ilmirons
27
  1. insérer s'il n'existe pas
  2. mettre à jour:
    
INSÉRER DANS mytable (id1, t1) 
  SELECT 11, 'x1' FROM DUAL 
  O NOT N'EXISTE PAS (CHOISIR id1 DANS mytble O WH id1 = 11); 

UPDATE mytable SET t1 = 'x1' WHERE id1 = 11;
test1
la source
26

Aucune des réponses données jusqu'à présent n'est sûre face aux accès simultanés , comme indiqué dans le commentaire de Tim Sylvester, et soulèvera des exceptions en cas de courses. Pour résoudre ce problème, le combo d'insertion / mise à jour doit être enveloppé dans une sorte d'instruction de boucle, de sorte qu'en cas d'exception, le tout soit réessayé.

À titre d'exemple, voici comment le code de Grommit peut être encapsulé dans une boucle pour le rendre sûr lorsqu'il est exécuté simultanément:

PROCEDURE MyProc (
 ...
) IS
BEGIN
 LOOP
  BEGIN
    MERGE INTO Employee USING dual ON ( "id"=2097153 )
      WHEN MATCHED THEN UPDATE SET "last"="smith" , "name"="john"
      WHEN NOT MATCHED THEN INSERT ("id","last","name") 
        VALUES ( 2097153,"smith", "john" );
    EXIT; -- success? -> exit loop
  EXCEPTION
    WHEN NO_DATA_FOUND THEN -- the entry was concurrently deleted
      NULL; -- exception? -> no op, i.e. continue looping
    WHEN DUP_VAL_ON_INDEX THEN -- an entry was concurrently inserted
      NULL; -- exception? -> no op, i.e. continue looping
  END;
 END LOOP;
END; 

NB En mode transaction SERIALIZABLE, que je ne recommande pas btw, vous pourriez rencontrer ORA-08177: impossible de sérialiser l'accès pour cette exception de transaction à la place.

Eugene Beresovsky
la source
3
Excellent! Enfin, une réponse simultanée accède en toute sécurité. Est-il possible d'utiliser une telle construction à partir d'un client (par exemple à partir d'un client Java)?
Sebien
1
Vous voulez dire ne pas avoir à appeler un proc stocké? Eh bien, dans ce cas, vous pouvez également intercepter les exceptions Java spécifiques et réessayer dans une boucle Java. C'est beaucoup plus pratique en Java qu'en SQL Oracle.
Eugene Beresovsky
Je suis désolé: je n'étais pas assez précis. Mais vous avez bien compris. J'ai démissionné pour faire comme vous venez de le dire. Mais je ne suis pas satisfait à 100% car cela génère plus de requêtes SQL, plus d'allers-retours client / serveur. Ce n'est pas une bonne solution en termes de performances. Mais mon objectif est de laisser les développeurs Java de mon projet utiliser ma méthode pour faire une upsert dans n'importe quelle table (je ne peux pas créer une procédure stockée PLSQL par table, ou une procédure par type upsert).
Sebien
@Sebien Je suis d'accord, il serait plus agréable de l'encapsuler dans le domaine SQL, et je pense que vous pouvez le faire. Je ne me porte pas volontaire pour le découvrir ... :) De plus, en réalité, ces exceptions se produiront probablement moins d'une fois dans une lune bleue, donc vous ne devriez pas voir d'impact sur les performances dans 99,9% des cas. Sauf lors des tests de charge bien sûr ...
Eugene Beresovsky
24

Je voudrais une réponse de Grommit, sauf qu'elle nécessite des valeurs de dupe. J'ai trouvé une solution où elle peut apparaître une fois: http://forums.devshed.com/showpost.php?p=1182653&postcount=2

MERGE INTO KBS.NUFUS_MUHTARLIK B
USING (
    SELECT '028-01' CILT, '25' SAYFA, '6' KUTUK, '46603404838' MERNIS_NO
    FROM DUAL
) E
ON (B.MERNIS_NO = E.MERNIS_NO)
WHEN MATCHED THEN
    UPDATE SET B.CILT = E.CILT, B.SAYFA = E.SAYFA, B.KUTUK = E.KUTUK
WHEN NOT MATCHED THEN
    INSERT (  CILT,   SAYFA,   KUTUK,   MERNIS_NO)
    VALUES (E.CILT, E.SAYFA, E.KUTUK, E.MERNIS_NO); 
Hubbitus
la source
2
Vous vouliez dire INSERT (B.CILT, B.SAYFA, B.KUTUK, B.MERNIS_NO) VALUES (E.CILT, E.SAYFA, E.KUTUK, E.MERNIS_NO); ?
Matteo
Sûr. Merci. Fixé.
Hubbitus
Heureusement, vous avez modifié votre réponse! :) ma modification a malheureusement été rejetée stackoverflow.com/review/suggested-edits/7555674
Matteo
9

Une note concernant les deux solutions qui suggèrent:

1) Insérer, si exception puis mettre à jour,

ou

2) Mettre à jour, si sql% rowcount = 0 alors insérer

La question de savoir s'il faut insérer ou mettre à jour en premier dépend également de l'application. Vous attendez-vous à plus d'inserts ou de mises à jour? Celui qui est le plus susceptible de réussir doit passer en premier.

Si vous choisissez le mauvais, vous obtiendrez un tas de lectures d'index inutiles. Pas une grosse affaire mais quelque chose à considérer.

AnthonyVO
la source
sql% notfound est ma préférence personnelle
Arturo Hernandez
8

J'utilise le premier exemple de code depuis des années. Remarquez non trouvé plutôt que de compter.

UPDATE tablename SET val1 = in_val1, val2 = in_val2
    WHERE val3 = in_val3;
IF ( sql%notfound ) THEN
    INSERT INTO tablename
        VALUES (in_val1, in_val2, in_val3);
END IF;

Le code ci-dessous est le code éventuellement nouveau et amélioré

MERGE INTO tablename USING dual ON ( val3 = in_val3 )
WHEN MATCHED THEN UPDATE SET val1 = in_val1, val2 = in_val2
WHEN NOT MATCHED THEN INSERT 
    VALUES (in_val1, in_val2, in_val3)

Dans le premier exemple, la mise à jour effectue une recherche d'index. Il le faut pour mettre à jour la ligne de droite. Oracle ouvre un curseur implicite et nous l'utilisons pour encapsuler un insert correspondant afin que nous sachions que l'insertion ne se produira que lorsque la clé n'existe pas. Mais l'insert est une commande indépendante et doit effectuer une deuxième recherche. Je ne connais pas le fonctionnement interne de la commande de fusion, mais comme la commande est une seule unité, Oracle aurait pu exécuter l'insertion ou la mise à jour correcte avec une seule recherche d'index.

Je pense que la fusion est meilleure lorsque vous avez un traitement à faire, ce qui signifie prendre des données de certaines tables et mettre à jour une table, éventuellement insérer ou supprimer des lignes. Mais pour le cas d'une seule ligne, vous pouvez considérer le premier cas car la syntaxe est plus courante.

Arturo Hernandez
la source
0

Exemple de copier-coller pour insérer une table dans une autre, avec MERGE:

CREATE GLOBAL TEMPORARY TABLE t1
    (id VARCHAR2(5) ,
     value VARCHAR2(5),
     value2 VARCHAR2(5)
     )
  ON COMMIT DELETE ROWS;

CREATE GLOBAL TEMPORARY TABLE t2
    (id VARCHAR2(5) ,
     value VARCHAR2(5),
     value2 VARCHAR2(5))
  ON COMMIT DELETE ROWS;
ALTER TABLE t2 ADD CONSTRAINT PK_LKP_MIGRATION_INFO PRIMARY KEY (id);

insert into t1 values ('a','1','1');
insert into t1 values ('b','4','5');
insert into t2 values ('b','2','2');
insert into t2 values ('c','3','3');


merge into t2
using t1
on (t1.id = t2.id) 
when matched then 
  update set t2.value = t1.value,
  t2.value2 = t1.value2
when not matched then
  insert (t2.id, t2.value, t2.value2)  
  values(t1.id, t1.value, t1.value2);

select * from t2

Résultat:

  1. b 4 5
  2. c 3 3
  3. a 1 1
Bechyňák Petr
la source
-3

Essaye ça,

insert into b_building_property (
  select
    'AREA_IN_COMMON_USE_DOUBLE','Area in Common Use','DOUBLE', null, 9000, 9
  from dual
)
minus
(
  select * from b_building_property where id = 9
)
;
r4bitt
la source
-6

Sur http://www.praetoriate.com/oracle_tips_upserts.htm :

"Dans Oracle9i, un UPSERT peut accomplir cette tâche en une seule instruction:"

INSERT
FIRST WHEN
   credit_limit >=100000
THEN INTO
   rich_customers
VALUES(cust_id,cust_credit_limit)
   INTO customers
ELSE
   INTO customers SELECT * FROM new_customers;
Anon
la source
14
-1 Don Burleson cr @ p typique J'ai peur - c'est un insert dans une table ou une autre, il n'y a pas de "upsert" ici!
Tony Andrews