Làm cách nào để sử dụng RETURNING với ON CONFLICT trong PostgreSQL?


149

Tôi có UPSERT sau trong PostgreQuery 9.5:

INSERT INTO chats ("user", "contact", "name") 
           VALUES ($1, $2, $3), 
                  ($2, $1, NULL) 
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id;

Nếu không có xung đột, nó sẽ trả về một cái gì đó như thế này:

----------
    | id |
----------
  1 | 50 |
----------
  2 | 51 |
----------

Nhưng nếu có xung đột, nó sẽ không trả về bất kỳ hàng nào:

----------
    | id |
----------

Tôi muốn trả về các idcột mới nếu không có xung đột hoặc trả về các idcột hiện có của các cột xung đột.
Điều này có thể được thực hiện? Nếu vậy thì thế nào?


1
Sử dụng ON CONFLICT UPDATEđể có một sự thay đổi cho hàng. Sau đó RETURNINGsẽ chụp nó.
Gordon Linoff

1
@GordonLinoff Nếu không có gì để cập nhật thì sao?
Okku

1
Nếu không có gì để cập nhật, điều đó có nghĩa là không có xung đột nên nó chỉ chèn các giá trị mới và trả về id của họ
zola

1
Bạn sẽ tìm thấy những cách khác ở đây . Tôi rất muốn biết sự khác biệt giữa hai về mặt hiệu suất mặc dù.
Stanislasdrg Tái lập Monica

Câu trả lời:


88

Tôi đã có cùng một vấn đề và tôi đã giải quyết nó bằng cách sử dụng 'do update' thay vì 'do nothing', mặc dù tôi không có gì để cập nhật. Trong trường hợp của bạn, nó sẽ là một cái gì đó như thế này:

INSERT INTO chats ("user", "contact", "name") 
       VALUES ($1, $2, $3), 
              ($2, $1, NULL) 
ON CONFLICT("user", "contact") DO UPDATE SET name=EXCLUDED.name RETURNING id;

Truy vấn này sẽ trả về tất cả các hàng, bất kể chúng vừa được chèn hoặc chúng tồn tại trước đó.


11
Một vấn đề với cách tiếp cận này là, số thứ tự của khóa chính được tăng lên sau mỗi xung đột (cập nhật không có thật), về cơ bản có nghĩa là bạn có thể kết thúc với những khoảng trống lớn trong chuỗi. Bất kỳ ý tưởng làm thế nào để tránh điều đó?
Mischa

9
@Mischa: vậy thì sao? Các trình tự không bao giờ được đảm bảo là không có khoảng trống ở vị trí đầu tiên và các khoảng trống không thành vấn đề (và nếu chúng xảy ra, một chuỗi là điều sai trái)
a_horse_with_no_name

24
Tôi sẽ không khuyên sử dụng điều này trong hầu hết các trường hợp. Tôi đã thêm một câu trả lời tại sao.
Erwin Brandstetter

4
Câu trả lời này dường như không đạt được DO NOTHINGkhía cạnh của câu hỏi ban đầu - đối với tôi, nó dường như cập nhật trường không xung đột (ở đây, "tên") cho tất cả các hàng.
PeterJCLaw

Như đã thảo luận trong câu trả lời rất dài dưới đây, sử dụng "Do Update" cho trường không thay đổi không phải là giải pháp "sạch" và có thể gây ra các vấn đề khác.
Bill Worthington

202

Các câu trả lời chấp nhận hiện nay có vẻ ok cho một mục tiêu duy nhất xung đột, ít xung đột, tuples nhỏ và không gây nên. Nó tránh được vấn đề đồng thời 1 (xem bên dưới) với lực lượng vũ phu. Giải pháp đơn giản có sức hấp dẫn của nó, các tác dụng phụ có thể ít quan trọng hơn.

Tuy nhiên, đối với tất cả các trường hợp khác, không cập nhật các hàng giống hệt nhau mà không cần. Ngay cả khi bạn thấy không có sự khác biệt trên bề mặt, có nhiều tác dụng phụ khác nhau :

  • Nó có thể kích hoạt kích hoạt không nên bắn.

  • Nó ghi các hàng "vô tội", có thể phát sinh chi phí cho các giao dịch đồng thời.

  • Nó có thể làm cho hàng có vẻ mới, mặc dù nó đã cũ (dấu thời gian giao dịch).

  • Quan trọng nhất , với mô hình MVCC của PostgreQuery, một phiên bản hàng mới được viết cho mọi người UPDATE, bất kể dữ liệu hàng có thay đổi hay không. Điều này phải chịu một hình phạt hiệu suất cho chính UPSERT, phình bảng, phình chỉ mục, phạt hiệu năng cho các hoạt động tiếp theo trên bảng, VACUUMchi phí. Một hiệu ứng nhỏ cho một vài bản sao, nhưng lớn cho hầu hết các bản sao .

