Oracle: як UPSERT (оновити чи вставити в таблицю?)


293

Операція UPSERT або оновлює, або вставляє рядок у таблицю, залежно від того, якщо в таблиці вже є рядок, який відповідає даним:

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

Оскільки Oracle не має конкретної заяви UPSERT, який найкращий спосіб зробити це?

Відповіді:


60

Альтернатива MERGE ("старомодний спосіб"):

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;   

3
@chotchki: справді? Пояснення було б корисним.
Тоні Ендрюс

15
Проблема полягає в тому, що у вас є вікно між вставкою та оновленням, де інший процес може успішно запустити видалення. Однак я використовував цю схему на таблиці, яка ніколи не видаляла проти неї видалення.
чочки

2
Добре, я згоден. Не знаю, чому мені це не було очевидно.
Тоні Ендрюс

4
Я не погоджуюся з Чотчками. "Тривалість блокування: Усі блокування, отримані виписками в рамках транзакції, зберігаються протягом тривалості транзакції, запобігаючи руйнівним втручанням, включаючи брудні читання, втрачені оновлення та руйнівні операції DDL від одночасних транзакцій." Souce: посилання
yohannc

5
@yohannc: Я думаю, справа в тому, що ми не придбали блокування, просто намагаючись вставити рядок.
Тоні Ендрюс

211

Оператор MERGE об'єднує дані між двома таблицями. Використання DUAL дозволяє нам використовувати цю команду. Зауважте, що це не захищено від паралельного доступу.

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

57
Мабуть, заява "злиття в" не є атомною. Це може призвести до "ORA-0001: унікальне обмеження" при одночасному використанні. Перевірка на наявність матчу та вставлення нового запису не захищені блокуванням, тому існує умова гонки. Щоб зробити це надійно, вам потрібно зловити цей виняток і повторно запустити об'єднання або замість цього зробити просте оновлення. В Oracle 10 ви можете використовувати пункт «помилки журналу», щоб змусити його продовжуватись з рештою рядків, коли виникає помилка, і записувати порушуючий рядок до іншої таблиці, а не просто зупинятись.
Тім Сильвестр

1
Привіт, я намагався використовувати той самий шаблон запиту у своєму запиті, але якось мій запит вставляє повторювані рядки. Я не в змозі знайти більше інформації про таблицю DUAL. Хто-небудь, будь ласка, скажіть мені, де я можу отримати інформацію про DUAL, а також про синтаксис злиття?
Шехар

5
@Shekhar Dual - це фіктивна таблиця з одним рядком і стовпцем adp-gmbh.ch/ora/misc/dual.html
YogoZuno

7
@TimSylvester - Oracle використовує транзакції, тому гарантує, що знімок даних на початку транзакції є послідовним протягом усієї транзакції, за винятком будь-яких змін, внесених до неї. Одночасні дзвінки в базу даних використовують стек скасування; тож Oracle буде керувати остаточним станом на основі порядку, коли одночасні транзакції починаються / завершуються. Отже, ви ніколи не будете мати умови перегонів, якщо перевірка обмежень буде зроблена перед вставкою, незалежно від того, скільки одночасних дзвінків робиться на той самий код SQL. Найгірший випадок, у вас можуть виникнути великі суперечки, і Oracle піде набагато більше часу, щоб досягти остаточного стану.
Нео

2
@RandyMagruder Це так, що його 2015 рік, і ми все ще не можемо зробити надійний настрій в Oracle! Чи знаєте ви про одночасне безпечне рішення?
дан b

105

Подвійний приклад, над яким є PL / SQL, був чудовим, тому що я хотів зробити щось подібне, але я хотів, щоб це було клієнтом ... так ось ось SQL, який я використовував для надсилання подібного твердження безпосередньо з якогось 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" )

Однак з точки зору C # це може бути повільніше, ніж робити оновлення та бачити, чи були порушені рядки 0 і робити вставку, якщо вона була.


10
Я повернувся сюди, щоб ще раз перевірити цю схему. Він виходить з ладу під час спроби одночасних вставок. Одна вставка набуває чинності, друга злиття ні вставляє, ні оновлює. Однак швидший підхід зробити два окремих твердження безпечний.
Синесо

3
oralcle новачки на зразок мене можуть запитати , що це подвійний стіл подивитися: stackoverflow.com/q/73751/808698
Хайо Тельна

5
Шкода , що з цієї моделі ми повинні написати два рази дані (Джон Сміт ...). У цьому випадку, я нічого не виграю , використовуючи MERGE, і я вважаю за краще використовувати набагато простіше DELETEтоді INSERT.
Ніколя Барбулеско

@NicolasBarbulesco ця відповідь не потрібно записувати дані двічі: stackoverflow.com/a/4015315/8307814
whyer

@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

Ще одна альтернатива без перевірки винятку:

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;

Ваше надане рішення не працює для мене. Чи працює% rowcount тільки з явними курсорами?
Синесо

Що робити, якщо оновлення повернуло 0 рядків змінених, оскільки запис вже був, а значення були однакові?
Адріано Варолі П'яцца

10
@Adriano: sql% кількість рядків все одно повернеться> 0, якщо пункт WHERE відповідає будь-яким рядкам, навіть якщо оновлення насправді не змінює жодних даних про ці рядки.
Тоні Ендрюс

