Làm rõ ON CONFLICT DO UPDATE
hành vi
Xem xét hướng dẫn ở đây :
Đối với mỗi hàng riêng lẻ được đề xuất để chèn, quá trình chèn sẽ tiến hành hoặc, nếu một ràng buộc trọng tài hoặc chỉ số được chỉ định bởi
conflict_target
bị vi phạm, phương án thay thế conflict_action
được thực hiện.
Nhấn mạnh đậm của tôi. Vì vậy, bạn không phải lặp lại các biến vị ngữ cho các cột được bao gồm trong chỉ mục duy nhất trong WHERE
mệnh đề cho UPDATE
(the conflict_action
):
INSERT INTO test_upsert AS tu
(name , status, test_field , identifier, count)
VALUES ('shaun', 1 , 'test value', 'ident' , 1)
ON CONFLICT (name, status, test_field) DO UPDATE
SET count = tu.count + 1;
WHERE tu.name = 'shaun' AND tu.status = 1 AND tu.test_field = 'test value'
Vi phạm duy nhất đã thiết lập điều WHERE
khoản bổ sung của bạn sẽ thi hành một cách dư thừa.
Làm rõ chỉ số một phần
Thêm một WHERE
mệnh đề để biến nó thành một chỉ mục một phần thực tế như bạn đã đề cập đến chính mình (nhưng với logic đảo ngược):
CREATE UNIQUE INDEX test_upsert_partial_idx
ON public.test_upsert (name, status)
WHERE test_field IS NULL; -- not: "is not null"
Để sử dụng chỉ mục một phần này trong UPSERT của bạn, bạn cần một kết quả khớp như @ypercube chứng minh :conflict_target
ON CONFLICT (name, status) WHERE test_field IS NULL
Bây giờ chỉ số một phần ở trên được suy ra. Tuy nhiên , như hướng dẫn cũng lưu ý :
[...] một chỉ mục duy nhất không phải là một phần (một chỉ mục duy nhất không có biến vị ngữ) sẽ được suy ra (và do đó được sử dụng bởi ON CONFLICT
) nếu một chỉ mục đó đáp ứng mọi tiêu chí khác có sẵn.
Nếu bạn có một chỉ mục bổ sung (hoặc duy nhất), (name, status)
nó sẽ (cũng) được sử dụng. Một chỉ số trên (name, status, test_field)
rõ ràng sẽ không được suy luận. Điều này không giải thích vấn đề của bạn, nhưng có thể đã thêm vào sự nhầm lẫn trong khi thử nghiệm.
Giải pháp
AIUI, không có điều nào ở trên giải quyết được vấn đề của bạn . Với chỉ mục một phần, chỉ những trường hợp đặc biệt có giá trị NULL phù hợp mới được bắt gặp. Và các hàng trùng lặp khác sẽ được chèn nếu bạn không có các chỉ mục / ràng buộc duy nhất phù hợp khác hoặc đưa ra một ngoại lệ nếu bạn làm như vậy. Tôi cho rằng đó không phải là những gì bạn muốn. Bạn viết:
Khóa tổng hợp được tạo thành từ 20 cột, 10 trong số đó có thể là null.
Chính xác những gì bạn xem xét một bản sao? Postgres (theo tiêu chuẩn SQL) không coi hai giá trị NULL là bằng nhau. Hướng dẫn sử dụng:
Nói chung, một ràng buộc duy nhất bị vi phạm nếu có nhiều hơn một hàng trong bảng trong đó các giá trị của tất cả các cột được bao gồm trong ràng buộc là bằng nhau. Tuy nhiên, hai giá trị null không bao giờ được coi là bằng nhau trong so sánh này. Điều đó có nghĩa là ngay cả khi có một ràng buộc duy nhất, có thể lưu trữ các hàng trùng lặp có chứa giá trị null trong ít nhất một trong các cột bị ràng buộc. Hành vi này phù hợp với tiêu chuẩn SQL, nhưng chúng tôi đã nghe nói rằng các cơ sở dữ liệu SQL khác có thể không tuân theo quy tắc này. Vì vậy, hãy cẩn thận khi phát triển các ứng dụng dự định là di động.
Liên quan:
Tôi giả sử bạn muốnNULL
các giá trị trong tất cả 10 cột không thể được coi là bằng nhau. Thật là thanh lịch và thiết thực khi bao gồm một cột nullable duy nhất với một chỉ mục một phần bổ sung như được trình bày ở đây:
Nhưng điều này nhanh chóng vượt khỏi tầm kiểm soát đối với các cột rỗng hơn. Bạn sẽ cần một chỉ mục một phần cho mọi kết hợp khác nhau của các cột không thể. Chỉ có 2 trong số đó là 3 chỉ mục một phần cho (a)
, (b)
và (a,b)
. Con số đang tăng theo cấp số nhân 2^n - 1
. Đối với 10 cột không thể bỏ qua của bạn, để bao gồm tất cả các kết hợp giá trị NULL có thể, bạn đã cần 1023 chỉ mục một phần. Không đi.
Giải pháp đơn giản: thay thế các giá trị NULL và xác định các cột liên quan NOT NULL
và mọi thứ sẽ hoạt động tốt với một UNIQUE
ràng buộc đơn giản .
Nếu đó không phải là một tùy chọn, tôi đề xuất một chỉ mục biểu thức COALESCE
để thay thế NULL trong chỉ mục:
CREATE UNIQUE INDEX test_upsert_solution_idx
ON test_upsert (name, status, COALESCE(test_field, ''));
Chuỗi rỗng ( ''
) là một ứng cử viên rõ ràng cho các loại ký tự, nhưng bạn có thể sử dụng bất kỳ giá trị pháp lý nào không bao giờ xuất hiện hoặc có thể được gấp lại bằng NULL theo định nghĩa của bạn về "duy nhất".
Sau đó sử dụng tuyên bố này:
INSERT INTO test_upsert as tu(name,status,test_field,identifier, count)
VALUES ('shaun', 1, null , 'ident', 11) -- works with
, ('bob' , 2, 'test value', 'ident', 22) -- and without NULL
ON CONFLICT (name, status, COALESCE(test_field, '')) DO UPDATE -- match expr. index
SET count = COALESCE(tu.count + EXCLUDED.count, EXCLUDED.count, tu.count);
Giống như @ypercube Tôi giả sử bạn thực sự muốn thêm count
vào số lượng hiện có. Vì cột có thể là NULL, nên thêm NULL sẽ đặt cột NULL. Nếu bạn xác định count NOT NULL
, bạn có thể đơn giản hóa.
Một ý tưởng khác là chỉ cần loại bỏ mâu thuẫn từ câu lệnh để bao gồm tất cả các vi phạm duy nhất . Sau đó, bạn có thể định nghĩa các chỉ mục duy nhất khác nhau cho một định nghĩa phức tạp hơn về những gì được cho là "duy nhất". Nhưng điều đó sẽ không bay cùng ON CONFLICT DO UPDATE
. Hướng dẫn một lần nữa:
Đối với ON CONFLICT DO NOTHING
, nó là tùy chọn để chỉ định mâu thuẫn; khi được bỏ qua, xung đột với tất cả các ràng buộc có thể sử dụng (và các chỉ mục duy nhất) được xử lý. Đối với ON CONFLICT DO UPDATE
, một mâu thuẫn phải được cung cấp.
count = CASE WHEN EXCLUDED.count IS NULL THEN tu.count ELSE COALESCE(tu.count, 0) + COALESCE(EXCLUDED.count, 0) END
thể đơn giản hóa thànhcount = COALESCE(tu.count+EXCLUDED.count, EXCLUDED.count, tu.count)