Chèn, vào bản cập nhật trùng lặp trong PostgreSQL?


644

Cách đây vài tháng, tôi đã học được từ một câu trả lời trên Stack Overflow về cách thực hiện nhiều bản cập nhật cùng một lúc trong MySQL bằng cú pháp sau:

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

Bây giờ tôi đã chuyển sang PostgreSQL và dường như điều này không chính xác. Nó đề cập đến tất cả các bảng chính xác vì vậy tôi cho rằng đó là vấn đề của các từ khóa khác nhau đang được sử dụng nhưng tôi không chắc chắn tài liệu PostgreQuery này được đề cập ở đâu.

Để làm rõ, tôi muốn chèn một số thứ và nếu chúng đã tồn tại để cập nhật chúng.


38
Bất cứ ai tìm thấy câu hỏi này nên đọc bài viết của Depesz "Tại sao upert lại phức tạp như vậy?" . Nó giải thích vấn đề và giải pháp có thể rất tốt.
Craig Ringer

8
UPSERT sẽ được thêm vào trong Postgres 9.5: wiki.postgresql.org/wiki/ mẹo
thiệu vào

4
@tommed - nó đã được thực hiện: stackoverflow.com/a/34639431/4418
warren

Câu trả lời:


515

PostgreSQL kể từ phiên bản 9.5 có cú pháp UPSERT , với mệnh đề ON CONFLICT . với cú pháp sau (tương tự như 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;

Tìm kiếm tài liệu lưu trữ nhóm email của postgresql cho "upsert" dẫn đến việc tìm một ví dụ về việc bạn có thể muốn làm, trong hướng dẫn :

Ví dụ 38-2. Các ngoại lệ với CẬP NHẬT / CHERTN

Ví dụ này sử dụng xử lý ngoại lệ để thực hiện CẬP NHẬT hoặc CHERTN, nếu phù hợp:

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

Có thể có một ví dụ về cách thực hiện hàng loạt, sử dụng CTE trong 9.1 trở lên, trong danh sách gửi thư của tin tặc :

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;

Xem câu trả lời của a_horse_with_no_name để biết ví dụ rõ ràng hơn.


7
Điều duy nhất tôi không thích ở đây là nó sẽ chậm hơn nhiều, bởi vì mỗi lần nâng cấp sẽ là cuộc gọi riêng của nó vào cơ sở dữ liệu.
baash05

@ baash05 có thể có một cách để làm điều đó với số lượng lớn, xem câu trả lời cập nhật của tôi.
Stephen Denne

2
Điều duy nhất tôi làm khác là sử dụng FOR 1..2 LOOP thay vì chỉ LOOP để nếu một số ràng buộc duy nhất khác bị vi phạm, nó sẽ không quay vô thời hạn.
olamork

2
Điều gì excludedđề cập đến trong giải pháp đầu tiên ở đây?
ichbinallen

2
@ichbinallen trong các tài liệu Các mệnh đề SET và WHERE trong ON CONFLICT DO UPDATE có quyền truy cập vào hàng hiện có bằng tên của bảng (hoặc bí danh) và đến các hàng được đề xuất để chèn bằng bảng loại trừ đặc biệt . Trong trường hợp này, excludedbảng đặc biệt cung cấp cho bạn quyền truy cập vào các giá trị mà bạn đang cố gắng CHỌN ở vị trí đầu tiên.
TMichel

429

Cảnh báo: điều này không an toàn nếu được thực hiện từ nhiều phiên cùng một lúc (xem phần cảnh báo bên dưới).


Một cách thông minh khác để thực hiện "UPSERT" trong postgresql là thực hiện hai câu lệnh UPDATE / INSERT liên tiếp được thiết kế để thành công hoặc không có hiệu lực.

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

CẬP NHẬT sẽ thành công nếu một hàng có "id = 3" đã tồn tại, nếu không nó không có hiệu lực.

INSERT sẽ thành công chỉ khi hàng có "id = 3" chưa tồn tại.

Bạn có thể kết hợp cả hai thành một chuỗi và chạy cả hai với một câu lệnh SQL được thực thi từ ứng dụng của bạn. Chạy chúng cùng nhau trong một giao dịch duy nhất rất được khuyến khích.

Điều này hoạt động rất tốt khi chạy cách ly hoặc trên bảng bị khóa, nhưng phải tuân theo các điều kiện cuộc đua có nghĩa là nó vẫn có thể bị lỗi với khóa trùng lặp nếu một hàng được chèn đồng thời hoặc có thể chấm dứt không có hàng nào được chèn khi một hàng bị xóa đồng thời . Một SERIALIZABLEgiao dịch trên PostgreSQL 9.1 trở lên sẽ xử lý nó một cách đáng tin cậy với chi phí tỷ lệ thất bại tuần tự hóa rất cao, có nghĩa là bạn sẽ phải thử lại rất nhiều. Xem lý do tại sao upert rất phức tạp , trong đó thảo luận chi tiết hơn về trường hợp này.

Cách tiếp cận này cũng bị mất các cập nhật bị read committedcô lập trừ khi ứng dụng kiểm tra số lượng hàng bị ảnh hưởng và xác minh rằng một inserthoặc hàng updatebị ảnh hưởng .


6
Câu trả lời ngắn: nếu bản ghi tồn tại thì INSERT không làm gì cả. Câu trả lời dài: CHỌN trong INSERT sẽ trả về nhiều kết quả như có các kết quả khớp của mệnh đề where. Đó là nhiều nhất một (nếu số một không nằm trong kết quả của lựa chọn phụ), nếu không thì bằng không. Do đó, INSERT sẽ thêm một hoặc một hàng không.
Peter Becker

3
phần 'nơi' có thể được đơn giản hóa bằng cách sử dụng tồn tại:... where not exists (select 1 from table where id = 3);
Endy Tjahjono

1
Đây phải là câu trả lời đúng .. với một số điều chỉnh nhỏ, nó có thể được sử dụng để thực hiện cập nhật hàng loạt .. Humm .. Tôi tự hỏi liệu có thể sử dụng bảng tạm thời không ..
baash05

1
@keaplogik, giới hạn 9.1 đó là với CTE có thể ghi (biểu thức bảng chung) được mô tả trong một câu trả lời khác. Cú pháp được sử dụng trong câu trả lời này là rất cơ bản và đã được hỗ trợ từ lâu.

8
Cảnh báo, đây là đối tượng bị mất cập nhật trong read committedsự cô lập trừ khi ứng dụng của bạn kiểm tra để đảm bảo rằng inserthoặc updatecó một hàng không khác không. Xem dba.stackexchange.com/q/78510/7788
Craig Ringer

227

Với PostgreQuery 9.1, điều này có thể đạt được bằng cách sử dụng CTE có thể ghi ( biểu thức bảng chung ):

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)

