คำตอบที่ได้รับการยอมรับในขณะนี้ดูเหมือนว่า ok สำหรับเป้าหมายความขัดแย้งเดียวความขัดแย้งไม่กี่ tuples ขนาดเล็กและไม่มีการเรียก มันหลีกเลี่ยงปัญหาการเกิดพร้อมกัน 1 (ดูด้านล่าง) ด้วยกำลังดุร้าย ทางออกที่ง่ายมีการอุทธรณ์ผลข้างเคียงอาจมีความสำคัญน้อยกว่า
สำหรับกรณีอื่น ๆ แม้ว่าจะไม่ได้อัปเดตแถวโดยไม่จำเป็นต้องเหมือนกัน แม้ว่าคุณจะไม่เห็นความแตกต่างบนพื้นผิว แต่ก็มีผลข้างเคียงที่หลากหลาย :
อาจเป็นสาเหตุของไฟที่ไม่ควรถูกยิง
มันเขียนล็อคแถว "ผู้บริสุทธิ์" ซึ่งอาจมีค่าใช้จ่ายที่เกิดขึ้นสำหรับการทำธุรกรรมพร้อมกัน
อาจทำให้แถวดูเหมือนใหม่แม้ว่าจะเก่าแล้ว (การประทับเวลาของธุรกรรม)
สิ่งสำคัญที่สุดคือด้วยโมเดล MVCC ของ PostgreSQL รุ่นแถวใหม่จะถูกเขียนขึ้นสำหรับทุก ๆ คนUPDATE
ไม่ว่าข้อมูลแถวจะเปลี่ยนไปก็ตาม สิ่งนี้ทำให้เกิดการลงโทษประสิทธิภาพสำหรับ UPSERT เอง, ตารางขยาย, ดัชนีขยายตัว, ค่าปรับประสิทธิภาพสำหรับการดำเนินการตามตารางVACUUM
ราคา ผลเล็กน้อยสำหรับการทำซ้ำเล็กน้อย แต่มีขนาดใหญ่สำหรับการดักจับส่วนใหญ่
พลัสON CONFLICT DO UPDATE
บางครั้งมันไม่จริงหรือแม้กระทั่งไปได้ที่จะใช้ คู่มือ:
สำหรับON CONFLICT DO UPDATE
ที่conflict_target
จะต้องให้
เดียว "เป้าหมายของความขัดแย้ง" เป็นไปไม่ได้ถ้าดัชนีหลาย / ข้อ จำกัด ที่มีส่วนเกี่ยวข้อง
คุณสามารถทำได้ (เกือบ) เหมือนกันโดยไม่มีการอัพเดตว่างเปล่าและผลข้างเคียง บางส่วนของการแก้ปัญหาต่อไปนี้ยังทำงานร่วมกับON CONFLICT DO NOTHING
(ไม่ "เป้าหมายของความขัดแย้ง") ที่จะจับทุกความขัดแย้งที่เป็นไปได้ที่อาจเกิดขึ้น - ซึ่งอาจจะหรืออาจจะไม่เป็นที่น่าพอใจ
ไม่มีโหลดการเขียนพร้อมกัน
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
source
คอลัมน์คือนอกจากตัวเลือกที่จะแสดงให้เห็นถึงวิธีการทำงานนี้ คุณอาจจำเป็นต้องใช้เพื่อบอกความแตกต่างระหว่างทั้งสองกรณี (ประโยชน์อื่นจากการเขียนที่ว่างเปล่า)
การJOIN chats
ทำงานขั้นสุดท้ายเพราะแทรกแถวใหม่จากCTE การแก้ไขข้อมูลที่แนบมายังไม่สามารถมองเห็นได้ในตารางต้นแบบ (ทุกส่วนของคำสั่ง SQL เดียวกันดูสแน็ปช็อตเดียวกันของตารางต้นแบบ)
เนื่องจากการVALUES
แสดงออกเป็นแบบอิสระ (ไม่ได้แนบโดยตรงกับINSERT
) Postgres จึงไม่สามารถรับชนิดข้อมูลจากคอลัมน์เป้าหมายและคุณอาจต้องเพิ่มประเภทการส่งแบบชัดแจ้ง คู่มือ:
เมื่อVALUES
มีการใช้งานINSERT
ค่าต่างๆจะถูกรวมเข้ากับประเภทข้อมูลของคอลัมน์ปลายทางที่เกี่ยวข้องโดยอัตโนมัติ เมื่อมันถูกใช้ในบริบทอื่น ๆ มันอาจจำเป็นต้องระบุชนิดข้อมูลที่ถูกต้อง หากรายการเป็นค่าคงที่ตัวอักษรที่ยกมาทั้งหมดการข่มขู่ครั้งแรกก็เพียงพอที่จะกำหนดประเภทสันนิษฐานสำหรับทั้งหมด
แบบสอบถามเอง (ไม่นับผลข้างเคียง) อาจมีราคาแพงกว่าเล็กน้อยสำหรับdupe สองสามอันเนื่องจากค่าโสหุ้ยของ CTE และเพิ่มเติมSELECT
(ซึ่งควรมีราคาถูกเนื่องจากดัชนีที่สมบูรณ์แบบจะมีตามคำจำกัดความ - ข้อ จำกัด เฉพาะถูกนำมาใช้กับ ดัชนี)
อาจเร็วขึ้นสำหรับการทำซ้ำหลายรายการ ค่าใช้จ่ายที่มีประสิทธิภาพของการเขียนเพิ่มเติมขึ้นอยู่กับหลายปัจจัย
แต่มีผลข้างเคียงน้อยลงและมีค่าใช้จ่ายแอบแฝงในทุกกรณี มันอาจถูกกว่ามาก
ลำดับที่แนบมานั้นยังคงขั้นสูงเนื่องจากค่าเริ่มต้นจะถูกกรอกก่อนที่จะทดสอบความขัดแย้ง
เกี่ยวกับ CTEs:
พร้อมโหลดการเขียนพร้อมกัน
สมมติว่าเริ่มต้นแยกรายการREAD COMMITTED
ที่เกี่ยวข้อง:
กลยุทธ์ที่ดีที่สุดในการป้องกันสภาพการแข่งขันขึ้นอยู่กับข้อกำหนดที่แน่นอนจำนวนและขนาดของแถวในตารางและใน UPSERT จำนวนธุรกรรมที่เกิดขึ้นพร้อมกันโอกาสในการเกิดความขัดแย้งทรัพยากรที่มีอยู่และปัจจัยอื่น ๆ ...
ปัญหาการเกิดพร้อมกัน 1
หากธุรกรรมที่เกิดขึ้นพร้อมกันได้เขียนลงในแถวที่ธุรกรรมของคุณพยายามที่จะ UPSERT ตอนนี้การทำธุรกรรมของคุณจะต้องรอให้อีกรายการหนึ่งเสร็จสิ้น
หากธุรกรรมอื่น ๆ ลงท้ายด้วยROLLBACK
(หรือข้อผิดพลาดใด ๆ เช่นอัตโนมัติROLLBACK
) ธุรกรรมของคุณสามารถดำเนินการตามปกติ ผลข้างเคียงที่เป็นไปได้เล็กน้อย: ช่องว่างในตัวเลขตามลำดับ แต่ไม่มีแถวที่ขาดหายไป
หากธุรกรรมอื่นสิ้นสุดลงตามปกติ (โดยนัยหรือชัดเจนCOMMIT
) คุณINSERT
จะตรวจพบข้อขัดแย้ง ( UNIQUE
ดัชนี / ข้อ จำกัด นั้นแน่นอน) และDO NOTHING
ดังนั้นจะไม่ส่งคืนแถว (และไม่สามารถล็อกแถวดังที่แสดงในปัญหาการทำงานพร้อมกันที่ 2ด้านล่างเนื่องจากไม่สามารถมองเห็นได้ ) การSELECT
ดูสแนปชอตเดียวกันจากจุดเริ่มต้นของแบบสอบถามและยังไม่สามารถส่งคืนแถวที่มองไม่เห็นได้
แถวใดหายไปจากชุดผลลัพธ์ (แม้ว่าจะมีอยู่ในตารางที่อยู่ข้างใต้)!
นี้อาจจะตกลงเป็น โดยเฉพาะอย่างยิ่งถ้าคุณไม่คืนแถวเหมือนในตัวอย่างและพอใจที่รู้ว่าแถวนั้นอยู่ที่นั่น หากยังไม่ดีพอมีหลายวิธีที่อยู่รอบ ๆ
คุณสามารถตรวจสอบจำนวนแถวของเอาต์พุตและทำซ้ำคำสั่งได้หากไม่ตรงกับจำนวนแถวของอินพุต อาจจะดีพอสำหรับกรณีที่หายาก จุดคือการเริ่มแบบสอบถามใหม่ (สามารถอยู่ในการทำธุรกรรมเดียวกัน) ซึ่งจะเห็นแถวมุ่งมั่นใหม่
หรือตรวจสอบการขาดหายไปแถวผลภายในแบบสอบถามเดียวกันและเขียนทับผู้ที่มีเคล็ดลับแรงเดรัจฉานแสดงให้เห็นในคำตอบของ 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;
มันเหมือนกับแบบสอบถามด้านบน แต่เราเพิ่มอีกหนึ่งขั้นตอนกับ CTE ups
ก่อนที่เราจะส่งคืนชุดผลลัพธ์ที่สมบูรณ์ CTE สุดท้ายนั้นจะไม่ทำอะไรส่วนใหญ่ เฉพาะในกรณีที่แถวหายไปจากผลตอบแทนที่เราใช้แรงเดรัจฉาน
ค่าใช้จ่ายเพิ่มเติมยัง ยิ่งความขัดแย้งกับแถวที่มีอยู่แล้วมีความเป็นไปได้สูงที่วิธีนี้จะมีประสิทธิภาพมากกว่าวิธีง่าย ๆ
ผลข้างเคียงหนึ่ง: UPSERT ตัวที่ 2 เขียนแถวที่ไม่เป็นระเบียบดังนั้นจึงแนะนำความเป็นไปได้ของการหยุดชะงัก (ดูด้านล่าง) หากธุรกรรมสามรายการขึ้นไปเขียนทับแถวเดียวกัน หากเป็นปัญหาคุณต้องมีโซลูชันอื่น - เช่นการทำซ้ำคำสั่งทั้งหมดดังกล่าวข้างต้น
ปัญหาการเกิดพร้อมกัน 2
หากการทำธุรกรรมที่เกิดขึ้นพร้อมกันสามารถเขียนคอลัมน์มีส่วนร่วมของแถวได้รับผลกระทบและคุณต้องให้แน่ใจว่าแถวที่คุณพบว่ายังคงมีในขั้นตอนต่อไปในรายการเดียวกันคุณสามารถล็อคแถวที่มีราคาถูกใน CTE ins
(ซึ่งจะไปปลดล็อค) ด้วย:
...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE -- never executed, but still locks the row
...
และเพิ่มข้อล็อคไปSELECT
FOR UPDATE
ด้วยเช่น
สิ่งนี้ทำให้การดำเนินการเขียนแบบแข่งขันรอจนถึงจุดสิ้นสุดของธุรกรรมเมื่อการล็อกทั้งหมดได้รับการปล่อยตัว ดังนั้นจะสั้น
รายละเอียดเพิ่มเติมและคำอธิบาย:
deadlocks?
ป้องกันการติดตายโดยการแทรกแถวในการสั่งซื้อที่สอดคล้องกัน ดู:
ชนิดข้อมูลและการปลดเปลื้อง
ตารางที่มีอยู่เป็นเทมเพลตสำหรับชนิดข้อมูล ...
ประเภทที่ชัดเจนได้ปลดเปลื้องสำหรับแถวแรกของข้อมูลในVALUES
นิพจน์ยืนฟรีอาจไม่สะดวก มีวิธีการอยู่รอบ ๆ คุณสามารถใช้ความสัมพันธ์ใด ๆ ที่มีอยู่ (ตารางมุมมอง ... ) เป็นแม่แบบแถว ตารางเป้าหมายเป็นตัวเลือกที่ชัดเจนสำหรับกรณีการใช้งาน ข้อมูลอินพุตถูกบีบบังคับให้เป็นชนิดที่เหมาะสมโดยอัตโนมัติเช่นในVALUES
ข้อของ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')
)
...
สิ่งนี้ใช้ไม่ได้กับข้อมูลบางประเภท ดู:
... และชื่อ
ใช้ได้กับทุกประเภทข้อมูล
ขณะแทรกลงในคอลัมน์ (นำหน้า) ทั้งหมดของตารางคุณสามารถละเว้นชื่อคอลัมน์ สมมติว่าตารางchats
ในตัวอย่างประกอบด้วย 3 คอลัมน์ที่ใช้ใน 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
)
...
นอกเหนือ: อย่าใช้คำสงวนเช่นเดียว"user"
กับตัวระบุ นั่นคือปืนพกที่โหลด ใช้ตัวระบุที่ถูกต้องตัวพิมพ์เล็กและตัวใหญ่ usr
ฉันแทนที่มันด้วย
ON CONFLICT UPDATE
เพื่อให้มีการเปลี่ยนแปลงในแถว จากนั้นRETURNING
จะจับภาพ