วิธี UPSERT (รวมแทรก ... ในการอัปเดตซ้ำซ้อน) ใน PostgreSQL


267

คำถามที่ถามบ่อยมากที่นี่คือวิธีการเพิ่มความโกรธซึ่งเป็นสิ่งที่ MySQL เรียกINSERT ... ON DUPLICATE UPDATEและมาตรฐานรองรับเป็นส่วนหนึ่งของการMERGEดำเนินการ

เนื่องจาก PostgreSQL ไม่สนับสนุนโดยตรง (ก่อนหน้า 9.5) คุณจะทำอย่างไร พิจารณาสิ่งต่อไปนี้:

CREATE TABLE testtable (
    id integer PRIMARY KEY,
    somedata text NOT NULL
);

INSERT INTO testtable (id, somedata) VALUES
(1, 'fred'),
(2, 'bob');

ตอนนี้คิดว่าคุณต้องการที่จะ "upsert" tuples (2, 'Joe'), (3, 'Alan')ดังนั้นเนื้อหาตารางใหม่จะเป็น:

(1, 'fred'),
(2, 'Joe'),    -- Changed value of existing tuple
(3, 'Alan')    -- Added new tuple

นั่นคือสิ่งที่คนกำลังพูดถึงเมื่อพูดถึง upsertวิธีการใด ๆ ที่จะต้องมีความปลอดภัยในการปรากฏตัวของการทำธุรกรรมหลายรายการที่ทำงานบนตารางเดียวกัน - โดยใช้การล็อคอย่างชัดเจน

หัวข้อนี้มีการกล่าวถึงอย่างกว้างขวางที่ส่วนแทรกในการอัพเดตซ้ำใน PostgreSQL? แต่นั่นเป็นเรื่องของทางเลือกในไวยากรณ์ของ MySQL และมันก็เพิ่มรายละเอียดที่ไม่เกี่ยวข้องในช่วงเวลาหนึ่ง ฉันกำลังทำงานกับคำตอบที่ชัดเจน

เทคนิคเหล่านี้ยังมีประโยชน์สำหรับ "แทรกถ้าไม่มีอยู่, ไม่ทำอะไรเลย" เช่น "แทรก ... ในการเพิกเฉยกับคีย์ซ้ำ"


1
เป็นไปได้ที่ซ้ำกันของการแทรกในการอัพเดทซ้ำใน PostgreSQL?
Michael Hampton

8
@MichaelHampton เป้าหมายที่นี่คือการสร้างเวอร์ชันที่ชัดเจนซึ่งไม่สับสนกับคำตอบที่ล้าสมัยหลายครั้ง - และถูกล็อกดังนั้นจึงไม่มีใครสามารถทำอะไรกับมันได้ ฉันไม่เห็นด้วยกับเพื่อนสนิท
Craig Ringer

ทำไมสิ่งนี้จะล้าสมัยในไม่ช้า - และถูกล็อกดังนั้นจึงไม่มีใครทำอะไรได้เลย
Michael Hampton

2
@MichaelHampton หากคุณเป็นห่วงบางทีคุณอาจตั้งค่าสถานะที่คุณเชื่อมโยงและขอให้ปลดล็อคเพื่อให้สามารถล้างออกได้จากนั้นเราสามารถรวมสิ่งต่อไปนี้เข้าด้วยกันฉันแค่เบื่อที่จะมีคนใกล้ชิดเท่านั้น - เช่นเดียวกับการเพิ่มความสับสนและสับสน
Craig Ringer

1
คำถาม & คำตอบนั้นไม่ได้ล็อค!
Michael Hampton

คำตอบ:


396

9.5 และใหม่กว่า:

PostgreSQL 9.5 และการสนับสนุนที่ใหม่กว่าINSERT ... ON CONFLICT UPDATE(และON CONFLICT DO NOTHING) เช่นสุดยอด

ON DUPLICATE KEY UPDATEเมื่อเทียบกับ

คำอธิบายด่วนคำอธิบายอย่างรวดเร็ว

สำหรับการใช้งานดูคู่มือ - เฉพาะconflict_actionประโยคในแผนภาพไวยากรณ์และข้อความอธิบาย