Xem các mục blog này:


Lưu ý rằng giải pháp này không ngăn chặn vi phạm khóa duy nhất nhưng nó không dễ bị mất cập nhật.
Xem theo dõi của Craig Ringer trên dba.stackexchange.com


1
@ FrançoisBeausoleil: cơ hội của tình trạng chủng tộc nhỏ hơn nhiều so với phương pháp "thử / xử lý ngoại lệ"
a_horse_with_no_name

2
@a_horse_with_no_name Làm thế nào để bạn chính xác có nghĩa là cơ hội về điều kiện cuộc đua nhỏ hơn nhiều? Khi tôi thực hiện truy vấn này đồng thời với cùng một bản ghi, tôi nhận được lỗi "giá trị khóa trùng lặp vi phạm ràng buộc duy nhất" 100% cho đến khi truy vấn phát hiện rằng bản ghi đã được chèn. Đây có phải là một ví dụ đầy đủ?
Jeroen van Dijk

4
@a_horse_with_no_name Giải pháp của bạn dường như hoạt động trong các tình huống đồng thời khi bạn bọc câu lệnh upsert bằng khóa sau: BEGIN WORK; LOCK TABLE mytable TRONG CHIA SẺ CHẾ ĐỘ ĐỘC QUYỀN ROW; <CHỨNG MINH TẠI ĐÂY>; CAM KẾT LÀM VIỆC;
Jeroen van Dijk

