ปัญหา PostgreSQL UPSERT ด้วยค่า NULL


13

ฉันมีปัญหากับการใช้คุณสมบัติใหม่ของ UPSERT ใน Postgres 9.5

ฉันมีตารางที่ใช้สำหรับรวบรวมข้อมูลจากตารางอื่น คีย์ผสมประกอบด้วย 20 คอลัมน์โดย 10 ซึ่งสามารถเป็นโมฆะได้ ด้านล่างฉันได้สร้างรุ่นที่เล็กกว่าของปัญหาที่ฉันมีโดยเฉพาะกับค่าเป็นศูนย์

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

การเรียกใช้คิวรีนี้ทำงานได้ตามต้องการ (แทรกครั้งแรกจากนั้นแทรกตามมาก็เพิ่มจำนวน):

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

อย่างไรก็ตามถ้าฉันเรียกใช้คิวรีนี้ 1 แถวจะถูกแทรกในแต่ละครั้งแทนที่จะเพิ่มจำนวนสำหรับแถวเริ่มต้น:

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;

นี่คือปัญหาของฉัน ฉันต้องการเพียงแค่เพิ่มค่าการนับและไม่สร้างแถวที่เหมือนกันหลายค่าด้วยค่า Null

ความพยายามที่จะเพิ่มดัชนีเฉพาะบางส่วน:

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

อย่างไรก็ตามสิ่งนี้ให้ผลลัพธ์ที่เหมือนกันไม่ว่าจะเป็นแถว null หลายแถวที่ถูกแทรกหรือข้อความแสดงข้อผิดพลาดเมื่อพยายามแทรก

ข้อผิดพลาด: ไม่มีข้อ จำกัด เฉพาะหรือข้อยกเว้นที่ตรงกับข้อกำหนด ON CONFLICT

WHERE test_field is not null OR identifier is not nullฉันพยายามแล้วที่จะเพิ่มรายละเอียดเพิ่มเติมเกี่ยวกับดัชนีบางส่วนเช่น อย่างไรก็ตามเมื่อแทรกฉันจะได้รับข้อความแสดงข้อผิดพลาดข้อ จำกัด

คำตอบ:


15

ชี้แจงON CONFLICT DO UPDATEพฤติกรรม

พิจารณาคู่มือที่นี่ :

สำหรับแต่ละแถวที่เสนอสำหรับการแทรกการแทรกจะดำเนินต่อหรือหากข้อ จำกัด ของผู้ตัดสินหรือดัชนีที่ระบุโดย conflict_targetถูกละเมิดconflict_actionจะถูกนำมาใช้แทน