ซึ่งแตกต่างจากโซลูชั่นสำหรับ 9.4 และรุ่นเก่ากว่าที่ได้รับด้านล่างคุณสมบัตินี้ทำงานร่วมกับหลายแถวที่ขัดแย้งกันและไม่จำเป็นต้องล็อคแบบเอกสิทธิ์เฉพาะบุคคลหรือวนซ้ำ

กระทำการเพิ่มคุณลักษณะอยู่ที่นี่และอภิปรายรอบการพัฒนาที่อยู่ที่นี่


หากคุณอายุ 9.5 ปีและไม่จำเป็นต้องรองรับการย้อนกลับคุณสามารถหยุดอ่านได้ทันที


9.4 และมากกว่า:

PostgreSQL ไม่มีสิ่งอำนวยความสะดวกในตัวUPSERT(หรือMERGE) และการทำอย่างมีประสิทธิภาพเมื่อเผชิญกับการใช้งานพร้อมกันนั้นเป็นเรื่องยากมาก

บทความนี้กล่าวถึงปัญหาในรายละเอียดที่เป็นประโยชน์บทความนี้กล่าวถึงปัญหาในรายละเอียดที่เป็นประโยชน์

โดยทั่วไปคุณต้องเลือกระหว่างสองตัวเลือก:

  • การดำเนินการแทรก / อัพเดตส่วนบุคคลในการลองส่งซ้ำ หรือ
  • ล็อคตารางและทำการผสานแบทช์

แต่ละแถวลองซ้ำแถว

การใช้แต่ละแถว upserts ในการวนซ้ำเป็นตัวเลือกที่เหมาะสมถ้าคุณต้องการการเชื่อมต่อจำนวนมากพร้อมกันพยายามที่จะทำการแทรก

เอกสาร PostgreSQL มีขั้นตอนที่มีประโยชน์ที่จะช่วยให้คุณสามารถทำเช่นนี้ในวงภายในฐานข้อมูล มันป้องกันการอัพเดทที่สูญหายและแทรกการแข่งขัน มันจะทำงานในREAD COMMITTEDโหมดและจะปลอดภัยก็ต่อเมื่อมันเป็นสิ่งเดียวที่คุณทำในการทำธุรกรรม ฟังก์ชั่นจะทำงานไม่ถูกต้องหากทริกเกอร์หรือคีย์ที่ไม่ซ้ำรองทำให้เกิดการละเมิดที่ไม่ซ้ำกัน

กลยุทธ์นี้ไม่มีประสิทธิภาพมาก เมื่อใดก็ตามที่ปฏิบัติได้จริงคุณควรเข้าคิวและทำงานเป็นกลุ่มให้มากขึ้นดังที่อธิบายไว้ด้านล่างแทน

วิธีแก้ไขปัญหาที่พยายามทำหลายอย่างล้มเหลวในการพิจารณาการย้อนกลับดังนั้นจึงส่งผลให้มีการปรับปรุงที่ไม่สมบูรณ์ ธุรกรรมสองรายการแข่งขันกันเอง หนึ่งในนั้นประสบความสำเร็จINSERTs; อีกอันหนึ่งได้รับข้อผิดพลาดของคีย์ที่ซ้ำกันและทำUPDATEแทน UPDATEบล็อกรอINSERTที่จะย้อนกลับหรือกระทำ เมื่อย้อนกลับUPDATEเงื่อนไขการตรวจสอบอีกครั้งจะจับคู่กับศูนย์แถวดังนั้นแม้ว่าการUPDATEกระทำดังกล่าวจะไม่ได้เพิ่มยอดขายตามที่คุณคาดหวัง คุณต้องตรวจสอบจำนวนแถวผลลัพธ์และลองอีกครั้งตามที่จำเป็น

โซลูชันที่พยายามทำบางอย่างยังไม่สามารถพิจารณาเชื้อชาติที่เลือกได้ หากคุณลองชัดเจนและเรียบง่าย:

-- THIS IS WRONG. DO NOT COPY IT. It's an EXAMPLE.

BEGIN;

UPDATE testtable
SET somedata = 'blah'
WHERE id = 2;

-- Remember, this is WRONG. Do NOT COPY IT.

INSERT INTO testtable (id, somedata)
SELECT 2, 'blah'
WHERE NOT EXISTS (SELECT 1 FROM testtable WHERE testtable.id = 2);