Thêm vào đó , đôi khi nó không thực tế hoặc thậm chí có thể sử dụng ON CONFLICT DO UPDATE. Hướng dẫn sử dụng:

Đối với ON CONFLICT DO UPDATE, conflict_targetphải được cung cấp.

Không thể có một "mục tiêu xung đột" nếu có nhiều chỉ mục / ràng buộc.

Bạn có thể đạt được (gần như) như nhau mà không cần cập nhật trống và tác dụng phụ. Một số giải pháp sau đây cũng hoạt động với ON CONFLICT DO NOTHING(không có "mục tiêu xung đột"), để nắm bắt tất cả các xung đột có thể xảy ra - có thể hoặc không thể mong muốn.

Không có tải ghi đồng thời

WITH input_rows(usr, contact, name) AS (
   VALUES
      (text 'foo1', text 'bar1', text 'bob1')  -- type casts in first row
    , ('foo2', 'bar2', 'bob2')
    -- more?
   )
, ins AS (
   INSERT INTO chats (usr, contact, name) 
   SELECT * FROM input_rows
   ON CONFLICT (usr, contact) DO NOTHING
   RETURNING id  --, usr, contact              -- return more columns?
   )
SELECT 'i' AS source                           -- 'i' for 'inserted'
     , id  --, usr, contact                    -- return more columns?
FROM   ins
UNION  ALL
SELECT 's' AS source                           -- 's' for 'selected'
     , c.id  --, usr, contact                  -- return more columns?
FROM   input_rows
JOIN   chats c USING (usr, contact);           -- columns of unique index

Các sourcecột là một sự bổ sung bắt buộc để chứng minh cách làm việc này. Bạn thực sự có thể cần nó để nói sự khác biệt giữa cả hai trường hợp (một lợi thế khác so với ghi trống).

Công JOIN chatsviệc cuối cùng vì các hàng mới được chèn từ CTE sửa đổi dữ liệu đính kèm chưa được hiển thị trong bảng bên dưới. (Tất cả các phần của cùng một câu lệnh SQL đều nhìn thấy các ảnh chụp nhanh giống nhau của các bảng bên dưới.)

VALUESbiểu thức là tự do (không được gắn trực tiếp vào một INSERT) Postgres không thể lấy được các kiểu dữ liệu từ các cột mục tiêu và bạn có thể phải thêm các kiểu phôi rõ ràng. Hướng dẫn sử dụng:

Khi VALUESđược sử dụng INSERT, tất cả các giá trị sẽ tự động được ép theo kiểu dữ liệu của cột đích tương ứng. Khi nó được sử dụng trong các bối cảnh khác, có thể cần phải chỉ định loại dữ liệu chính xác. Nếu các mục nhập là tất cả các hằng số được trích dẫn, việc ép buộc đầu tiên là đủ để xác định loại giả định cho tất cả.

Bản thân truy vấn (không tính các tác dụng phụ) có thể tốn kém hơn một chút cho một số bản sao, do chi phí hoạt động của CTE và phần bổ sung SELECT(nên rẻ vì chỉ số hoàn hảo có theo định nghĩa - một ràng buộc duy nhất được thực hiện với một chỉ số).

Có thể (nhiều) nhanh hơn cho nhiều bản sao. Chi phí hiệu quả của việc viết thêm phụ thuộc vào nhiều yếu tố.

Nhưng có ít tác dụng phụ và chi phí ẩn trong mọi trường hợp. Nhìn chung, nó có lẽ rẻ hơn.

Trình tự đính kèm vẫn được nâng cao, vì các giá trị mặc định được điền vào trước khi kiểm tra xung đột.

Về CTE:

Với tải ghi đồng thời

Giả sử READ COMMITTEDcách ly giao dịch mặc định . Liên quan:

