PostgreSQL
 sql >> Cơ Sở Dữ Liệu >  >> RDS >> PostgreSQL

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

Câu trả lời hiện được chấp nhận có vẻ ổn đối với một mục tiêu xung đột duy nhất, ít xung đột, bộ giá trị nhỏ và không có trình kích hoạt. Nó tránh vấn đề đồng thời 1 (xem bên dưới) với tính 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 không thấy sự khác biệt nào trên bề mặt, thì vẫn có nhiều tác dụng phụ khác nhau :

  • Nó có thể kích hoạt các trình kích hoạt không nên kích hoạt.

  • Nó khóa 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 PostgreSQL, một phiên bản hàng mới được viết cho mỗi UPDATE , bất kể dữ liệu hàng có thay đổi hay không. Điều này dẫn đến hình phạt hiệu suất cho chính UPSERT, phồng bảng, phồng chỉ mục, hình phạt hiệu suất cho các hoạt động tiếp theo trên bảng, VACUUM Giá cả. Một hiệu ứng nhỏ đối với một số bản sao, nhưng lớn hầu hết là lừa đảo.

Thêm nữa , đôi khi 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 , một conflict_target phải được cung cấp.

Đ ĩa đơn "mục tiêu xung đột" là không thể nếu nhiều chỉ mục / ràng buộc có liên quan. Nhưng đây là một giải pháp liên quan cho nhiều chỉ mục từng phần:

  • UPSERT dựa trên ràng buộc DUY NHẤT với giá trị NULL

Quay lại chủ đề, bạn có thể đạt được (gần như) như vậy mà không cần cập nhật trống rỗng và tác dụng phụ. Một số giải pháp sau 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ó thể xảy ra xung đột - có thể có hoặc có thể không mong muốn.

Không 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

Nguồn source là phần bổ sung tùy chọn để chứng minh cách hoạt động của cột này. Bạn có thể thực sự cần nó để biết 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).

Cuộc trò chuyện JOIN chats hoạt động vì các hàng mới được chèn từ CTE sửa đổi dữ liệu đính kèm chưa 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 sẽ thấy các ảnh chụp nhanh giống nhau của các bảng bên dưới.)

Kể từ VALUES biểu thức là độc lập (không được gắn trực tiếp vào INSERT ) Postgres không thể lấy 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 phôi kiểu rõ ràng. Hướng dẫn sử dụng:

Khi VALUES được sử dụng trong INSERT , tất cả các giá trị đều được tự động buộc vào kiểu dữ liệu của cột đích tương ứng. Whenit được sử dụng trong các ngữ cảnh khác, có thể cần phải chỉ định kiểu dữ liệu chính xác. Nếu tất cả các mục nhập đều là hằng số được trích dẫn, thì việc ép buộc mục nhập đầu tiên là đủ để xác định kiểu 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 hơn một chút đối với vài lừa đảo, do chi phí của CTE và SELECT bổ sung (nên rẻ vì có chỉ mục hoàn hảo theo định nghĩa - một ràng buộc duy nhất được thực hiện với một chỉ mục).

Có thể nhanh hơn (nhiều) đối với nhiều các 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ụ hơn và chi phí ẩn trong bất kỳ trường hợp nào. Nhìn chung có lẽ nó rẻ hơn.

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

Giới thiệu về CTE:

  • Các truy vấn loại SELECT có phải là loại duy nhất có thể được lồng vào nhau không?
  • Các câu lệnh SELECT trùng lặp trong phép chia quan hệ

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

Giả sử READ COMMITTED mặc định cô lập giao dịch. Có liên quan:

  • Các giao dịch đồng thời dẫn đến tình trạng chạy đua với ràng buộc duy nhất về chèn

Chiến lược tốt nhất để bảo vệ trước các điều kiện chủng tộc phụ thuộc vào các 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 UPSERT, số lượng giao dịch đồng thời, khả năng xảy ra xung đột, tài nguyên sẵn có và các yếu tố khác ...