COMMIT;

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

นี่คือเหตุผลที่คุณต้องลองวนซ้ำ คุณอาจคิดว่าคุณสามารถป้องกันข้อผิดพลาดที่สำคัญที่ซ้ำกันหรือการปรับปรุงที่สูญหายด้วย SQL ที่ฉลาด แต่คุณทำไม่ได้ คุณต้องตรวจสอบจำนวนแถวหรือจัดการข้อผิดพลาดที่สำคัญซ้ำ (ขึ้นอยู่กับวิธีที่เลือก) และลองอีกครั้ง

โปรดอย่าม้วนโซลูชันของคุณเองสำหรับสิ่งนี้ เช่นเดียวกับการรอคิวข้อความอาจผิดปกติ

จำนวนมากขึ้นพร้อมล็อค

บางครั้งคุณต้องการเพิ่มจำนวนมากขึ้นซึ่งคุณมีชุดข้อมูลใหม่ที่คุณต้องการผสานเข้ากับชุดข้อมูลที่มีอยู่เดิม สิ่งนี้มีประสิทธิภาพมากกว่าการตั้งแถวเดี่ยวอย่างมากและควรได้รับการพิจารณาเมื่อใช้งานได้จริง

ในกรณีนี้คุณมักจะทำตามขั้นตอนต่อไปนี้:

  • CREATETEMPORARYตาราง

  • COPY หรือแทรกข้อมูลใหม่จำนวนมากลงในตารางชั่วคราว

  • LOCKIN EXCLUSIVE MODEตารางเป้าหมาย สิ่งนี้อนุญาตให้ทำธุรกรรมอื่น ๆSELECTแต่ไม่ทำการเปลี่ยนแปลงใด ๆ ในตาราง

  • ทำUPDATE ... FROMระเบียนที่มีอยู่โดยใช้ค่าในตาราง temp

  • ทำINSERTแถวที่ไม่มีอยู่ในตารางเป้าหมาย

  • COMMITปลดล็อค

ตัวอย่างเช่นสำหรับตัวอย่างที่ให้ไว้ในคำถามการใช้หลายค่าINSERTเพื่อเติมข้อมูลตารางชั่วคราว:

BEGIN;

CREATE TEMPORARY TABLE newvals(id integer, somedata text);

INSERT INTO newvals(id, somedata) VALUES (2, 'Joe'), (3, 'Alan');

LOCK TABLE testtable IN EXCLUSIVE MODE;

UPDATE testtable
SET somedata = newvals.somedata
FROM newvals
WHERE newvals.id = testtable.id;

INSERT INTO testtable
SELECT newvals.id, newvals.somedata
FROM newvals
LEFT OUTER JOIN testtable ON (testtable.id = newvals.id)
WHERE testtable.id IS NULL;

COMMIT;

การอ่านที่เกี่ยวข้อง

เกี่ยวกับMERGEอะไร

MERGEจริง ๆ แล้วมาตรฐาน SQL มีซีแมนทิกส์กำหนดไว้ไม่ดีและไม่เหมาะสำหรับการ upserting โดยไม่ต้องล็อคตารางก่อน

มันเป็นคำสั่ง OLAP ที่มีประโยชน์จริง ๆ สำหรับการรวมข้อมูล แต่จริงๆแล้วไม่ใช่โซลูชันที่มีประโยชน์สำหรับการใช้งานพร้อมกันที่ปลอดภัย มีคำแนะนำมากมายสำหรับผู้ที่ใช้ DBMS อื่น ๆ เพื่อใช้MERGEสำหรับ upserts แต่จริงๆแล้วมันผิด

ฐานข้อมูลอื่น ๆ :


