Tại sao các hàng được chèn trong CTE không được cập nhật trong cùng một tuyên bố?


11

Trong PostgreSQL 9.5, được cung cấp một bảng đơn giản được tạo bằng:

create table tbl (
    id serial primary key,
    val integer
);

Tôi chạy SQL để XÁC NHẬN một giá trị, sau đó CẬP NHẬT nó trong cùng một câu lệnh:

WITH newval AS (
    INSERT INTO tbl(val) VALUES (1) RETURNING id
) UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id;

Kết quả là CẬP NHẬT bị bỏ qua:

testdb=> select * from tbl;
┌────┬─────┐
 id  val 
├────┼─────┤
  1    1 
└────┴─────┘

Tại sao lại thế này? Đây có phải là một phần giới hạn của tiêu chuẩn SQL (nghĩa là hiện diện trong các cơ sở dữ liệu khác) hoặc một cái gì đó cụ thể đối với PostgreQuery có thể được sửa trong tương lai không? Các VỚI truy vấn tài liệu nói nhiều thông tin cập nhật không được hỗ trợ, nhưng không đề cập đến chèn và cập nhật.

Câu trả lời:


13

Tất cả các tuyên bố trong CTE xảy ra hầu như cùng một lúc. Tức là, chúng dựa trên cùng một ảnh chụp nhanh của cơ sở dữ liệu.

Cái UPDATEnhìn thấy trạng thái tương tự của bảng bên dưới là INSERT, có nghĩa là hàng val = 1không có ở đó. Hướng dẫn làm rõ ở đây:

Tất cả các câu lệnh được thực hiện với cùng một ảnh chụp nhanh (xem Chương 13 ), vì vậy chúng không thể "nhìn thấy" các hiệu ứng của nhau trên các bảng đích.

Mỗi câu lệnh có thể thấy những gì được trả về bởi một CTE khác trong RETURNINGmệnh đề. Nhưng các bảng bên dưới trông giống như tất cả chúng.

Bạn sẽ cần hai báo cáo (trong một giao dịch) cho những gì bạn đang cố gắng thực hiện. Ví dụ đã cho thực sự chỉ nên là một đơn INSERTđể bắt đầu, nhưng đó có thể là do ví dụ đơn giản hóa.


13

Đây là một quyết định thực hiện. Nó được mô tả trong tài liệu Postgres, WITHTruy vấn (Biểu thức bảng chung) . Có hai đoạn liên quan đến vấn đề.

Đầu tiên, lý do cho hành vi được quan sát:

Các báo cáo phụ WITHđược thực hiện đồng thời với nhau và với truy vấn chính . Do đó, khi sử dụng các câu lệnh sửa đổi dữ liệu WITH, thứ tự các cập nhật được chỉ định thực sự xảy ra là không thể đoán trước. Tất cả các câu lệnh được thực hiện với cùng một ảnh chụp nhanh (xem Chương 13), vì vậy chúng không thể "nhìn thấy" các hiệu ứng của nhau trên các bảng đích. Điều này làm giảm bớt ảnh hưởng của tính không thể đoán trước của thứ tự cập nhật hàng thực tế và có nghĩa là RETURNINGdữ liệu là cách duy nhất để truyền đạt các thay đổi giữa các WITHcâu lệnh phụ khác nhau và truy vấn chính. Một ví dụ về điều này là trong ...

Sau khi tôi đăng một đề nghị cùng với pssql-docs , Marko Tiikkaja đã giải thích (đồng ý với câu trả lời của Erwin):

Các trường hợp chèn-cập nhật và chèn-xóa không hoạt động vì các CẬP NHẬT và XÓA không có cách nào nhìn thấy các hàng được CHỌN do ảnh chụp nhanh của chúng đã được thực hiện trước khi xảy ra INSERT. Không có gì khó lường về hai trường hợp này.

Vì vậy, lý do tại sao tuyên bố của bạn không cập nhật có thể được giải thích bằng đoạn đầu tiên ở trên (về "ảnh chụp nhanh"). Điều xảy ra khi bạn sửa đổi CTE là tất cả chúng và truy vấn chính được thực thi và "xem" cùng một ảnh chụp nhanh của dữ liệu (bảng), vì chúng ngay lập tức trước khi thực hiện câu lệnh. CTE có thể chuyển thông tin về những gì họ chèn / cập nhật / xóa cho nhau và truy vấn chính bằng cách sử dụng RETURNINGmệnh đề nhưng họ không thể thấy các thay đổi trực tiếp trong các bảng. Vì vậy, hãy xem những gì xảy ra trong tuyên bố của bạn:

WITH newval AS (
    INSERT INTO tbl(val) VALUES (1) RETURNING id
) UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id;

Chúng tôi có 2 phần, CTE ( newval):

-- newval
     INSERT INTO tbl(val) VALUES (1) RETURNING id

và truy vấn chính:

-- main 
UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id

Luồng thực thi là như thế này:

           initial data: tbl
                id  val 
                 (empty)
               /         \
              /           \
             /             \
    newval:                 \
       tbl (after newval)    \
           id  val           \
            1    1           |
                              |
    newval: returns           |
           id                 |
            1                 |
               \              |
                \             |
                 \            |
                    main query

Kết quả là, khi truy vấn chính kết hợp tbl(như được thấy trong ảnh chụp nhanh) với newvalbảng, nó sẽ nối một bảng trống với bảng 1 hàng. Rõ ràng nó cập nhật 0 hàng. Vì vậy, tuyên bố không bao giờ thực sự đến để sửa đổi hàng vừa được chèn và đó là những gì bạn thấy.

Giải pháp trong trường hợp của bạn là viết lại câu lệnh để chèn các giá trị đúng vào vị trí đầu tiên hoặc sử dụng 2 câu lệnh. Một trong đó chèn và một giây để cập nhật.


Có những tình huống khác, tương tự, như nếu câu lệnh có một INSERTvà sau đó a DELETEtrên cùng một hàng. Việc xóa sẽ thất bại vì những lý do chính xác như vậy.

Một số trường hợp khác, với cập nhật-cập nhật và cập nhật-xóa và hành vi của chúng được giải thích trong một đoạn sau, trong cùng một trang tài liệu.

Cố gắng cập nhật cùng một hàng hai lần trong một câu lệnh không được hỗ trợ. Chỉ có một trong những sửa đổi diễn ra, nhưng không dễ dàng (và đôi khi không thể) để dự đoán một cách đáng tin cậy cái nào. Điều này cũng áp dụng để xóa một hàng đã được cập nhật trong cùng một tuyên bố: chỉ cập nhật được thực hiện. Do đó, bạn thường nên tránh cố gắng sửa đổi một hàng hai lần trong một câu lệnh. Cụ thể, tránh viết các câu lệnh phụ có thể ảnh hưởng đến cùng một hàng được thay đổi bởi câu lệnh chính hoặc câu lệnh phụ anh chị em. Những ảnh hưởng của một tuyên bố như vậy sẽ không thể dự đoán được.

Và trong thư trả lời từ Marko Tiikkaja:

Các trường hợp cập nhật-cập nhật và cập nhật-xóa không rõ ràng gây ra bởi cùng một chi tiết triển khai cơ bản (như các trường hợp chèn-cập nhật và chèn-xóa).
Trường hợp cập nhật cập nhật không hoạt động vì bên trong nó trông giống như sự cố Halloween và Postgres không có cách nào để biết bộ dữ liệu nào sẽ ổn khi cập nhật hai lần và cái nào có thể giới thiệu lại vấn đề Halloween.

Vì vậy, lý do là như nhau (cách sửa đổi CTE được thực hiện và cách mỗi CTE nhìn thấy cùng một ảnh chụp nhanh) nhưng chi tiết khác nhau trong 2 trường hợp này, vì chúng phức tạp hơn và kết quả có thể không dự đoán được trong trường hợp cập nhật-cập nhật.

Trong bản cập nhật chèn (như trường hợp của bạn) và chèn-xóa tương tự, kết quả có thể dự đoán được. Chỉ có thao tác chèn xảy ra khi thao tác thứ hai (cập nhật hoặc xóa) không có cách nào để xem và ảnh hưởng đến các hàng mới được chèn.


Mặc dù vậy, giải pháp được đề xuất là giống nhau cho tất cả các trường hợp cố gắng sửa đổi cùng một hàng nhiều lần: Đừng làm điều đó. Viết các câu lệnh sửa đổi mỗi hàng một lần hoặc sử dụng các câu lệnh riêng biệt (2 hoặc nhiều hơn).

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.