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

ทางเลือกอื่นในการผสาน (วิธี "แบบเก่า"):

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: จริงเหรอ? คำอธิบายจะเป็นประโยชน์
Tony Andrews

15
ปัญหาคือคุณมีหน้าต่างอยู่ระหว่างการแทรกและการอัพเดทที่กระบวนการอื่นสามารถทำการลบได้สำเร็จ อย่างไรก็ตามฉันใช้รูปแบบนี้บนโต๊ะที่ไม่เคยถูกไล่ออก
chotchki

2
ตกลงฉันเห็นด้วย. ไม่รู้ทำไมฉันไม่ชัดเจน
Tony Andrews

4
ฉันไม่เห็นด้วยกับ Chotchki "ล็อคระยะเวลา: การล็อกทั้งหมดที่ได้รับจากคำสั่งภายในธุรกรรมนั้นจะถูกระงับไว้ตลอดระยะเวลาของการทำธุรกรรมการป้องกันการรบกวนที่เป็นอันตรายรวมถึงการอ่านสกปรกอัปเดตที่สูญหายและการดำเนินการ DDL ที่ทำลายล้าง Souce: link
yohannc

5
@ โยฮันน์: ฉันคิดว่าประเด็นคือว่าเราไม่ได้ล็อคใด ๆ เพียงแค่พยายามและล้มเหลวในการแทรกแถว
Tony Andrews

211

งบรวม ผสานข้อมูลระหว่างสองตาราง การใช้ 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 คุณสามารถใช้ส่วน "ข้อผิดพลาดการบันทึก" เพื่อให้ดำเนินการต่อกับส่วนที่เหลือของแถวเมื่อเกิดข้อผิดพลาดและเข้าสู่แถวที่ละเมิดไปยังตารางอื่นแทนที่จะหยุดเพียงแค่
ทิม Sylvester

1
สวัสดีฉันพยายามใช้รูปแบบแบบสอบถามเดียวกันในแบบสอบถามของฉัน แต่อย่างใดแบบสอบถามของฉันแทรกแถวที่ซ้ำกัน ฉันไม่สามารถหาข้อมูลเพิ่มเติมเกี่ยวกับตาราง DUAL ทุกคนได้โปรดบอกฉันว่าฉันจะหาข้อมูลของ DUAL ได้ที่ไหนและรวมถึงการผสานไวยากรณ์หรือไม่
Shekhar

5
@Shekhar Dual เป็นตารางจำลองที่มีแถวเดียวและคอลัมน์adp-gmbh.ch/ora/misc/dual.html
YogoZuno

7
@TimSylvester - Oracle ใช้การทำธุรกรรมดังนั้นจึงรับประกันได้ว่าภาพรวมของข้อมูลที่จุดเริ่มต้นของการทำธุรกรรมมีความสอดคล้องกันตลอดการทำธุรกรรมบันทึกการเปลี่ยนแปลงใด ๆ ที่เกิดขึ้นภายใน การโทรไปยังฐานข้อมูลพร้อมกันจะใช้เลิกทำสแต็ก ดังนั้นออราเคิลจะจัดการสถานะสุดท้ายตามลำดับของเมื่อการทำธุรกรรมพร้อมกันเริ่มต้น / เสร็จสิ้น ดังนั้นคุณจะไม่เคยมีสภาพการแข่งขันหากมีการตรวจสอบข้อ จำกัด ก่อนที่จะแทรกโดยไม่คำนึงถึงจำนวนการโทรพร้อมกันที่ทำกับรหัส SQL เดียวกัน กรณีที่เลวร้ายที่สุดคุณอาจได้รับข้อโต้แย้งมากมายและ Oracle จะใช้เวลานานกว่าจะถึงสถานะสุดท้าย
Neo

2
@RandyMagruder เป็นกรณีที่ปี 2015 และเรายังไม่สามารถเพิ่มความน่าเชื่อถือใน Oracle! คุณรู้วิธีแก้ปัญหาความปลอดภัยพร้อมกันหรือไม่?
dan 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
ฉันกลับมาที่นี่อีกครั้งเพื่อตรวจสอบรูปแบบนี้อีกครั้ง มันล้มเหลวอย่างเงียบ ๆ เมื่อพยายามแทรกเกิดขึ้นพร้อมกัน การแทรกครั้งเดียวจะมีผลการผสานครั้งที่สองจะไม่มีการแทรกหรือการปรับปรุง อย่างไรก็ตามวิธีที่เร็วกว่าในการทำคำสั่งสองข้อความที่แยกต่างหากนั้นปลอดภัย
Synesso

3
มือใหม่ที่ชอบปากฉันอาจถามว่าโต๊ะคู่นี่คืออะไรดู: stackoverflow.com/q/73751/808698
Hajo Thelen

5
น่าเสียดายที่รูปแบบนี้เราต้องเขียนข้อมูล สองครั้ง (John, Smith ... ) ในกรณีนี้ผมชนะอะไรใช้MERGEและฉันชอบใช้ง่ายมากแล้วDELETE INSERT
Nicolas Barbulesco