ในการเพิ่มจำนวนมากจะมีค่าที่เป็นไปได้ในการลบจากช่วงเวลาใหม่มากกว่าการกรอง INSERT หรือไม่ เช่นกับ upd AS (อัพเดท ... new NEW. การกลับมาใหม่) ลบออกจากช่วงเวลาใหม่โดยใช้ upd ที่ newvals.id = upd.id แล้วตามด้วย INSERT INTO เปลือยเลือกตารางทดสอบ * จากช่วงใหม่? ความคิดของฉันเกี่ยวกับสิ่งนี้: แทนที่จะกรองสองครั้งใน INSERT (สำหรับ JOIN / WHERE และสำหรับข้อ จำกัด ที่ไม่ซ้ำกัน) ให้นำผลการตรวจสอบการดำรงอยู่มาใช้ใหม่จาก UPDATE ซึ่งอยู่ใน RAM แล้วและอาจมีขนาดเล็กกว่ามาก นี่อาจเป็นเรื่องชนะถ้าสองสามแถวที่จับคู่กันและ / หรือช่วงเวลาใหม่มีขนาดเล็กกว่าโต๊ะทดสอบมาก
Gunnlaugur Briem

1
ยังมีปัญหาที่ไม่ได้รับการแก้ไขและสำหรับผู้ค้ารายอื่น ๆ มันไม่ชัดเจนว่าอะไรทำงานและอะไรไม่ 1. โซลูชันวนลูป Postgres ตามที่ระบุไว้ไม่ทำงานในกรณีของคีย์ที่ไม่ซ้ำหลาย ๆ อัน 2. คีย์ซ้ำซ้อนสำหรับ mysql นั้นไม่สามารถใช้งานได้กับคีย์เฉพาะหลาย ๆ อัน 3. โซลูชันอื่น ๆ สำหรับ MySQL, SQL Server และ Oracle ที่โพสต์ไว้ด้านบนใช้งานได้หรือไม่ มีข้อยกเว้นที่เป็นไปได้ในกรณีเหล่านั้นและเราจำเป็นต้องวนซ้ำหรือไม่
แดนข

@danb นี่เป็นเพียงเกี่ยวกับ PostgreSQL เท่านั้น ไม่มีโซลูชันข้ามผู้ขาย โซลูชันสำหรับ PostgreSQL ไม่ทำงานสำหรับหลายแถวคุณต้องทำธุรกรรมหนึ่งรายการต่อแถวอย่างน่าเสียดาย "วิธีแก้ปัญหา" ที่ใช้MERGEสำหรับ SQL Server และ Oracle นั้นไม่ถูกต้องและมีแนวโน้มที่จะแย่งกันตามที่ระบุไว้ข้างต้น คุณจะต้องตรวจสอบแต่ละ DBMS โดยเฉพาะเพื่อหาวิธีจัดการกับมันฉันสามารถให้คำแนะนำกับ PostgreSQL ได้เท่านั้น วิธีเดียวที่จะทำ upsert แบบหลายแถวที่ปลอดภัยบน PostgreSQL จะเป็นการเพิ่มการสนับสนุน upsert แบบดั้งเดิมในเซิร์ฟเวอร์หลัก
Craig Ringer

แม้แต่ PostGresQL โซลูชันไม่ทำงานในกรณีที่ตารางมีคีย์ที่ไม่ซ้ำกันหลายรายการ (อัปเดตเพียงหนึ่งแถว) ในกรณีนั้นคุณต้องระบุว่ามีการอัปเดตคีย์ใด อาจมีโซลูชันข้ามผู้ขายโดยใช้ jdbc เช่น
b

2
ตอนนี้ Postgres รองรับ UPSERT แล้ว - git.postgresql.org/gitweb/ …
Chris

32

ฉันกำลังพยายามมีส่วนร่วมกับวิธีแก้ไขปัญหาอื่นสำหรับปัญหาการแทรกครั้งเดียวกับ PostgreSQL รุ่น pre-9.5 แนวคิดนี้เป็นเพียงการพยายามแทรกครั้งแรกและในกรณีที่มีบันทึกอยู่แล้วให้อัปเดต:

do $$
begin 
  insert into testtable(id, somedata) values(2,'Joe');
exception when unique_violation then
  update testtable set somedata = 'Joe' where id = 2;
end $$;

โปรดทราบว่าการแก้ปัญหานี้สามารถนำมาใช้เฉพาะในกรณีที่มีการลบแถวของตารางไม่มี

ฉันไม่รู้เกี่ยวกับประสิทธิภาพของโซลูชันนี้ แต่ดูเหมือนว่าสมเหตุสมผลพอ


3
ขอบคุณนั่นคือสิ่งที่ฉันกำลังมองหา ไม่เข้าใจว่าทำไมมันหายากมาก
isapir