2
@JeroenvanDijk: cảm ơn. Điều tôi muốn nói với "nhỏ hơn nhiều" là nếu một vài giao dịch này (và cam kết thay đổi!) Thì khoảng thời gian giữa bản cập nhật và phần chèn nhỏ hơn vì mọi thứ chỉ là một câu lệnh. Bạn luôn có thể tạo vi phạm pk bằng hai câu lệnh INSERT độc lập. Nếu bạn khóa toàn bộ bảng, bạn sẽ tuần tự hóa một cách hiệu quả tất cả quyền truy cập vào bảng (điều gì đó bạn có thể đạt được với mức cô lập tuần tự hóa).
a_horse_with_no_name

12
Giải pháp này có thể bị mất cập nhật nếu giao dịch chèn quay trở lại; không có kiểm tra để thực thi rằng UPDATEbất kỳ hàng nào bị ảnh hưởng.
Craig Ringer

132

Trong PostgreQuery 9.5 và mới hơn bạn có thể sử dụng INSERT ... ON CONFLICT UPDATE.

Xem tài liệu .

Một MySQL INSERT ... ON DUPLICATE KEY UPDATEcó thể được gửi lại trực tiếp đến a ON CONFLICT UPDATE. Cả cú pháp chuẩn SQL, chúng đều là các phần mở rộng dành riêng cho cơ sở dữ liệu. Có nhiều lý do MERGEkhông được sử dụng cho việc này , một cú pháp mới không được tạo ra chỉ để giải trí. (Cú pháp của MySQL cũng có các vấn đề có nghĩa là nó không được chấp nhận trực tiếp).

ví dụ như thiết lập đã cho:

CREATE TABLE tablename (a integer primary key, b integer, c integer);
INSERT INTO tablename (a, b, c) values (1, 2, 3);

truy vấn MySQL:

INSERT INTO tablename (a,b,c) VALUES (1,2,3)
  ON DUPLICATE KEY UPDATE c=c+1;

trở thành:

INSERT INTO tablename (a, b, c) values (1, 2, 10)
ON CONFLICT (a) DO UPDATE SET c = tablename.c + 1;

Sự khác biệt:

  • Bạn phải chỉ định tên cột (hoặc tên ràng buộc duy nhất) để sử dụng cho kiểm tra tính duy nhất. Đó làON CONFLICT (columnname) DO

  • Từ khóa SETphải được sử dụng, như thể đây là một UPDATEtuyên bố bình thường

Nó cũng có một số tính năng hay:

  • Bạn có thể có một WHEREmệnh đề trên UPDATE(cho phép bạn chuyển ON CONFLICT UPDATEthành ON CONFLICT IGNOREcác giá trị nhất định một cách hiệu quả )

  • Các giá trị được đề xuất để chèn có sẵn dưới dạng biến hàng EXCLUDED, có cùng cấu trúc với bảng đích. Bạn có thể lấy các giá trị gốc trong bảng bằng cách sử dụng tên bảng. Vì vậy, trong trường hợp EXCLUDED.cnày sẽ là 10(vì đó là những gì chúng tôi đã cố gắng chèn) và "table".csẽ là 3vì đó là giá trị hiện tại trong bảng. Bạn có thể sử dụng một hoặc cả hai trong các SETbiểu thức và WHEREmệnh đề.

Để biết thông tin cơ bản về upert, hãy xem Làm thế nào để UPSERT (MERGE, INSERT ... TRÊN CẬP NHẬT DUPLICATE) trong PostgreQuery?


Tôi đã xem xét giải pháp 9.5 của PostgreSQL như bạn đã mô tả ở trên vì tôi gặp phải các lỗ hổng trong trường tăng tự động trong khi ở dưới MySQL ON DUPLICATE KEY UPDATE. Tôi đã tải xuống Postgres 9.5 và triển khai mã của bạn nhưng điều kỳ lạ là vấn đề tương tự xảy ra trong Postgres: trường nối tiếp của khóa chính không liên tiếp (có khoảng cách giữa các lần chèn và cập nhật.). Bất cứ ý tưởng những gì đang xảy ra ở đây? Điều này có bình thường không? Bất kỳ ý tưởng làm thế nào để tránh hành vi này? Cảm ơn bạn.
WM