Chiến lược tốt nhất để bảo vệ chống lại các điều kiện chủng tộc phụ thuộc vào yêu cầu chính xác, số lượng và kích thước của các hàng trong bảng và trong các UPSERT, số lượng giao dịch đồng thời, khả năng xảy ra xung đột, tài nguyên có sẵn và các yếu tố khác ...

Vấn đề đồng thời 1

Nếu một giao dịch đồng thời được ghi vào một hàng mà giao dịch của bạn hiện đang cố gắng gửi tới UPSERT, giao dịch của bạn phải đợi một giao dịch khác kết thúc.

Nếu giao dịch khác kết thúc bằng ROLLBACK(hoặc bất kỳ lỗi nào, tức là tự động ROLLBACK), giao dịch của bạn có thể tiến hành bình thường. Tác dụng phụ nhỏ có thể xảy ra: khoảng trống trong các số liên tiếp. Nhưng không thiếu hàng.

Nếu giao dịch khác kết thúc bình thường (ẩn hoặc rõ ràng COMMIT), bạn INSERTsẽ phát hiện xung đột ( UNIQUEchỉ số / ràng buộc là tuyệt đối) và DO NOTHINGdo đó cũng không trả về hàng. (Cũng không thể khóa hàng như thể hiện trong đồng thời vấn đề 2 bên dưới, vì nó không nhìn thấy được .) Các SELECTnhìn thấy ảnh chụp tương tự từ khi bắt đầu truy vấn và cũng không thể trả lại hàng nhưng vô hình.

Bất kỳ hàng nào như vậy bị thiếu trong tập kết quả (mặc dù chúng tồn tại trong bảng bên dưới)!

Điều này có thể là ok như là . Đặc biệt nếu bạn không trả về các hàng như trong ví dụ và hài lòng khi biết hàng đó ở đó. Nếu điều đó không đủ tốt, có nhiều cách khác nhau xung quanh nó.

Bạn có thể kiểm tra số hàng của đầu ra và lặp lại câu lệnh nếu nó không khớp với số hàng của đầu vào. Có thể đủ tốt cho trường hợp hiếm. Vấn đề là bắt đầu một truy vấn mới (có thể trong cùng một giao dịch), sau đó sẽ thấy các hàng mới được cam kết.

Hoặc kiểm tra các hàng kết quả bị thiếu trong cùng một truy vấn và ghi đè lên các hàng bằng thủ thuật vũ phu được thể hiện trong câu trả lời của Alexton .

WITH input_rows(usr, contact, name) AS ( ... )  -- see above
, ins AS (
   INSERT INTO chats AS c (usr, contact, name) 
   SELECT * FROM input_rows
   ON     CONFLICT (usr, contact) DO NOTHING
   RETURNING id, usr, contact                   -- we need unique columns for later join
   )
, sel AS (
   SELECT 'i'::"char" AS source                 -- 'i' for 'inserted'
        , id, usr, contact
   FROM   ins
   UNION  ALL
   SELECT 's'::"char" AS source                 -- 's' for 'selected'
        , c.id, usr, contact
   FROM   input_rows
   JOIN   chats c USING (usr, contact)
   )
, ups AS (                                      -- RARE corner case
   INSERT INTO chats AS c (usr, contact, name)  -- another UPSERT, not just UPDATE
   SELECT i.*
   FROM   input_rows i
   LEFT   JOIN sel   s USING (usr, contact)     -- columns of unique index
   WHERE  s.usr IS NULL                         -- missing!
   ON     CONFLICT (usr, contact) DO UPDATE     -- we've asked nicely the 1st time ...
   SET    name = c.name                         -- ... this time we overwrite with old value
   -- SET name = EXCLUDED.name                  -- alternatively overwrite with *new* value
   RETURNING 'u'::"char" AS source              -- 'u' for updated
           , id  --, usr, contact               -- return more columns?
   )
SELECT source, id FROM sel
UNION  ALL
TABLE  ups;

Giống như truy vấn ở trên, nhưng chúng tôi thêm một bước nữa với CTE ups, trước khi chúng tôi trả về tập kết quả hoàn chỉnh . Đó là CTE cuối cùng sẽ không làm gì hầu hết thời gian. Chỉ khi hàng bị mất từ ​​kết quả trả về, chúng tôi sử dụng lực lượng vũ phu.

Nhiều chi phí hơn, chưa. Càng nhiều xung đột với các hàng có sẵn, điều này càng có khả năng tốt hơn phương pháp đơn giản.

