Oracle: làm thế nào để UPSERT (cập nhật hoặc chèn vào bảng?)


293

Hoạt động của UPSERT cập nhật hoặc chèn một hàng trong bảng, tùy thuộc vào việc bảng đã có hàng khớp với dữ liệu hay chưa:

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

Vì Oracle không có một tuyên bố UPSERT cụ thể, cách tốt nhất để làm điều này là gì?

Câu trả lời:


60

Một thay thế cho MERGE ("cách thức cũ"):

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: thật sao? Một lời giải thích sẽ hữu ích.
Tony Andrew

15
Vấn đề là bạn có một cửa sổ ở giữa phần chèn và bản cập nhật trong đó một quy trình khác có thể thực hiện xóa thành công. Tuy nhiên, tôi đã sử dụng mô hình này trên một bảng chưa bao giờ xóa bắn chống lại nó.
chotchki

2
OK tôi đồng ý. Không biết tại sao nó không rõ ràng với tôi.
Tony Andrew

4
Tôi không đồng ý với Chotchki. "Thời lượng khóa: Tất cả các khóa có được bởi các tuyên bố trong giao dịch được giữ trong suốt thời gian giao dịch, ngăn chặn sự can thiệp phá hoại bao gồm đọc bẩn, cập nhật bị mất và hoạt động DDL phá hủy từ các giao dịch đồng thời." Souce: link
yohannc

5
@yohannc: Tôi nghĩ vấn đề là chúng tôi đã không có được bất kỳ ổ khóa nào chỉ bằng cách thử và không chèn một hàng.
Tony Andrew

211

Câu lệnh MERGE hợp nhất dữ liệu giữa hai bảng. Sử dụng DUAL cho phép chúng tôi sử dụng lệnh này. Lưu ý rằng điều này không được bảo vệ chống lại truy cập đồng thời.

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
Rõ ràng tuyên bố "hợp nhất thành" không phải là nguyên tử. Nó có thể dẫn đến "ORA-0001: ràng buộc duy nhất" khi được sử dụng đồng thời. Việc kiểm tra sự tồn tại của một trận đấu và việc chèn một bản ghi mới không được bảo vệ bởi một khóa, do đó có một điều kiện cuộc đua. Để làm điều này một cách đáng tin cậy, bạn cần phải bắt ngoại lệ này và chạy lại hợp nhất hoặc thay vào đó là một bản cập nhật đơn giản. Trong Oracle 10, bạn có thể sử dụng mệnh đề "lỗi nhật ký" để tiếp tục với phần còn lại của các hàng khi xảy ra lỗi và ghi lại hàng vi phạm vào bảng khác, thay vì chỉ dừng lại.
Tim Sylvester

1
Xin chào, tôi đã cố sử dụng cùng một mẫu truy vấn trong truy vấn của mình nhưng bằng cách nào đó, truy vấn của tôi đang chèn các hàng trùng lặp. Tôi không thể tìm thêm thông tin về bảng DUAL. Ai đó có thể vui lòng cho tôi biết tôi có thể lấy thông tin về DUAL ở đâu và về cú pháp hợp nhất không?
Shekhar

5
@Shekhar Dual là một bảng giả với một hàng duy nhất và cột adp-gmbh.ch/ora/misc/dual.html
YogoZuno

7
@TimSylvester - Oracle sử dụng các giao dịch, vì vậy đảm bảo ảnh chụp nhanh dữ liệu khi bắt đầu giao dịch là nhất quán trong suốt giao dịch lưu lại mọi thay đổi được thực hiện trong đó. Các cuộc gọi đồng thời đến cơ sở dữ liệu sử dụng ngăn xếp hoàn tác; do đó, Oracle sẽ quản lý trạng thái cuối cùng dựa trên thứ tự khi các giao dịch đồng thời bắt đầu / hoàn thành. Vì vậy, bạn sẽ không bao giờ có điều kiện cuộc đua nếu việc kiểm tra ràng buộc được thực hiện trước khi chèn bất kể có bao nhiêu cuộc gọi đồng thời được thực hiện cho cùng một mã SQL. Trường hợp xấu nhất, bạn có thể nhận được rất nhiều sự tranh chấp và Oracle sẽ mất nhiều thời gian hơn để đi đến trạng thái cuối cùng.
Neo

2
@RandyMagruder Có phải là trường hợp của năm 2015 và chúng tôi vẫn không thể làm một cách đáng tin cậy trong Oracle! Bạn có biết về một giải pháp an toàn đồng thời?
dan b

105

