Sự cố UPSERT của PostgreSQL với các giá trị NULL


13

Tôi gặp sự cố khi sử dụng tính năng UPSERT mới trong Postgres 9.5

Tôi có một bảng được sử dụng để tổng hợp dữ liệu từ một bảng khác. Khóa tổng hợp được tạo thành từ 20 cột, 10 trong số đó có thể là null. Dưới đây tôi đã tạo một phiên bản nhỏ hơn của vấn đề tôi đang gặp phải, cụ thể là với các giá trị NULL.

CREATE TABLE public.test_upsert (
upsert_id serial,
name character varying(32) NOT NULL,
status integer NOT NULL,
test_field text,
identifier character varying(255),
count integer,
CONSTRAINT upsert_id_pkey PRIMARY KEY (upsert_id),
CONSTRAINT test_upsert_name_status_test_field_key UNIQUE (name, status, test_field)
);

Chạy truy vấn này hoạt động khi cần thiết (Chèn đầu tiên, sau đó chèn tiếp theo chỉ đơn giản là tăng số lượng):

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

Tuy nhiên, nếu tôi chạy truy vấn này, mỗi lần 1 hàng được chèn thay vì tăng số đếm cho hàng ban đầu:

INSERT INTO test_upsert as tu(name,status,test_field,identifier, count) 
VALUES ('shaun',1,null,'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 = null;

Đây là vấn đề của tôi. Tôi chỉ cần tăng giá trị đếm và không tạo nhiều hàng giống nhau với giá trị null.

Cố gắng thêm một chỉ mục duy nhất một phần:

CREATE UNIQUE INDEX test_upsert_upsert_id_idx
ON public.test_upsert
USING btree
(name COLLATE pg_catalog."default", status, test_field, identifier);

Tuy nhiên, điều này mang lại kết quả tương tự, hoặc nhiều hàng null được chèn hoặc thông báo lỗi này khi cố gắng chèn:

LRI: không có ràng buộc duy nhất hoặc loại trừ phù hợp với đặc điểm kỹ thuật ON CONFLICT

Tôi đã cố gắng thêm chi tiết bổ sung vào chỉ mục một phần, chẳng hạn như WHERE test_field is not null OR identifier is not null. Tuy nhiên, khi chèn tôi nhận được thông báo lỗi ràng buộc.

Câu trả lời:


14

Làm rõ ON CONFLICT DO UPDATEhà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_targetbị 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 WHEREmệ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 WHEREkhoả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 WHEREmệ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ốnNULLcá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)(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 NULLvà mọi thứ sẽ hoạt động tốt với một UNIQUErà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 countvà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.


1
Đẹp. Tôi đã bỏ qua phần 20-10 cột trong lần đầu tiên tôi đọc câu hỏi và không có thời gian để hoàn thành sau này. Có count = CASE WHEN EXCLUDED.count IS NULL THEN tu.count ELSE COALESCE(tu.count, 0) + COALESCE(EXCLUDED.count, 0) ENDthể đơn giản hóa thànhcount = COALESCE(tu.count+EXCLUDED.count, EXCLUDED.count, tu.count)
ypercubeᵀᴹ

Nhìn lại, phiên bản "đơn giản hóa" của tôi không phải là tài liệu tự.
ypercubeᵀᴹ

@ ypercubeᵀᴹ: Tôi đã áp dụng bản cập nhật được đề xuất của bạn. Nó đơn giản hơn, cảm ơn.
Erwin Brandstetter

@ErwinBrandstetter bạn là người giỏi nhất
Seamus abshere

7

Tôi nghĩ vấn đề là bạn không có chỉ mục một phần và ON CONFLICTcú pháp không khớp với test_upsert_upsert_id_idxchỉ mục nhưng ràng buộc duy nhất khác.

Nếu bạn xác định chỉ mục là một phần (với WHERE test_field IS NULL):

CREATE UNIQUE INDEX test_upsert_upsert_id_idx
ON public.test_upsert
USING btree
(name COLLATE pg_catalog."default", status)
WHERE test_field IS NULL ;

và các hàng này đã có trong bảng:

INSERT INTO test_upsert as tu
    (name, status, test_field, identifier, count) 
VALUES 
    ('shaun', 1, null, 'ident', 1),
    ('maria', 1, null, 'ident', 1) ;

sau đó truy vấn sẽ thành công:

INSERT INTO test_upsert as tu
    (name, status, test_field, identifier, count) 
VALUES 
    ('peter', 1,   17, 'ident', 1),
    ('shaun', 1, null, 'ident', 3),
    ('maria', 1, null, 'ident', 7)
ON CONFLICT 
    (name, status) WHERE test_field IS NULL   -- the conflicting condition
DO UPDATE SET
    count = tu.count + EXCLUDED.count 
WHERE                                         -- when to update
    tu.name = 'shaun' AND tu.status = 1 ;     -- if you don't want all of the
                                              -- updates to happen

với kết quả như sau:

('peter', 1,   17, 'ident', 1)  -- no conflict: row inserted

('shaun', 1, null, 'ident', 3)  -- conflict: no insert
                           -- matches where: row updated with count = 1+3 = 4

('maria', 1, null, 'ident', 1)  -- conflict: no insert
                     -- doesn't match where: no update

Điều này làm rõ cách sử dụng một chỉ mục một phần. Nhưng (tôi nghĩ) nó vẫn chưa giải quyết được vấn đề.
Erwin Brandstetter

không nên tính cho 'maria' ở mức 1 vì không có cập nhật nào xảy ra?
mpprdev

@mpprdev vâng, bạn nói đúng.
ypercubeᵀᴹ
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.