เหมืองเน้นหนัก ดังนั้นคุณไม่ต้องทำเพรดิเคตซ้ำสำหรับคอลัมน์ที่รวมอยู่ในดัชนีที่ไม่ซ้ำกันในส่วนWHEREคำสั่งUPDATE(( 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'

การละเมิดที่ไม่ซ้ำกันได้กำหนดWHEREข้อที่เพิ่มไว้ของคุณไว้แล้วว่าจะบังคับใช้ซ้ำซ้อน

ชี้แจงดัชนีบางส่วน

เพิ่มส่วนWHEREคำสั่งเพื่อให้เป็นดัชนีบางส่วนที่แท้จริงเช่นเดียวกับที่คุณพูดถึงตัวคุณ (แต่ใช้ตรรกะกลับด้าน):

CREATE UNIQUE INDEX test_upsert_partial_idx
ON public.test_upsert (name, status)
WHERE test_field IS NULL;  -- not: "is not null"

ในการใช้ดัชนีบางส่วนนี้ใน UPSERT ของคุณคุณต้องมีการจับคู่อย่าง @ypercube แสดง :conflict_target

ON CONFLICT (name, status) WHERE test_field IS NULL

ตอนนี้ดัชนีบางส่วนข้างต้นถูกอนุมาน อย่างไรก็ตามในขณะที่คู่มือยังบันทึก :

[... ] ดัชนีที่ไม่ซ้ำกันบางส่วน (ดัชนีที่ไม่ซ้ำโดยไม่มีภาคแสดง) จะถูกอนุมาน (และใช้โดยON CONFLICT) หากดัชนีดังกล่าวเป็นที่น่าพอใจทุกเกณฑ์อื่น ๆ ที่มีอยู่

หากคุณมีดัชนีเพิ่มเติม (หรือเท่านั้น) (name, status)ก็จะใช้ (ก็) ดัชนีใน(name, status, test_field)จะไม่ถูกอนุมานอย่างชัดเจน สิ่งนี้ไม่ได้อธิบายปัญหาของคุณ แต่อาจเพิ่มความสับสนขณะทดสอบ

วิธีการแก้

AIUI, ไม่มีการแก้ปัญหาดังกล่าวข้างต้นของคุณยัง ด้วยดัชนีบางส่วนจะมีเพียงกรณีพิเศษที่จับคู่ค่า NULL เท่านั้น และจะมีการแทรกแถวที่ซ้ำกันอื่น ๆ หากคุณไม่มีดัชนี / ข้อ จำกัด เฉพาะที่ตรงกันอื่น ๆ หรือเพิ่มข้อยกเว้นถ้าคุณทำ ฉันคิดว่านั่นไม่ใช่สิ่งที่คุณต้องการ ที่คุณเขียน:

คีย์ผสมประกอบด้วย 20 คอลัมน์โดย 10 ซึ่งสามารถเป็นโมฆะได้

คุณคิดว่าสิ่งใดที่ซ้ำกันอย่างแน่นอน Postgres (ตามมาตรฐาน SQL) ไม่ถือว่าค่า NULL สองค่าให้เท่ากัน คู่มือ:

โดยทั่วไปข้อ จำกัด ที่ไม่ซ้ำกันจะถูกละเมิดหากมีมากกว่าหนึ่งแถวในตารางที่ค่าของคอลัมน์ทั้งหมดที่รวมอยู่ในข้อ จำกัด มีค่าเท่ากัน อย่างไรก็ตามค่า null สองค่าจะไม่ถือว่าเท่ากันในการเปรียบเทียบนี้ นั่นหมายความว่าแม้ในกรณีที่มีข้อ จำกัด ที่ไม่ซ้ำกันคุณสามารถจัดเก็บแถวที่ซ้ำกันที่มีค่า Null ในคอลัมน์ที่ จำกัด อย่างน้อยหนึ่งคอลัมน์ พฤติกรรมนี้เป็นไปตามมาตรฐาน SQL แต่เราเคยได้ยินว่าฐานข้อมูล SQL อื่นอาจไม่ปฏิบัติตามกฎนี้ ดังนั้นควรระมัดระวังเมื่อพัฒนาแอพพลิเคชั่นที่ต้องการพกพา

ที่เกี่ยวข้อง:

ฉันสมมติว่าคุณต้องการให้NULLค่าในคอลัมน์ที่สามารถ nullable ทั้ง 10 คอลัมน์ถือว่าเท่ากันได้ มันสวยงามและใช้งานได้จริงเพื่อครอบคลุมคอลัมน์ nullable เดียวด้วยดัชนีบางส่วนเพิ่มเติมเช่นที่แสดงที่นี่:

แต่สิ่งนี้อยู่นอกมืออย่างรวดเร็วสำหรับคอลัมน์ที่ไม่มีค่าได้ คุณต้องการดัชนีบางส่วนสำหรับชุดค่าผสมที่แตกต่างกันทุกคอลัมน์ เพียง 2 ของผู้ที่เป็น 3 ดัชนีบางส่วนสำหรับ(a), และ(b) จำนวนที่มีการเติบโตชี้แจงกับ(a,b) 2^n - 1สำหรับคอลัมน์ที่ไม่มีค่าได้ 10 คอลัมน์เพื่อให้ครอบคลุมค่า NULL ที่เป็นไปได้ทั้งหมดคุณต้องมีดัชนีบางส่วนแล้ว 1,023 รายการ ไม่ไป.

วิธีแก้ปัญหาง่าย ๆ : แทนที่ค่า NULL และกำหนดคอลัมน์ที่เกี่ยวข้องNOT NULLและทุกอย่างจะใช้ได้ดีกับUNIQUEข้อ จำกัดง่ายๆ

หากไม่ใช่ตัวเลือกฉันขอแนะนำให้ดัชนีนิพจน์ด้วยCOALESCEเพื่อแทนที่ NULL ในดัชนี:

CREATE UNIQUE INDEX test_upsert_solution_idx
    ON test_upsert (name, status, COALESCE(test_field, ''));

สตริงที่ว่างเปล่า ( '') เป็นผู้สมัครที่ชัดเจนสำหรับรูปแบบตัวอักษร, แต่คุณสามารถใช้ใด ๆทางกฎหมายที่อาจไม่เคยปรากฏหรือสามารถพับเก็บด้วยโมฆะตามที่คุณนิยามของ "ที่ไม่ซ้ำกัน"

จากนั้นใช้คำสั่งนี้:

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

เช่น @ypercube ฉันถือว่าคุณต้องการเพิ่มcountในจำนวนที่มีอยู่ เนื่องจากคอลัมน์สามารถเป็น NULL ได้การเพิ่ม NULL จะเป็นการตั้งค่าคอลัมน์ NULL หากคุณกำหนดcount NOT NULLคุณสามารถทำให้ง่ายขึ้น


ความคิดก็จะเป็นเพียงวางconflict_targetจากงบเพื่อให้ครอบคลุมการละเมิดที่ไม่ซ้ำกันทั้งหมด จากนั้นคุณสามารถกำหนดดัชนีเฉพาะต่าง ๆ สำหรับคำจำกัดความที่ซับซ้อนยิ่งขึ้นของสิ่งที่ควรจะเป็น "ไม่ซ้ำกัน" ON CONFLICT DO UPDATEแต่ที่จะไม่บินกับ คู่มืออีกครั้ง:

สำหรับON CONFLICT DO NOTHING, มันเป็นทางเลือกที่จะระบุความขัดแย้ง _target; เมื่อละเว้นจะขัดแย้งกับข้อ จำกัด ที่ใช้งานได้ทั้งหมด (และดัชนีที่ไม่ซ้ำกัน) ได้รับการจัดการ สำหรับต้องON CONFLICT DO UPDATEมีการขัดแย้งขัดแย้งกับเป้าหมาย


1
ดี ฉันข้ามคอลัมน์ 20-10 ส่วนในครั้งแรกที่ฉันอ่านคำถามและไม่มีเวลาให้เสร็จในภายหลัง count = CASE WHEN EXCLUDED.count IS NULL THEN tu.count ELSE COALESCE(tu.count, 0) + COALESCE(EXCLUDED.count, 0) ENDได้ง่ายไปcount = COALESCE(tu.count+EXCLUDED.count, EXCLUDED.count, tu.count)
ypercubeᵀᴹ

มองอีกครั้งเวอร์ชัน "เรียบง่าย" ของฉันไม่ได้เป็นเอกสารเอง
ypercubeᵀᴹ

@ ypercubeᵀᴹ: ฉันใช้การอัปเดตที่คุณแนะนำ มันง่ายกว่านี้ขอบคุณ
Erwin Brandstetter

@ErwinBrandstetter คุณเก่งที่สุด
Seamus Abshere

7

ฉันคิดว่าปัญหาคือคุณไม่มีดัชนีบางส่วนและON CONFLICTไวยากรณ์ไม่ตรงกับtest_upsert_upsert_id_idxดัชนี แต่เป็นข้อ จำกัด ที่ไม่ซ้ำกันอื่น ๆ

หากคุณกำหนดดัชนีเป็นบางส่วน (พร้อม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 ;

และแถวเหล่านี้อยู่ในตารางแล้ว:

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

ดังนั้นแบบสอบถามจะประสบความสำเร็จ:

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

ด้วยผลลัพธ์ต่อไปนี้:

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

สิ่งนี้จะอธิบายวิธีการใช้ดัชนีบางส่วน แต่ฉันคิดว่ามันยังไม่สามารถแก้ปัญหาได้
Erwin Brandstetter

การนับสำหรับ 'มาเรีย' ไม่ควรอยู่ที่ 1 เนื่องจากไม่มีการอัปเดตเกิดขึ้น?
mpprdev

@ mpprdev ใช่คุณพูดถูก
ypercubeᵀᴹ
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.