Не працює: PLS-00207: ідентифікатор "COUNT", застосований до неявного курсору SQL, не є юридичним атрибутом курсора
Патрік Бек,

Синтаксичні помилки тут :(
ilmirons

27
  1. вставити, якщо її немає
  2. оновлення:
    
ВСТАВИТИ В mytable (id1, t1) 
  ВИБІР 11, 'x1' ВІД ДУАЛЬНОГО 
  ДЕ НЕ ІСНУЄТЬСЯ (ВИБІРІТЬ id1 ВІД мітблера, де id1 = 11); 

ОНОВЛЕННЯ mytable SET t1 = 'x1' WHERE id1 = 11;

26

Жоден із наведених відповідей поки що не є безпечним в умовах одночасного доступу , на що вказував коментар Тіма Сильвестра, і не спричинить винятків у разі перегонів. Щоб виправити це, комбо вставлення / оновлення повинно бути загорнене в якесь твердження циклу, щоб у випадку винятку вся справа була повторена.

Як приклад, ось, як код Grommit можна зафіксувати в циклі, щоб зробити його безпечним при одночасному виконанні:

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; 

Примітка: У режимі транзакцій SERIALIZABLE, який я не рекомендую btw, ви можете зіткнутися з ORA-08177: натомість не можете серіалізувати доступ до винятків для цієї транзакції .


3
Відмінно! Нарешті, паралельний доступ до безпечної відповіді. Будь-який спосіб використовувати таку конструкцію від клієнта (наприклад, від клієнта Java)?
Себієн

1
Ви маєте на увазі не потрібно дзвонити на збережену програму? Ну, у такому випадку ви також можете просто виловити конкретні винятки Java та повторити спробу в циклі Java. На Java це набагато зручніше, ніж SQL у Oracle.
Євген Бересовський,

Вибачте: я був недостатньо конкретним. Але ви зрозуміли правильний шлях. Я пішов у відставку так, як ти щойно сказав. Але я не на 100% задоволений, оскільки він генерує більше SQL запитів, більше клієнтів / серверів. Це не добре рішення рішення. Але моя мета - дозволити розробникам Java мого проекту використовувати мій метод для внесення в будь-яку таблицю (я не можу створити одну збережену процедуру PLSQL на одну таблицю або одну процедуру на тип upsert).
Себієн

@Sebien Я згоден, було б краще, щоб він був інкапсульований у царині SQL, і я думаю, ви можете це зробити. Я просто не добровільно розумію це для вас ... :) Плюс, насправді ці винятки, мабуть, трапляться рідше одного разу на синьому місяці, тому ви не повинні бачити впливу на продуктивність у 99,9% випадків. За винятком випадків, коли ви робите навантажувальне тестування, звичайно ...
Євген Бересовський,

24

Я хотів би відповісти Grommit, за винятком випадків, коли він вимагає значущих значень. Я знайшов рішення, де воно може з’явитися один раз: 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); 

2
Ви мали на увазі INSERT (B.CILT, B.SAYFA, B.KUTUK, B.MERNIS_NO) VALUES (E.CILT, E.SAYFA, E.KUTUK, E.MERNIS_NO); ?
Маттео

Звичайно. Дякую. Виправлено.
Губбіт

Вдячно ви відредагували свою відповідь! :) мою
Маттео

9

Примітка щодо двох рішень, які пропонують:

1) Вставити, якщо виняток, то оновити,

або

2) Оновіть, якщо sql% rowcount = 0, тоді вставте

Питання, чи потрібно спочатку вставити чи оновити, також залежить від програми. Очікуєте більше вставок чи більше оновлень? Той, який, швидше за все, досягне успіху, повинен піти першим.

Якщо ви виберете неправильний, ви отримаєте купу непотрібних зчитувань індексу. Не величезна справа, але все-таки варто враховувати.


sql% notfound - це мої особисті переваги
Arturo Hernandez

8

Я перший рік використовую перший зразок коду. Зауважте, що не знайдете, а не порахуйте.

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;

Нижче наведений код - це можливо новий і вдосконалений код

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)

У першому прикладі оновлення здійснює пошук індексу. Це потрібно для того, щоб оновити потрібний рядок. Oracle відкриває неявний курсор, і ми використовуємо його для загортання відповідної вставки, щоб ми знали, що вставка відбудеться лише тоді, коли ключа не існує. Але вставка - це незалежна команда, і вона повинна зробити другий пошук. Я не знаю внутрішньої роботи команди злиття, але оскільки команда є єдиним блоком, Oracle міг би виконати правильну вставку або оновити за допомогою одного пошуку індексу.

Я думаю, що злиття краще, коли у вас є якась обробка, яка означає взяття даних з деяких таблиць і оновлення таблиці, можливо, вставляючи або видаляючи рядки. Але для випадку одного рядка ви можете розглянути перший випадок, оскільки синтаксис є більш поширеним.


0

Скопіюйте та вставте приклад для вставки однієї таблиці в іншу, 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

Результат:

  1. b 4 5
  2. c 3 3
  3. a 1 1

-3

Спробуйте це,

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

-6

Від http://www.praetoriate.com/oracle_tips_upserts.htm :

"У Oracle9i UPSERT може виконати це завдання в одному операторі:"

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;

14
-1 Типовий Дон Бурлесон cr @ p Боюся - це вставка в ту чи іншу таблицю, тут немає "прихисток"!
Тоні Ендрюс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.