Vấn đề tiền tệ 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 UPSERT, giao dịch của bạn phải đợi giao dịch kia 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à ROLLBACK tự động ), giao dịch của bạn có thể diễn ra bình thường. Tác dụng phụ nhỏ nhất có thể xảy ra:khoảng cách về số thứ tự. Nhưng không có hàng nào bị thiếu.

Nếu giao dịch khác kết thúc bình thường (COMMIT ngầm hoặc rõ ràng ), INSERT của bạn sẽ phát hiện xung đột (UNIQUE chỉ mục / ràng buộc là tuyệt đối) và DO NOTHING , do đó cũng không trả lại hàng. (Cũng không thể khóa hàng như đã trình bày trong vấn đề đồng thời 2 bên dưới, vì nó không hiển thị .) SELECT nhìn thấy cùng một ảnh chụp nhanh từ đầu truy vấn và cũng không thể trả lại hàng chưa ẩn. ​​

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

Điều này có thể ổn như hiện tại . Đặc biệt nếu bạn không trả lại các hàng như trong ví dụ và hài lòng khi biết hàng đó ở đó. Nếu điều đó là không đủ tốt, có nhiều cách khác nhau để giải quyết vấ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 hoi. 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 đè những kẻ có thủ đoạn vũ phu thể hiện trong câu trả lời của Alextoni.

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;

Nó 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ả lại hoàn tất tập kết quả. CTE cuối cùng đó sẽ không làm gì trong hầu hết thời gian. Chỉ khi các hàng bị thiếu trong kết quả trả về, chúng tôi sử dụng brute force.

Thêm chi phí, được nêu ra. Càng có nhiều xung đột với các hàng có sẵn, điều này càng có nhiều khả năng hoạt động tốt hơn so với cách tiếp cận đơn giản.

Một tác dụng phụ:UPSERT thứ 2 viết các hàng không theo thứ tự, vì vậy nó giới thiệu lại khả năng deadlock (xem bên dưới) nếu ba hoặc nhiều hơn các giao dịch ghi vào các hàng giống nhau chồng chéo lên nhau. Nếu đó là vấn đề, bạn cần một giải pháp khác - như lặp lại toàn bộ câu lệnh 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 ở đó ở 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ó 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 mệnh đề khóa vào SELECT cũng như FOR UPDATE .

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

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

  • Cách bao gồm các hàng bị loại trừ trong QUAY LẠI khỏi CHÈN ... BẬT MẶC BẤT NGỜ
  • CHỌN hoặc CHÈN trong một hàm có nguy cơ gặp phải các điều kiện về chủng tộc không?

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:

  • Chốt lại với các CHÈN nhiều hàng mặc dù KHÔNG CÓ GÌ MẶC BẬT

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

Bảng hiện có làm mẫu cho các kiểu dữ liệu ...

Loại rõ ràng truyền cho hàng dữ liệu đầu tiên trong VALUES độc lập biểu hiện có thể không thuận tiện. Có nhiều cách xung quanh nó. Bạn có thể sử dụng bất kỳ quan hệ hiện có nào (bảng, dạng xem, ...) làm mẫu hàng. Bảng mục tiêu 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 VALUES mệ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 hoạt động đối với một số kiểu dữ liệu. Xem:

  • Truyền kiểu NULL khi cập nhật nhiều hàng

... và tên

Điều này cũng hoạt động cho tất cả kiểu 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 tên cột. Giả sử bảng chats trong 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
   )
   ...

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



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Bảng in không vừa với kích thước trang

  2. Laravel:Lỗi [PDOException]:Không thể tìm thấy trình điều khiển trong PostgreSQL

  3. Ánh xạ cột PostgreSQL JSON thành thuộc tính thực thể Hibernate

  4. Không tìm thấy lớp đang tải JDBC org.postgresql.Driver

  5. Mô phỏng TẠO CƠ SỞ DỮ LIỆU NẾU KHÔNG TỒN TẠI cho PostgreSQL?