Ví dụ kép ở trên trong PL / SQL là tuyệt vời vì tôi muốn làm điều gì đó tương tự, nhưng tôi muốn nó ở phía khách hàng ... vì vậy đây là SQL tôi đã sử dụng để gửi một câu lệnh tương tự trực tiếp từ một số 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" )

Tuy nhiên, từ góc độ C #, điều này cung cấp chậm hơn so với thực hiện cập nhật và xem liệu các hàng bị ảnh hưởng có phải là 0 hay không và thực hiện thao tác chèn nếu có.


10
Tôi đã trở lại đây để kiểm tra mô hình này một lần nữa. Nó thất bại âm thầm khi cố gắng chèn đồng thời. Một chèn có hiệu lực, hợp nhất thứ hai không chèn hoặc cập nhật. Tuy nhiên, cách tiếp cận nhanh hơn để thực hiện hai tuyên bố riêng biệt là an toàn.
Synesso

3
những người mới sử dụng như tôi có thể hỏi bảng kép này là gì : xem stackoverflow.com/q/73751/808698
Hajo Thelen

5
Thật tệ là với mẫu này, chúng ta cần ghi hai lần dữ liệu (John, Smith ...). Trong trường hợp này, tôi giành chiến thắng không có gì sử dụng MERGE, và tôi thích sử dụng đơn giản hơn nhiều DELETErồi INSERT.
Nicolas Barbulesco

@NicolasBarbulesco câu trả lời này không cần ghi dữ liệu hai lần: 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);
sao

46

Một cách khác mà không cần kiểm tra ngoại lệ:

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;

Giải pháp được cung cấp của bạn không làm việc cho tôi. Liệu% rowcount chỉ hoạt động với các con trỏ rõ ràng?
Synesso

Điều gì xảy ra nếu bản cập nhật trả về 0 hàng được sửa đổi vì bản ghi đã có và các giá trị là như nhau?
Quảng trường Adriano Varoli

10
@Adriano: sql% rowcount vẫn sẽ trả về> 0 nếu mệnh đề WHERE khớp với bất kỳ hàng nào, ngay cả khi bản cập nhật không thực sự thay đổi bất kỳ dữ liệu nào trên các hàng đó.
Tony Andrew

Không hoạt động: PLS-00207: mã định danh 'COUNT', được áp dụng cho SQL con trỏ ẩn, không phải là thuộc tính con trỏ hợp pháp
Patrik Beck

Lỗi cú pháp tại đây :(
ilmirons

27
  1. chèn nếu không tồn tại
  2. cập nhật:
    
XÁC NHẬN VÀO mytable (id1, t1) 
  CHỌN 11, 'x1' TỪ DUAL 
  WHERE KHÔNG EXISTS (CHỌN id1 TỪ mytble WHERE id1 = 11); 

CẬP NHẬT mytable SET t1 = 'x1' WHERE id1 = 11;

26

Không có câu trả lời nào được đưa ra cho đến nay là an toàn khi đối mặt với các truy cập đồng thời , như được nêu trong nhận xét của Tim Sylvester, và sẽ đưa ra ngoại lệ trong trường hợp các cuộc đua. Để khắc phục điều đó, combo chèn / cập nhật phải được gói trong một số loại câu lệnh lặp, để trong trường hợp ngoại lệ, toàn bộ điều được thử lại.

Ví dụ, đây là cách mã của Grommit có thể được bọc trong một vòng lặp để đảm bảo an toàn khi chạy đồng thời:

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 Trong chế độ giao dịch SERIALIZABLE, mà tôi không khuyến nghị btw, bạn có thể chạy vào ORA-08177: không thể tuần tự hóa quyền truy cập cho ngoại lệ giao dịch này .


3
Thông minh! Cuối cùng, một câu trả lời đồng thời truy cập an toàn. Bất kỳ cách nào để sử dụng cấu trúc như vậy từ máy khách (ví dụ: từ máy khách Java)?
Sebien

1
Bạn có nghĩa là không phải gọi một Proc lưu trữ? Chà, trong trường hợp đó, bạn cũng có thể chỉ cần bắt các ngoại lệ Java cụ thể và thử lại trong một vòng lặp Java. Đó là một địa ngục thuận tiện hơn rất nhiều trong Java so với SQL của Oracle.
Eugene Beresovsky

Tôi xin lỗi: Tôi không đủ cụ thể. Nhưng bạn đã hiểu đúng cách. Tôi xin nghỉ việc để làm như bạn vừa nói. Nhưng tôi không hài lòng 100% vì nó tạo ra nhiều truy vấn SQL hơn, nhiều vòng máy khách / máy chủ hơn. Nó không phải là một giải pháp tốt hiệu suất-khôn ngoan. Nhưng mục tiêu của tôi là để cho các nhà phát triển Java của dự án của tôi sử dụng phương thức của tôi để nâng cấp trong bất kỳ bảng nào (tôi không thể tạo một thủ tục được lưu trữ PLQuery trên mỗi bảng hoặc một thủ tục cho mỗi loại upert).
Sabien

@Sebien Tôi đồng ý, sẽ tốt hơn nếu nó được gói gọn trong vương quốc SQL và tôi nghĩ bạn có thể làm điều đó. Tôi chỉ không tình nguyện tìm ra điều đó cho bạn ... :) Ngoài ra, trong thực tế, những trường hợp ngoại lệ này có thể sẽ xảy ra ít hơn một lần trong một mặt trăng xanh, vì vậy bạn không nên thấy tác động đến hiệu suất trong 99,9% trường hợp. Ngoại trừ khi thực hiện kiểm tra tải tất nhiên ...
Eugene Beresovsky