Một tác dụng phụ: UPSERT thứ 2 ghi các hàng không theo thứ tự, do đó, nó sẽ giới thiệu lại khả năng của các khóa chết (xem bên dưới) nếu ba hoặc nhiều giao dịch ghi vào cùng một hàng trùng nhau. Nếu đó là một vấn đề, bạn cần một giải pháp khác - như lặp lại toàn bộ tuyên bố như đã đề cập ở trên.

Vấn đề đồng thời 2

Nếu các giao dịch đồng thời có thể ghi vào các cột liên quan của các hàng bị ảnh hưởng và bạn phải đảm bảo các hàng bạn tìm thấy vẫn còn ở giai đoạn sau trong cùng một giao dịch, bạn có thể khóa các hàng hiện có với giá rẻ trong CTE ins(nếu không sẽ được mở khóa) với:

...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE  -- never executed, but still locks the row
...

Và thêm một điều khoản khóa SELECTlà tốt, nhưFOR UPDATE .

Điều này làm cho các hoạt động ghi cạnh tranh chờ đến khi kết thúc giao dịch, khi tất cả các khóa được phát hành. Vì vậy, hãy ngắn gọn.

Thêm chi tiết và giải thích:

Bế tắc?

Bảo vệ chống lại bế tắc bằng cách chèn các hàng theo thứ tự nhất quán . Xem:

Kiểu dữ liệu và phôi

Bảng hiện tại làm mẫu cho các loại dữ liệu ...

Loại phôi rõ ràng cho hàng dữ liệu đầu tiên trong VALUESbiểu thức đứng tự do có thể bất tiện. Có nhiều cách xung quanh nó. Bạn có thể sử dụng bất kỳ mối quan hệ hiện có (bảng, dạng xem, ...) làm mẫu hàng. Bảng đích là sự lựa chọn rõ ràng cho trường hợp sử dụng. Dữ liệu đầu vào được tự động ép buộc thành các loại thích hợp, như trong VALUESmệnh đề của một INSERT:

WITH input_rows AS (
  (SELECT usr, contact, name FROM chats LIMIT 0)  -- only copies column names and types
   UNION ALL
   VALUES
      ('foo1', 'bar1', 'bob1')  -- no type casts here
    , ('foo2', 'bar2', 'bob2')
   )
   ...

Điều này không làm việc cho một số loại dữ liệu. Xem:

... và tên

Điều này cũng hoạt động cho tất cả các loại dữ liệu.

Trong khi chèn vào tất cả các cột (hàng đầu) của bảng, bạn có thể bỏ qua các tên cột. Bảng giả sử chatstrong ví dụ chỉ bao gồm 3 cột được sử dụng trong UPSERT:

WITH input_rows AS (
   SELECT * FROM (
      VALUES
      ((NULL::chats).*)         -- copies whole row definition
      ('foo1', 'bar1', 'bob1')  -- no type casts needed
    , ('foo2', 'bar2', 'bob2')
      ) sub
   OFFSET 1
   )
   ...

Bên cạnh: không sử dụng từ dành riêng như "user"là định danh. Đó là một khẩu súng ngắn nạp đạn. Sử dụng định danh hợp pháp, chữ thường, không trích dẫn. Tôi đã thay thế nó bằng usr.


2
Bạn ngụ ý phương pháp này sẽ không tạo ra các khoảng trống trong các sê-ri, nhưng chúng là:
XÁC NHẬN

1
không phải là vấn đề nhiều như vậy, nhưng tại sao nó nối tiếp được tăng lên? và không có cách nào để tránh điều này?
nổi bật

1
@salient: Giống như tôi đã thêm ở trên: các giá trị mặc định của cột được điền vào trước khi kiểm tra các xung đột và chuỗi không bao giờ được khôi phục, để tránh xung đột với ghi đồng thời.
Erwin Brandstetter

7
Đáng kinh ngạc. Hoạt động như một nét duyên dáng và dễ hiểu một khi bạn nhìn vào nó một cách cẩn thận. Tôi vẫn ước ON CONFLICT SELECT...nơi có một thứ mặc dù :)
Roshambo

3
Đáng kinh ngạc. Những người tạo ra Postgres dường như đang tra tấn người dùng. Tại sao không chỉ đơn giản là làm cho trở về khoản luôn trả về giá trị, bất kể có chèn hay không?
Anatoly Alekseev