@WM Đó là khá nhiều vốn có cho một hoạt động nâng cấp. Bạn phải đánh giá chức năng tạo chuỗi trước khi thử chèn. Vì các chuỗi như vậy được thiết kế để hoạt động đồng thời, chúng được miễn khỏi ngữ nghĩa giao dịch thông thường, nhưng ngay cả khi chúng không được tạo ra trong một giao dịch con và quay trở lại, nó sẽ hoàn thành bình thường và cam kết với phần còn lại của hoạt động. Vì vậy, điều này sẽ xảy ra ngay cả với việc thực hiện trình tự "không khoảng cách". Cách duy nhất mà DB có thể tránh điều này là trì hoãn việc đánh giá việc tạo chuỗi cho đến sau khi kiểm tra khóa.
Craig Ringer

1
@WM sẽ tạo ra vấn đề của riêng mình. Về cơ bản, bạn đang bị mắc kẹt. Nhưng nếu bạn đang dựa vào serial / auto_increment thì bạn sẽ gặp lỗi. Bạn có thể có các khoảng trống trình tự do các lỗi khôi phục bao gồm các lỗi thoáng qua - khởi động lại khi tải, lỗi máy khách giữa giao dịch, sự cố, v.v. Bạn không bao giờ phải dựa vào SERIAL/ SEQUENCEhoặc AUTO_INCREMENTkhông có khoảng trống. Nếu bạn cần các chuỗi không khoảng cách, chúng phức tạp hơn; bạn cần sử dụng một bảng truy cập thường xuyên. Google sẽ cho bạn biết nhiều hơn. Nhưng hãy lưu ý các trình tự khoảng cách ngăn chặn tất cả chèn đồng thời.
Craig Ringer

@WM Nếu bạn hoàn toàn yêu cầu trình tự và khoảng cách không có khoảng cách, bạn có thể sử dụng phương pháp nâng cấp dựa trên chức năng được thảo luận trong hướng dẫn cùng với việc thực hiện trình tự khoảng cách sử dụng bảng đối số. Bởi vì các BEGIN ... EXCEPTION ...lần chạy trong một giao diện con bị lỗi quay trở lại, gia tăng trình tự của bạn sẽ bị khôi phục nếu INSERTthất bại.
Craig Ringer

Cảm ơn bạn rất nhiều @Craig Ringer, đó là thông tin khá hữu ích. Tôi nhận ra rằng tôi chỉ có thể từ bỏ việc có khóa chính tăng tự động đó. Tôi đã tạo một trường tổng hợp gồm 3 trường và đối với nhu cầu hiện tại cụ thể của tôi, thực sự không cần đến trường tăng tự động không khoảng cách. Cảm ơn bạn một lần nữa, thông tin bạn cung cấp sẽ giúp tôi tiết kiệm thời gian trong tương lai khi cố gắng ngăn chặn hành vi DB tự nhiên và lành mạnh. Tôi hiểu nó tốt hơn bây giờ.
WM

17

Tôi đã tìm kiếm điều tương tự khi tôi đến đây, nhưng việc thiếu chức năng "upsert" chung chung làm phiền tôi một chút vì vậy tôi nghĩ bạn chỉ có thể vượt qua bản cập nhật và chèn sql làm đối số trên hàm đó tạo thành hướng dẫn

nó sẽ trông như thế này:

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

và có lẽ để làm đầu tiên những gì bạn muốn làm, hàng loạt "upsert", bạn có thể sử dụng Tcl để phân chia các sql_update và vòng lặp các bản cập nhật cá nhân, preformance hit sẽ rất nhỏ thấy http://archives.postgresql.org/pgsql- hiệu suất / 2006-04 / dir00557.php

chi phí cao nhất là thực hiện truy vấn từ mã của bạn, về phía cơ sở dữ liệu, chi phí thực hiện nhỏ hơn nhiều


3
Bạn vẫn phải chạy cái này trong một vòng thử lại và nó dễ xảy ra các cuộc đua đồng thời DELETEtrừ khi bạn khóa bảng hoặc bị SERIALIZABLEcô lập giao dịch trên PostgreQuery 9.1 trở lên.
Craig Ringer

13

Không có lệnh đơn giản để làm điều đó.

Cách tiếp cận đúng nhất là sử dụng hàm, giống như từ tài liệu .