24

Tôi muốn câu trả lời của Grommit, ngoại trừ nó yêu cầu các giá trị dupe. Tôi đã tìm thấy giải pháp nơi nó có thể xuất hiện một lần: 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
Ý bạn là INSERT (B.CILT, B.SAYFA, B.KUTUK, B.MERNIS_NO) VALUES (E.CILT, E.SAYFA, E.KUTUK, E.MERNIS_NO); sao
Matteo

Chắc chắn rồi. Cảm ơn. Đã sửa.
Hubbitus

Rất may bạn đã chỉnh sửa câu trả lời của bạn! :) chỉnh sửa của tôi đã buồn bã từ chối stackoverflow.com/review/suggested-edits/7555674
Matteo

9

Một lưu ý liên quan đến hai giải pháp cho thấy:

1) Chèn, nếu ngoại lệ thì cập nhật,

hoặc là

2) Cập nhật, nếu sql% rowcount = 0 thì chèn

Câu hỏi về việc nên chèn hay cập nhật trước cũng phụ thuộc vào ứng dụng. Bạn đang mong đợi thêm chèn hoặc cập nhật nhiều hơn? Người nào có khả năng thành công nhất nên đi trước.

Nếu bạn chọn sai, bạn sẽ nhận được một loạt các chỉ số không cần thiết đọc. Không phải là một thỏa thuận lớn nhưng vẫn còn một cái gì đó để xem xét.


sql% notfound là sở thích cá nhân của tôi
Arturo Hernandez

8

Tôi đã sử dụng mẫu mã đầu tiên trong nhiều năm. Thông báo không rõ ràng hơn là đếm.

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;

Mã dưới đây là mã mới và có thể được cải thiện

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)

Trong ví dụ đầu tiên, bản cập nhật thực hiện tra cứu chỉ mục. Nó phải, để cập nhật hàng bên phải. Oracle mở một con trỏ ẩn và chúng tôi sử dụng nó để bọc một phần chèn tương ứng để chúng tôi biết rằng phần chèn sẽ chỉ xảy ra khi khóa không tồn tại. Nhưng chèn là một lệnh độc lập và nó phải thực hiện tra cứu thứ hai. Tôi không biết hoạt động bên trong của lệnh hợp nhất nhưng vì lệnh là một đơn vị, nên Oracle có thể thực hiện thao tác chèn hoặc cập nhật chính xác với một tra cứu chỉ mục duy nhất.

Tôi nghĩ hợp nhất sẽ tốt hơn khi bạn thực hiện một số xử lý có nghĩa là lấy dữ liệu từ một số bảng và cập nhật bảng, có thể chèn hoặc xóa các hàng. Nhưng đối với trường hợp hàng đơn, bạn có thể xem xét trường hợp đầu tiên vì cú pháp phổ biến hơn.


0

Sao chép và dán ví dụ để nâng cấp một bảng vào một bảng khác, với 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

Kết quả:

  1. b 4 5
  2. c 3 3
  3. một 1 1

-3

Thử cái này,

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

Từ http://www.praetoriate.com/oracle_tips_upserts.htmlm :

"Trong Oracle9i, một UPSERT có thể hoàn thành nhiệm vụ này trong một tuyên bố duy nhất:"

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 điển hình cr @ p Tôi sợ - đây là một chèn vào bảng này hay bảng khác, không có "upert" ở đây!
Tony Andrew
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.