16

Upsert, là một phần mở rộng của INSERTtruy vấn có thể được xác định bằng hai hành vi khác nhau trong trường hợp có xung đột ràng buộc: DO NOTHINGhoặc DO UPDATE.

INSERT INTO upsert_table VALUES (2, 6, 'upserted')
   ON CONFLICT DO NOTHING RETURNING *;

 id | sub_id | status
----+--------+--------
 (0 rows)

Cũng lưu ý rằng RETURNINGkhông trả về gì cả, vì không có bộ dữ liệu nào được chèn vào . Bây giờ với DO UPDATE, có thể thực hiện các hoạt động trên tuple có xung đột với. Đầu tiên lưu ý rằng điều quan trọng là xác định một ràng buộc sẽ được sử dụng để xác định rằng có xung đột.

INSERT INTO upsert_table VALUES (2, 2, 'inserted')
   ON CONFLICT ON CONSTRAINT upsert_table_sub_id_key
   DO UPDATE SET status = 'upserted' RETURNING *;

 id | sub_id |  status
----+--------+----------
  2 |      2 | upserted
(1 row)

2
Cách hay để luôn nhận được id hàng bị ảnh hưởng và biết liệu đó là chèn hay tăng. Đúng thứ tôi cần.
Vịt Moby

Điều này vẫn đang sử dụng "Do Update", những nhược điểm đã được thảo luận.
Bill Worthington

4

Đối với phần chèn thêm của một mục, tôi có thể sẽ sử dụng kết hợp khi trả về id:

WITH new_chats AS (
    INSERT INTO chats ("user", "contact", "name")
    VALUES ($1, $2, $3)
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
) SELECT COALESCE(
    (SELECT id FROM new_chats),
    (SELECT id FROM chats WHERE user = $1 AND contact = $2)
);

2
WITH e AS(
    INSERT INTO chats ("user", "contact", "name") 
           VALUES ($1, $2, $3), 
                  ($2, $1, NULL) 
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
)
SELECT * FROM e
UNION
    SELECT id FROM chats WHERE user=$1, contact=$2;

Mục đích chính của việc sử dụng ON CONFLICT DO NOTHINGlà để tránh lỗi ném, nhưng nó sẽ không trả về hàng. Vì vậy, chúng ta cần một cái khác SELECTđể có được id hiện có.

Trong SQL này, nếu nó không xảy ra xung đột, nó sẽ không trả về gì, sau đó lần thứ hai SELECTsẽ nhận được hàng hiện có; Nếu nó chèn thành công, thì sẽ có hai bản ghi giống nhau, sau đó chúng ta cần UNIONhợp nhất kết quả.


Giải pháp này hoạt động tốt và tránh thực hiện ghi (cập nhật) không cần thiết cho DB !! Đẹp!
Simon C

0

Tôi đã sửa đổi câu trả lời tuyệt vời của Erwin Brandstetter, điều này sẽ không làm tăng trình tự và cũng sẽ không ghi khóa bất kỳ hàng nào. Tôi còn khá mới đối với PostgreSQL, vì vậy xin vui lòng cho tôi biết nếu bạn thấy bất kỳ nhược điểm nào đối với phương pháp này:

WITH input_rows(usr, contact, name) AS (
   VALUES
      (text 'foo1', text 'bar1', text 'bob1')  -- type casts in first row
    , ('foo2', 'bar2', 'bob2')
    -- more?
   )
, new_rows AS (
   SELECT 
     c.usr
     , c.contact
     , c.name
     , r.id IS NOT NULL as row_exists
   FROM input_rows AS r
   LEFT JOIN chats AS c ON r.usr=c.usr AND r.contact=c.contact
   )
INSERT INTO chats (usr, contact, name)
SELECT usr, contact, name
FROM new_rows
WHERE NOT row_exists
RETURNING id, usr, contact, name

Điều này giả định rằng bảng chatscó một ràng buộc duy nhất trên các cột(usr, contact) .

Cập nhật: đã thêm các sửa đổi được đề xuất từ spatar (bên dưới). Cảm ơn!


1
Thay vì CASE WHEN r.id IS NULL THEN FALSE ELSE TRUE END AS row_existschỉ viết r.id IS NOT NULL as row_exists. Thay vì WHERE row_exists=FALSEchỉ viết WHERE NOT row_exists.
spatar
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.