4
อ๋อ การทำให้เข้าใจง่ายนี้ใช้ได้ถ้าหากไม่มีการลบเท่านั้น
Craig Ringer

@CraigRinger คุณช่วยอธิบายว่าจะเกิดอะไรขึ้นถ้ามีการลบ?
turbanoff

@turbanoff การแทรกอาจล้มเหลวเนื่องจากบันทึกมีอยู่แล้วจากนั้นจะถูกลบไปพร้อม ๆ กันและการอัปเดตจะมีผลต่อศูนย์แถวเนื่องจากแถวถูกลบ
Craig Ringer

@ CraigRinger ดังนั้น การลบคือเกิดขึ้นพร้อมกัน สิ่งที่เป็นไปได้ถ้า outways นี้เป็นผลงานที่ดี? หากการลบทำงานพร้อมกัน - สามารถดำเนินการได้หลังจากบล็อกของเรา สิ่งที่ฉันพยายามที่จะพูด - ถ้าเรามีการลบพร้อมกัน - แล้วนี้ woks รหัสในเดียวกันอย่างที่เหมาะสมinsert on update
turbanoff

28

นี่คือตัวอย่างสำหรับinsert ... on conflict ...( pg 9.5+ ):

  • แทรก, ขัดแย้ง - ไม่ทำอะไรเลย
    insert into dummy(id, name, size) values(1, 'new_name', 3)
    on conflict do nothing;`  
  • แทรกในความขัดแย้ง - ทำปรับปรุงระบุเป้าหมายความขัดแย้งผ่านทางคอลัมน์
    insert into dummy(id, name, size) values(1, 'new_name', 3)
    on conflict(id)
    do update set name = 'new_name', size = 3;  
  • แทรกในความขัดแย้ง - ทำปรับปรุงระบุเป้าหมายความขัดแย้งผ่านชื่อ จำกัด
    insert into dummy(id, name, size) values(1, 'new_name', 3)
    on conflict on constraint dummy_pkey
    do update set name = 'new_name', size = 4;

คำตอบที่ดี - คำถาม: ทำไมหรือในสถานการณ์ใดควรใช้ข้อมูลจำเพาะเป้าหมายผ่านคอลัมน์หรือชื่อข้อ จำกัด มีข้อดี / ข้อเสียสำหรับกรณีการใช้งานที่หลากหลายหรือไม่?
Nathan Benton

1
@NathanBenton ฉันคิดว่ามีความแตกต่างอย่างน้อย 2 รายการ: (1) ชื่อคอลัมน์ถูกระบุโดยโปรแกรมเมอร์ขณะที่ชื่อข้อ จำกัด อาจถูกระบุโดยโปรแกรมเมอร์หรือสร้างขึ้นโดยฐานข้อมูลตามชื่อตาราง / คอลัมน์ (2) แต่ละคอลัมน์อาจมีข้อ จำกัด หลายประการ ที่กล่าวว่าขึ้นอยู่กับกรณีของคุณในการเลือกตัวเลือกที่จะใช้
Eric Wang

8

SQLAlchemy เพิ่มขึ้นสำหรับ Postgres> = 9.5

เนื่องจากโพสต์ขนาดใหญ่ข้างต้นครอบคลุมวิธีการ SQL ที่แตกต่างกันมากมายสำหรับรุ่น Postgres (ไม่ใช่เฉพาะที่ไม่ใช่ 9.5 ตามคำถาม) ฉันต้องการเพิ่มวิธีการทำใน SQLAlchemy หากคุณใช้ Postgres 9.5 แทนที่จะใช้งาน upsert ของคุณเองคุณสามารถใช้ฟังก์ชันของ SQLAlchemy ได้ (ซึ่งถูกเพิ่มใน SQLAlchemy 1.1) ส่วนตัวผมขอแนะนำให้ใช้สิ่งเหล่านี้ถ้าเป็นไปได้ ไม่เพียงเพราะความสะดวกเท่านั้น แต่ยังช่วยให้ PostgreSQL จัดการกับสภาพการแข่งขันที่อาจเกิดขึ้นได้

ข้ามการโพสต์จากคำตอบอื่นที่ฉันให้เมื่อวานนี้ ( https://stackoverflow.com/a/44395983/2156909 )

SQLAlchemy รองรับON CONFLICTสองวิธีon_conflict_do_update()และon_conflict_do_nothing():

คัดลอกจากเอกสาร:

from sqlalchemy.dialects.postgresql import insert

stmt = insert(my_table).values(user_email='a@b.com', data='inserted data')
stmt = stmt.on_conflict_do_update(
    index_elements=[my_table.c.user_email],
    index_where=my_table.c.user_email.like('%@gmail.com'),
    set_=dict(data=stmt.excluded.data)
    )
conn.execute(stmt)

http://docs.sqlalchemy.org/en/latest/dialects/postgresql.html?highlight=conflict#insert-on-conflict-upsert


4
Python และ SQLAlchemy ไม่ได้กล่าวถึงในคำถาม
Alexander Emelianov

ฉันมักจะใช้ Python ในการแก้ปัญหาที่ฉันเขียน แต่ฉันไม่ได้ดู SQLAlchemy (หรือรู้ตัว) ดูเหมือนว่าตัวเลือกที่สง่างาม ขอบคุณ. ถ้าเช็คเอาท์ฉันจะนำเสนอสิ่งนี้กับองค์กรของฉัน
Robert

3
WITH UPD AS (UPDATE TEST_TABLE SET SOME_DATA = 'Joe' WHERE ID = 2 
RETURNING ID),
INS AS (SELECT '2', 'Joe' WHERE NOT EXISTS (SELECT * FROM UPD))
INSERT INTO TEST_TABLE(ID, SOME_DATA) SELECT * FROM INS

ทดสอบกับ Postgresql 9.3


@CraigRinger: คุณช่วยอธิบายเรื่องนี้ได้ไหม? อะตอม cte ไม่ใช่หรือ
parisni

2
@parisni ไม่แต่ละคำ CTE จะได้รับสแนปชอตของตัวเองหากดำเนินการเขียน นอกจากนี้ยังไม่มีการล็อกเพรดิเคตที่ดำเนินการในแถวที่ไม่พบดังนั้นพวกเขาจึงยังสามารถสร้างขึ้นได้พร้อมกันในเซสชันอื่น หากคุณใช้SERIALIZABLEการแยกคุณจะได้รับการยกเลิกด้วยความล้มเหลวของการทำให้เป็นอนุกรมมิฉะนั้นคุณอาจได้รับการละเมิดที่ไม่ซ้ำกัน อย่าสร้างความปั่นป่วนซ้ำซากการประดิษฐ์จะผิด INSERT ... ON CONFLICT ...ใช้ หาก PostgreSQL ของคุณเก่าเกินไปให้อัปเดต
Craig Ringer

@CraigRinger INSERT ... ON CLONFLICT ...ไม่ได้มีไว้สำหรับการโหลดจำนวนมาก จากโพสต์ของคุณLOCK TABLE testtable IN EXCLUSIVE MODE;ภายใน CTE เป็นวิธีแก้ปัญหาเพื่อให้ได้อะตอมมิก ไม่นะ
parisni

@parisni มันไม่ได้มีไว้สำหรับการโหลดจำนวนมาก? พูดว่าใคร postgresql.org/docs/current/sql-insert.html#SQL-ON-CONFLICT แน่นอนว่ามันช้ากว่าการโหลดจำนวนมากโดยไม่มีพฤติกรรมที่หนักหน่วงรุนแรง แต่เห็นได้ชัดว่าเป็นจริงไม่ว่าคุณจะทำอะไร เป็นวิธีที่เร็วกว่าการใช้การโต้ตอบแบบย่อยแน่นอนว่าแน่นอน วิธีที่เร็วที่สุดคือล็อคตารางเป้าหมายจากนั้นทำสิ่งที่insert ... where not exists ...คล้ายกันหรือคล้ายกัน
Craig Ringer

1

เนื่องจากคำถามนี้ถูกปิดฉันกำลังโพสต์ที่นี่สำหรับวิธีที่คุณใช้ SQLAlchemy ผ่านการเรียกซ้ำอีกครั้งจะลองใส่หรืออัปเดตจำนวนมากเพื่อต่อสู้กับสภาพการแข่งขันและข้อผิดพลาดในการตรวจสอบความถูกต้อง

ก่อนนำเข้า

import itertools as it

from functools import partial
from operator import itemgetter

from sqlalchemy.exc import IntegrityError
from app import session
from models import Posts

ตอนนี้ฟังก์ชั่นผู้ช่วยคู่

def chunk(content, chunksize=None):
    """Groups data into chunks each with (at most) `chunksize` items.
    https://stackoverflow.com/a/22919323/408556
    """
    if chunksize:
        i = iter(content)
        generator = (list(it.islice(i, chunksize)) for _ in it.count())
    else:
        generator = iter([content])

    return it.takewhile(bool, generator)


def gen_resources(records):
    """Yields a dictionary if the record's id already exists, a row object 
    otherwise.
    """
    ids = {item[0] for item in session.query(Posts.id)}

    for record in records:
        is_row = hasattr(record, 'to_dict')

        if is_row and record.id in ids:
            # It's a row but the id already exists, so we need to convert it 
            # to a dict that updates the existing record. Since it is duplicate,
            # also yield True
            yield record.to_dict(), True
        elif is_row:
            # It's a row and the id doesn't exist, so no conversion needed. 
            # Since it's not a duplicate, also yield False
            yield record, False
        elif record['id'] in ids:
            # It's a dict and the id already exists, so no conversion needed. 
            # Since it is duplicate, also yield True
            yield record, True
        else:
            # It's a dict and the id doesn't exist, so we need to convert it. 
            # Since it's not a duplicate, also yield False
            yield Posts(**record), False

และในที่สุดก็มีฟังก์ชั่น upsert

def upsert(data, chunksize=None):
    for records in chunk(data, chunksize):
        resources = gen_resources(records)
        sorted_resources = sorted(resources, key=itemgetter(1))

        for dupe, group in it.groupby(sorted_resources, itemgetter(1)):
            items = [g[0] for g in group]

            if dupe:
                _upsert = partial(session.bulk_update_mappings, Posts)
            else:
                _upsert = session.add_all

            try:
                _upsert(items)
                session.commit()
            except IntegrityError:
                # A record was added or deleted after we checked, so retry
                # 
                # modify accordingly by adding additional exceptions, e.g.,
                # except (IntegrityError, ValidationError, ValueError)
                db.session.rollback()
                upsert(items)
            except Exception as e:
                # Some other error occurred so reduce chunksize to isolate the 
                # offending row(s)
                db.session.rollback()
                num_items = len(items)

                if num_items > 1:
                    upsert(items, num_items // 2)
                else:
                    print('Error adding record {}'.format(items[0]))

นี่คือวิธีการใช้งาน

>>> data = [
...     {'id': 1, 'text': 'updated post1'}, 
...     {'id': 5, 'text': 'updated post5'}, 
...     {'id': 1000, 'text': 'new post1000'}]
... 
>>> upsert(data)

ข้อได้เปรียบนี้มีมากกว่าbulk_save_objectsคือมันสามารถจัดการความสัมพันธ์การตรวจสอบข้อผิดพลาด ฯลฯ ในการแทรก (ซึ่งแตกต่างจากการดำเนินงานจำนวนมาก )


มันก็ดูผิดสำหรับฉัน จะเกิดอะไรขึ้นถ้าเซสชันที่เกิดขึ้นพร้อมกันแทรกแถวหลังจากที่คุณรวบรวมรายการ ID ของคุณ หรือลบอย่างใดอย่างหนึ่ง
Craig Ringer

good point @CraigRinger ฉันทำสิ่งที่คล้ายกับสิ่งนี้ แต่มีเพียง 1 เซสชันที่ทำงาน วิธีที่ดีที่สุดในการจัดการหลายเซสชันนั้นคืออะไร ธุรกรรมอาจ?
reubano

การทำธุรกรรมไม่ได้เป็นทางออกที่วิเศษสำหรับปัญหาการเกิดพร้อมกันทั้งหมด คุณสามารถใช้SERIALIZABLE ธุรกรรมและจัดการกับความล้มเหลวของการทำให้เป็นอันดับ แต่มันช้า คุณต้องการการจัดการข้อผิดพลาดและการลองส่งซ้ำ ดูคำตอบของฉันและส่วน "การอ่านที่เกี่ยวข้อง" ในนั้น
Craig Ringer

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