@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);
ทำไม

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 ใช้งานได้กับเคอร์เซอร์อย่างชัดเจนเท่านั้นหรือไม่
Synesso

จะเกิดอะไรขึ้นถ้าการอัปเดตส่งกลับ 0 แถวที่แก้ไขเนื่องจากเรคคอร์ดมีอยู่แล้วและค่าเหมือนกันหรือไม่
Adriano Varoli Piazza

10
@Adriano: sql% rowcount จะยังคงกลับมา> 0 ถ้าส่วนคำสั่ง WHERE ตรงกับแถวใด ๆ แม้ว่าการปรับปรุงจะไม่เปลี่ยนข้อมูลใด ๆ ในแถวเหล่านั้น
Tony Andrews

ใช้งานไม่ได้: PLS-00207: ตัวระบุ 'COUNT' ซึ่งใช้กับเคอร์เซอร์ SQL โดยปริยายไม่ใช่แอตทริบิวต์เคอร์เซอร์ที่ถูกต้อง
Patrik Beck

ข้อผิดพลาดทางไวยากรณ์ที่นี่ :(
ilmirons

27
  1. ใส่ถ้าไม่มี
  2. อัปเดต:
    
ใส่เข้าไปใน mytable (id1, t1) 
  เลือก 11, 'x1' จาก DUAL 
  ไม่มีอยู่ (SELECT id1 จาก mytble WHERE id1 = 11); 

อัพเดต mytable SET t1 = 'x1' WHERE id1 = 11;

26

ไม่มีคำตอบใดที่ให้ไว้เพื่อความปลอดภัยในการเข้าถึงพร้อมกันดังที่ระบุไว้ในความคิดเห็นของ Tim Sylvester และจะเพิ่มข้อยกเว้นในกรณีของการแข่งขัน ในการแก้ไขปัญหานั้นคำสั่งผสม / update จะต้องถูกห่อหุ้มด้วยคำสั่งวนรอบบางชนิดดังนั้นในกรณีที่มีข้อยกเว้นจะลองใหม่ทั้งหมด

ตัวอย่างเช่นต่อไปนี้เป็นวิธีห่อโค้ดของ 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)?
Sebien

1
คุณหมายถึงไม่ต้องโทร proc ที่เก็บไว้? ในกรณีนี้คุณสามารถจับจาวาข้อยกเว้นเฉพาะและลองใหม่ในลูป Java มันเป็นนรกที่สะดวกมากใน Java กว่า SQL ของ Oracle
Eugene Beresovsky

ฉันขอโทษ: ฉันไม่เจาะจงพอ แต่คุณเข้าใจวิธีการที่เหมาะสม ฉันลาออกไปทำเหมือนที่คุณเพิ่งพูด แต่ฉันไม่พอใจ 100% เพราะมันสร้างการสืบค้น SQL มากขึ้น, ไคลเอนต์ / เซิร์ฟเวอร์ roundtrips มากขึ้น มันไม่ได้เป็นทางออกที่ดีประสิทธิภาพฉลาด แต่เป้าหมายของฉันคือให้ผู้พัฒนา Java ของโครงการของฉันใช้วิธีการของฉันเพื่อยกระดับในตารางใด ๆ (ฉันไม่สามารถสร้าง PLSQL หนึ่งกระบวนงานที่เก็บไว้ต่อตารางหรือหนึ่งขั้นตอนต่อประเภท upsert)
Sebien

@ เซเบียนฉันเห็นด้วยว่ามันคงจะดีกว่าถ้าได้ห่อหุ้มมันไว้ในอาณาจักร SQL และฉันคิดว่าคุณทำได้ ฉันแค่ไม่ได้อาสาที่จะคิดออกเพื่อคุณ ... :) นอกจากนี้ในความเป็นจริงข้อยกเว้นเหล่านี้อาจเกิดขึ้นน้อยกว่าหนึ่งครั้งในดวงจันทร์สีน้ำเงินดังนั้นคุณไม่ควรเห็นผลกระทบต่อประสิทธิภาพการทำงานในกรณี 99.9% ยกเว้นเมื่อทำการโหลดทดสอบแน่นอน ...
ยู Beresovsky

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); อะไร
Matteo

แน่ใจ ขอบคุณ แก้ไขแล้ว.
Hubbitus

ขอบคุณที่คุณแก้ไขคำตอบของคุณ! :) การแก้ไขของฉันเศร้าปฏิเสธ stackoverflow.com/review/suggested-edits/7555674
Matteo

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

คัดลอกและวางตัวอย่างสำหรับการ upserting หนึ่งตารางไปยังอีกด้วย 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. 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 โดยทั่วไป Don Burleson cr @ p ฉันกลัว - นี่คือการแทรกลงในหนึ่งตารางหรืออื่นไม่มี "upsert" ที่นี่!
Tony Andrews
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.