Một giải pháp khác (mặc dù không an toàn) là cập nhật bằng cách trả về, kiểm tra hàng nào là cập nhật và chèn phần còn lại của chúng

Một cái gì đó dọc theo dòng:

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;

giả sử id: 2 đã được trả về:

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

Tất nhiên nó sẽ bảo lãnh sớm hay muộn (trong môi trường đồng thời), vì có điều kiện chủng tộc rõ ràng ở đây, nhưng thường thì nó sẽ hoạt động.

Đây là một bài viết dài hơn và toàn diện hơn về chủ đề này .


1
Nếu sử dụng tùy chọn này, hãy chắc chắn kiểm tra xem id có được trả về ngay cả khi bản cập nhật không có gì. Tôi đã thấy các cơ sở dữ liệu truy vấn tối ưu hóa như "Cập nhật bảng foo set bar = 4 where bar = 4".
thelem

10

Cá nhân, tôi đã thiết lập một "quy tắc" được đính kèm với câu lệnh chèn. Giả sử bạn đã có bảng "dns" ghi lại số lần truy cập dns cho mỗi khách hàng trên cơ sở mỗi lần:

CREATE TABLE dns (
    "time" timestamp without time zone NOT NULL,
    customer_id integer NOT NULL,
    hits integer
);

Bạn muốn có thể chèn lại các hàng với các giá trị được cập nhật hoặc tạo chúng nếu chúng chưa tồn tại. Đã khóa trên khách hàng_id và thời gian. Một cái gì đó như thế này:

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

Cập nhật: Điều này có khả năng thất bại nếu việc chèn đồng thời đang diễn ra, vì nó sẽ tạo ra các ngoại lệ unique_violation. Tuy nhiên, giao dịch không kết thúc sẽ tiếp tục và thành công và bạn chỉ cần lặp lại giao dịch đã kết thúc.

Tuy nhiên, nếu có hàng tấn chèn luôn xảy ra, bạn sẽ muốn đặt khóa bảng xung quanh các câu lệnh chèn: Khóa SHARE ROW EXCLUSIVE sẽ ngăn mọi thao tác có thể chèn, xóa hoặc cập nhật các hàng trong bảng mục tiêu của bạn. Tuy nhiên, các bản cập nhật không cập nhật khóa duy nhất là an toàn, vì vậy nếu bạn không có thao tác nào thực hiện việc này, hãy sử dụng khóa tư vấn thay thế.

Ngoài ra, lệnh COPY không sử dụng RULES, vì vậy nếu bạn đang chèn bằng COPY, bạn sẽ cần sử dụng các kích hoạt thay thế.


9

Tôi sử dụng chức năng này hợp nhất

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

1
Sẽ hiệu quả hơn khi chỉ cần thực hiện updateđầu tiên và sau đó kiểm tra số lượng hàng được cập nhật. (Xem câu trả lời của Ahmad)
a_horse_with_no_name

8

Tôi tùy chỉnh chức năng "upsert" ở trên, nếu bạn muốn XÁC NHẬN VÀ THAY THẾ:

`

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

Và sau khi thực hiện, hãy làm một cái gì đó như thế này:

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

Điều quan trọng là phải đặt gấp đôi dấu phẩy để tránh lỗi trình biên dịch

  • kiểm tra tốc độ ...

7

Tương tự như câu trả lời thích nhất, nhưng hoạt động nhanh hơn một chút:

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)

(nguồn: http://www.the-art-of-web.com/sql/upsert/ )


3
Điều này sẽ thất bại nếu chạy đồng thời trong hai phiên, vì không cập nhật nào sẽ thấy một hàng hiện có nên cả hai bản cập nhật sẽ đạt 0 hàng, vì vậy cả hai truy vấn sẽ đưa ra một chèn.
Craig Ringer

6

Tôi có cùng một vấn đề để quản lý cài đặt tài khoản như các cặp giá trị tên. Tiêu chí thiết kế là các khách hàng khác nhau có thể có các bộ cài đặt khác nhau.

Giải pháp của tôi, tương tự như JWP là xóa và thay thế hàng loạt, tạo bản ghi hợp nhất trong ứng dụng của bạn.

Điều này khá chống đạn, độc lập với nền tảng và vì không bao giờ có nhiều hơn 20 cài đặt cho mỗi máy khách, đây chỉ là 3 cuộc gọi db tải khá thấp - có lẽ là phương pháp nhanh nhất.

Cách thay thế cập nhật các hàng riêng lẻ - kiểm tra ngoại lệ sau đó chèn - hoặc một số kết hợp của mã là ghê gớm, chậm và thường bị phá vỡ vì (như đã đề cập ở trên) xử lý ngoại lệ SQL không chuẩn thay đổi từ db sang db - hoặc thậm chí phát hành sang phát hành.

 #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

Chào mừng đến với SO. Giới thiệu tốt đẹp! :-)
Don Câu hỏi

1
Điều này giống như REPLACE INTOhơn INSERT INTO ... ON DUPLICATE KEY UPDATE, có thể gây ra vấn đề nếu bạn sử dụng kích hoạt. Cuối cùng, bạn sẽ chạy xóa và chèn các kích hoạt / quy tắc, thay vì cập nhật.
cHao

5

Theo tài liệu PostgreSQL của INSERTtuyên bố , xử lý ON DUPLICATE KEYvụ việc không được hỗ trợ. Đó là một phần của cú pháp là một phần mở rộng MySQL độc quyền.


@Lucian MERGEcũng thực sự là một hoạt động OLAP; xem stackoverflow.com/q/17267417/398670 để được giải thích. Nó không định nghĩa ngữ nghĩa đồng thời và hầu hết những người sử dụng nó cho upert chỉ đang tạo ra lỗi.
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

5

Để hợp nhất các bộ nhỏ, sử dụng chức năng trên là tốt. Tuy nhiên, nếu bạn đang hợp nhất một lượng lớn dữ liệu, tôi khuyên bạn nên xem xét http://mbk.projects.postgresql.org

Thực tiễn tốt nhất hiện nay mà tôi biết là:

  1. SAO CHÉP dữ liệu mới / cập nhật vào bảng tạm thời (chắc chắn, hoặc bạn có thể thực hiện CHERTN nếu chi phí ổn)
  2. Mua khóa [tùy chọn] (tư vấn thích hợp hơn với khóa bảng, IMO)
  3. Hợp nhất. (phần thú vị)

5

CẬP NHẬT sẽ trả về số lượng hàng sửa đổi. Nếu bạn sử dụng JDBC (Java), thì bạn có thể kiểm tra giá trị này so với 0 và nếu không có hàng nào bị ảnh hưởng, thay vào đó hãy bắn INSERT. Nếu bạn sử dụng một số ngôn ngữ lập trình khác, có thể vẫn lấy được số hàng đã sửa đổi, kiểm tra tài liệu.

Điều này có thể không thanh lịch nhưng bạn có SQL đơn giản hơn nhiều để sử dụng từ mã gọi. Khác nhau, nếu bạn viết tập lệnh mười dòng bằng PL / PSQL, có lẽ bạn nên có một bài kiểm tra đơn vị của một hoặc một loại khác chỉ cho riêng nó.


4

Chỉnh sửa: Điều này không hoạt động như mong đợi. Không giống như câu trả lời được chấp nhận, điều này tạo ra các vi phạm khóa duy nhất khi hai quá trình liên tục gọi upsert_foođồng thời.

Eureka! Tôi đã tìm ra một cách để làm điều đó trong một truy vấn: sử dụng UPDATE ... RETURNINGđể kiểm tra nếu có bất kỳ hàng nào bị ảnh hưởng:

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;

Việc UPDATEnày phải được thực hiện trong một quy trình riêng vì không may, đây là lỗi cú pháp:

... WHERE NOT EXISTS (UPDATE ...)

Bây giờ nó hoạt động như mong muốn:

SELECT upsert_foo(1, 'hi');
SELECT upsert_foo(1, 'bye');
SELECT upsert_foo(3, 'hi');
SELECT upsert_foo(3, 'bye');

1
Bạn có thể kết hợp chúng thành một tuyên bố nếu bạn sử dụng CTE có thể ghi. Nhưng giống như hầu hết các giải pháp được đăng ở đây, điều này là sai và sẽ thất bại trong sự hiện diện của các bản cập nhật đồng thời.
Craig Ringer
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.