แทรกเมื่อมีการอัพเดทซ้ำใน PostgreSQL?


644

หลายเดือนที่ผ่านมาฉันได้เรียนรู้จากคำตอบใน Stack Overflow วิธีดำเนินการอัปเดตหลายรายการพร้อมกันใน MySQL โดยใช้ไวยากรณ์ต่อไปนี้:

INSERT INTO table (id, field, field2) VALUES (1, A, X), (2, B, Y), (3, C, Z)
ON DUPLICATE KEY UPDATE field=VALUES(Col1), field2=VALUES(Col2);

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

เพื่อชี้แจงฉันต้องการแทรกหลายสิ่งและหากมีอยู่แล้วเพื่ออัปเดต


38
ทุกคนที่พบคำถามนี้ควรอ่านบทความของ Depesz "ทำไมความสลับซับซ้อนจึงซับซ้อน" . มันอธิบายปัญหาและแนวทางแก้ไขที่เป็นไปได้อย่างดีเยี่ยม
Craig Ringer

8
UPSERT จะถูกเพิ่มใน Postgres 9.5: wiki.postgresql.org/wiki/…
tommed

4
@tommed - เสร็จเรียบร้อยแล้ว: stackoverflow.com/a/34639631/4418
warren

คำตอบ:


515

PostgreSQL ตั้งแต่เวอร์ชัน 9.5 มีไวยากรณ์ของUPSERTโดยมีON ON clause clause ด้วยไวยากรณ์ต่อไปนี้ (คล้ายกับ MySQL)

INSERT INTO the_table (id, column_1, column_2) 
VALUES (1, 'A', 'X'), (2, 'B', 'Y'), (3, 'C', 'Z')
ON CONFLICT (id) DO UPDATE 
  SET column_1 = excluded.column_1, 
      column_2 = excluded.column_2;

การค้นหาจดหมายเหตุกลุ่มอีเมลของ postgresql สำหรับ "upsert" จะนำไปสู่การค้นหาตัวอย่างของการทำสิ่งที่คุณอาจต้องการทำในคู่มือ :

ตัวอย่างที่ 38-2 ข้อยกเว้นสำหรับ UPDATE / INSERT

ตัวอย่างนี้ใช้การจัดการข้อยกเว้นเพื่อดำเนินการ UPDATE หรือ INSERT ตามความเหมาะสม:

CREATE TABLE db (a INT PRIMARY KEY, b TEXT);

CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS
$$
BEGIN
    LOOP
        -- first try to update the key
        -- note that "a" must be unique
        UPDATE db SET b = data WHERE a = key;
        IF found THEN
            RETURN;
        END IF;
        -- not there, so try to insert the key
        -- if someone else inserts the same key concurrently,
        -- we could get a unique-key failure
        BEGIN
            INSERT INTO db(a,b) VALUES (key, data);
            RETURN;
        EXCEPTION WHEN unique_violation THEN
            -- do nothing, and loop to try the UPDATE again
        END;
    END LOOP;
END;
$$
LANGUAGE plpgsql;

SELECT merge_db(1, 'david');
SELECT merge_db(1, 'dennis');

อาจมีตัวอย่างของวิธีการนี้เป็นกลุ่มโดยใช้ CTE ใน 9.1 และสูงกว่าในรายชื่อผู้รับจดหมายของแฮกเกอร์ :

WITH foos AS (SELECT (UNNEST(%foo[])).*)
updated as (UPDATE foo SET foo.a = foos.a ... RETURNING foo.id)
INSERT INTO foo SELECT foos.* FROM foos LEFT JOIN updated USING(id)
WHERE updated.id IS NULL;

ดูคำตอบของ a_horse_with_no_nameสำหรับตัวอย่างที่ชัดเจนยิ่งขึ้น


7
สิ่งเดียวที่ฉันไม่ชอบเกี่ยวกับเรื่องนี้ก็คือมันจะช้ากว่ากันมากเพราะแต่ละครั้งจะเป็นการโทรของตัวเองไปยังฐานข้อมูล
baash05

@ baash05 อาจมีวิธีการทำแบบกลุ่มดูคำตอบที่อัปเดตของฉัน
Stephen Denne

2
สิ่งเดียวที่ฉันทำแตกต่างกันคือใช้ FOR1.2 LOOP แทนที่จะเป็นแค่ LOOP เพื่อที่ว่าหากมีการละเมิดข้อ จำกัด ที่ไม่เหมือนใครมันจะไม่หมุนไปเรื่อย ๆ
olamork

2
การexcludedอ้างถึงในโซลูชันแรกที่นี่คืออะไร
ichbinallen

2
@ichbinallen ในเอกสาร ตลาดหลักทรัพย์และข้อที่เกี่ยวกับความขัดแย้งในการอัปเดตที่มีสิทธิ์เข้าถึงแถวที่มีอยู่โดยใช้ชื่อของตาราง (หรือนามแฝง) และแถวที่นำเสนอสำหรับการแทรกการใช้ตารางพิเศษยกเว้น ในกรณีนี้excludedตารางพิเศษจะช่วยให้คุณสามารถเข้าถึงค่าที่คุณพยายาม INSERT ตั้งแต่แรก
TMichel

429

คำเตือน: สิ่งนี้ไม่ปลอดภัยหากเรียกใช้จากหลาย ๆ เซสชันในเวลาเดียวกัน (ดูคำเตือนด้านล่าง)


อีกวิธีที่ฉลาดในการทำ "UPSERT" ใน postgresql คือทำสองคำสั่ง UPDATE / INSERT ตามลำดับที่แต่ละข้อความได้รับการออกแบบให้ประสบความสำเร็จหรือไม่มีผล

UPDATE table SET field='C', field2='Z' WHERE id=3;
INSERT INTO table (id, field, field2)
       SELECT 3, 'C', 'Z'
       WHERE NOT EXISTS (SELECT 1 FROM table WHERE id=3);

การอัปเดตจะสำเร็จหากแถวที่มี "id = 3" มีอยู่แล้วมิฉะนั้นจะไม่มีผลกระทบ

INSERT จะประสบความสำเร็จก็ต่อเมื่อแถวที่มี "id = 3" ยังไม่มีอยู่

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

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

วิธีนี้ยังเป็นเรื่องการอัปเดตหายไปในread committedการแยกเว้นแต่การตรวจสอบโปรแกรมการนับแถวได้รับผลกระทบและยืนยันว่าทั้งสองinsertหรือupdateได้รับผลกระทบแถว


6
คำตอบสั้น ๆ : ถ้าบันทึกมีอยู่ INSERT ไม่ทำอะไรเลย คำตอบยาว: SELECT ใน INSERT จะส่งคืนผลลัพธ์ให้มากที่สุดเท่าที่จะมีการแข่งขันของส่วนคำสั่งที่ นั่นคือมากที่สุดหนึ่ง (ถ้าหมายเลขหนึ่งไม่ได้อยู่ในผลลัพธ์ของตัวเลือกย่อย) มิฉะนั้นเป็นศูนย์ INSERT จะเพิ่มแถวหนึ่งหรือศูนย์
Peter Becker

3
ส่วน 'where' สามารถทำให้ง่ายขึ้นโดยการใช้งานอยู่:... where not exists (select 1 from table where id = 3);
Endy Tjahjono

1
นี้ควรจะเป็นคำตอบที่เหมาะสม .. กับการปรับแต่งเล็กน้อยบางอย่างก็อาจจะใช้ในการทำปรับปรุงมวล .. Humm .. ฉันสงสัยว่าตาราง temp สามารถนำมาใช้ ..
baash05

1
@keaplogik, 9.1 ข้อ จำกัด นั้นคือ CTE ที่เขียนได้ (นิพจน์ทั่วไป) ที่อธิบายไว้ในคำตอบอื่น ไวยากรณ์ที่ใช้ในคำตอบนี้เป็นพื้นฐานมากและได้รับการสนับสนุนมานาน
วัว

8
คำเตือนนี้อาจมีการอัปเดตที่หายไปในread committedการแยกเว้นแต่การตรวจสอบใบสมัครของคุณเพื่อให้แน่ใจว่าinsertหรือupdateมีไม่ใช่ศูนย์ rowcount ดูdba.stackexchange.com/q/78510/7788
Craig Ringer

227

ด้วย PostgreSQL 9.1 สามารถทำได้โดยใช้ CTE แบบเขียนได้ ( นิพจน์ตารางทั่วไป ):

WITH new_values (id, field1, field2) as (
  values 
     (1, 'A', 'X'),
     (2, 'B', 'Y'),
     (3, 'C', 'Z')

),
upsert as
( 
    update mytable m 
        set field1 = nv.field1,
            field2 = nv.field2
    FROM new_values nv
    WHERE m.id = nv.id
    RETURNING m.*
)
INSERT INTO mytable (id, field1, field2)
SELECT id, field1, field2
FROM new_values
WHERE NOT EXISTS (SELECT 1 
                  FROM upsert up 
                  WHERE up.id = new_values.id)

ดูรายการบล็อกเหล่านี้:


โปรดทราบว่าโซลูชันนี้ไม่ได้ป้องกันการละเมิดคีย์ที่ไม่ซ้ำกัน แต่จะไม่เสี่ยงต่อการอัปเดตที่สูญหาย
ดูการติดตามโดย Craig Ringer ใน dba.stackexchange.com


1
@ FrançoisBeausoleil: โอกาสของสภาพการแข่งขันมีขนาดเล็กกว่าด้วยวิธี "ลอง / จัดการข้อยกเว้น"
a_horse_with_no_name

2
@a_horse_with_no_name คุณหมายถึงโอกาสในการแข่งขันที่มีขนาดเล็กลงได้อย่างไร เมื่อฉันเรียกใช้แบบสอบถามนี้พร้อมกับระเบียนเดียวกันฉันได้รับข้อผิดพลาด "ค่าคีย์ที่ซ้ำกันละเมิดข้อ จำกัด ที่ไม่ซ้ำกัน" 100% ของเวลาจนกระทั่งแบบสอบถามตรวจพบว่ามีการแทรกระเบียน นี่เป็นตัวอย่างที่สมบูรณ์หรือไม่
Jeroen van Dijk

4
@a_horse_with_no_name ดูเหมือนว่าโซลูชันของคุณจะทำงานในสถานการณ์ที่เกิดขึ้นพร้อมกันเมื่อคุณตัดคำสั่ง upsert ด้วยการล็อกต่อไปนี้: BEGIN WORK; LOCK ตาราง mytable ในโหมดพิเศษ ROW ROW; <UPSERT ที่นี่>; ความมุ่งมั่นในการทำงาน;
Jeroen van Dijk

2
@JeroenvanDijk: ขอบคุณ สิ่งที่ฉันหมายถึงด้วย "เล็กกว่ามาก" คือถ้าหลาย ๆ ธุรกรรมของรายการนี้ (และคอมมิชชันการเปลี่ยนแปลง!) ช่วงเวลาระหว่างการอัพเดตและการแทรกมีขนาดเล็กลงเนื่องจากทุกอย่างเป็นเพียงคำสั่งเดียว คุณสามารถสร้างการละเมิด pk ได้โดยใช้คำสั่ง INSERT อิสระสองข้อความ หากคุณล็อคทั้งตารางคุณจะทำให้การเข้าถึงทั้งหมดเป็นไปอย่างมีประสิทธิภาพ (สิ่งที่คุณสามารถทำได้ด้วยระดับการแยกที่เป็นอนุกรมได้)
a_horse_with_no_name

12
วิธีการแก้ไขนี้อาจมีการอัพเดทที่ขาดหายไปหากการทำธุรกรรมการแทรกย้อนกลับ ไม่มีการตรวจสอบเพื่อบังคับให้มีUPDATEผลกระทบกับแถวใด ๆ
Craig Ringer

132

ใน PostgreSQL 9.5 และใหม่กว่าคุณสามารถINSERT ... ON CONFLICT UPDATEใช้ได้

ดูเอกสาร

MySQL สามารถซักค้านโดยตรงกับINSERT ... ON DUPLICATE KEY UPDATE ON CONFLICT UPDATEไม่มีไวยากรณ์มาตรฐาน SQL ทั้งคู่เป็นส่วนขยายเฉพาะฐานข้อมูล มีเหตุผลที่ดีที่MERGEไม่ได้ใช้สำหรับเรื่องนี้ไวยากรณ์ใหม่ไม่ได้สร้างขึ้นเพื่อความสนุกสนานเท่านั้น (ไวยากรณ์ของ MySQL ยังมีปัญหาที่หมายความว่ามันไม่ได้นำมาใช้โดยตรง)

เช่นการตั้งค่าที่กำหนด:

CREATE TABLE tablename (a integer primary key, b integer, c integer);
INSERT INTO tablename (a, b, c) values (1, 2, 3);

แบบสอบถาม MySQL:

INSERT INTO tablename (a,b,c) VALUES (1,2,3)
  ON DUPLICATE KEY UPDATE c=c+1;

กลายเป็น:

INSERT INTO tablename (a, b, c) values (1, 2, 10)
ON CONFLICT (a) DO UPDATE SET c = tablename.c + 1;

แตกต่าง:

  • คุณต้องระบุชื่อคอลัมน์ (หรือชื่อข้อ จำกัด ที่ไม่ซ้ำกัน) เพื่อใช้สำหรับการตรวจสอบที่ไม่ซ้ำกัน เป็นเรื่องที่ON CONFLICT (columnname) DO

  • SETต้องใช้คำหลักราวกับว่านี่เป็นUPDATEคำสั่งปกติ

มันมีคุณสมบัติที่ดีเช่นกัน:

  • คุณสามารถมีส่วนWHEREคำสั่งของคุณUPDATE(ช่วยให้คุณกลายON CONFLICT UPDATEเป็นON CONFLICT IGNOREค่าบางอย่างได้อย่างมีประสิทธิภาพ)

  • ค่าที่เสนอสำหรับการแทรกจะพร้อมใช้งานเป็นตัวแปรแถวEXCLUDEDซึ่งมีโครงสร้างเดียวกันกับตารางเป้าหมาย คุณสามารถรับค่าดั้งเดิมในตารางโดยใช้ชื่อตาราง ดังนั้นในกรณีนี้EXCLUDED.cจะเป็น10(เพราะนั่นคือสิ่งที่เราพยายามแทรก) และ"table".cจะเป็น3เพราะนั่นคือค่าปัจจุบันในตาราง คุณสามารถใช้อย่างใดอย่างหนึ่งหรือทั้งสองอย่างในการSETแสดงออกและWHEREประโยค

สำหรับพื้นหลังบน upsert ดูวิธีการ UPSERT (รวมแทรก ...ในการอัปเดตซ้ำซ้อน) ใน PostgreSQL


ฉันได้ดูเป็นวิธีการแก้ปัญหาของ PostgreSQL 9.5 ในขณะที่คุณอธิบายไว้ข้างต้นเพราะผมกำลังประสบปัญหาช่องว่างในสาขาที่เพิ่มขึ้นโดยอัตโนมัติขณะที่ภายใต้ของ ON DUPLICATE KEY UPDATEMySQL ฉันได้ดาวน์โหลด Postgres 9.5 และใช้รหัสของคุณแล้ว แต่ปัญหาเดียวกันนี้เกิดขึ้นภายใต้ Postgres: เขตข้อมูลอนุกรมของคีย์หลักไม่ต่อเนื่องกัน (มีช่องว่างระหว่างส่วนแทรกและส่วนปรับปรุง) ความคิดใด ๆ ที่เกิดขึ้นที่นี่? เป็นเรื่องปกติหรือไม่ แนวคิดใดที่จะหลีกเลี่ยงพฤติกรรมนี้ ขอบคุณ.
WM

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

1
@WM ซึ่งจะสร้างปัญหาของตัวเอง โดยทั่วไปคุณติดอยู่ แต่ถ้าคุณพึ่งพา serial / auto_increment แบบไม่มีช่องว่างคุณก็จะมีบั๊กอยู่แล้ว คุณสามารถมีช่องว่างลำดับเนื่องจาก rollbacks รวมทั้งข้อผิดพลาดชั่วคราว - เรียบภายใต้ภาระข้อผิดพลาดของลูกค้ากลางธุรกรรมเกิดปัญหาอื่น ๆ คุณต้องไม่เคยพึ่งพาSERIAL/ SEQUENCEหรือAUTO_INCREMENTไม่ได้มีช่องว่าง หากคุณต้องการลำดับที่ไม่มีช่องว่างพวกเขาจะซับซ้อนมากขึ้น คุณต้องใช้โต๊ะเคาน์เตอร์ปกติ Google จะบอกคุณมากกว่านี้ แต่ระวังลำดับช่องว่างที่ไม่ได้ป้องกันการเกิดพร้อมกันทั้งหมด
Craig Ringer

@WM หากคุณต้องการลำดับและช่องว่างแบบไม่มีช่องว่างอย่างแน่นอนคุณสามารถใช้วิธี upsert ตามฟังก์ชั่นที่กล่าวถึงในคู่มือพร้อมกับการใช้งานลำดับช่องว่างที่ใช้เคาน์เตอร์ตาราง เนื่องจากการBEGIN ... EXCEPTION ...ทำงานในธุรกรรมย่อยที่ได้รับการย้อนกลับเนื่องจากข้อผิดพลาดการเพิ่มลำดับของคุณจะได้รับการย้อนกลับหากINSERTล้มเหลว
Craig Ringer

ขอบคุณ @Craig Ringer มากซึ่งเป็นข้อมูลที่ค่อนข้างดี ฉันรู้ว่าฉันสามารถเพียงแค่ยอมแพ้ในการมีคีย์หลักที่เพิ่มขึ้นอัตโนมัติ ฉันสร้างคอมโพสิตหลักของ 3 ฟิลด์และสำหรับความต้องการในปัจจุบันโดยเฉพาะของฉันไม่มีความจำเป็นสำหรับฟิลด์การเพิ่มอัตโนมัติที่ไม่มีช่องว่าง ขอขอบคุณอีกครั้งข้อมูลที่คุณให้จะช่วยประหยัดเวลาในการพยายามป้องกันพฤติกรรมฐานข้อมูลที่เป็นธรรมชาติและดีต่อสุขภาพในอนาคต ตอนนี้ฉันเข้าใจแล้วดีขึ้น
WM

17

ฉันกำลังมองหาสิ่งเดียวกันเมื่อฉันมาที่นี่ แต่ไม่มีฟังก์ชั่น "upsert" ทั่วไปรบกวนฉันนิดหน่อยดังนั้นฉันคิดว่าคุณสามารถผ่านการอัปเดตและแทรก sql เป็นอาร์กิวเมนต์ในฟังก์ชั่นนั้นด้วยตนเอง

ที่จะมีลักษณะเช่นนี้:

CREATE FUNCTION upsert (sql_update TEXT, sql_insert TEXT)
    RETURNS VOID
    LANGUAGE plpgsql
AS $$
BEGIN
    LOOP
        -- first try to update
        EXECUTE sql_update;
        -- check if the row is found
        IF FOUND THEN
            RETURN;
        END IF;
        -- not found so insert the row
        BEGIN
            EXECUTE sql_insert;
            RETURN;
            EXCEPTION WHEN unique_violation THEN
                -- do nothing and loop
        END;
    END LOOP;
END;
$$;

และบางทีอาจจะทำสิ่งที่คุณเริ่มอยากที่จะทำชุด "upsert" คุณสามารถใช้ Tcl แยก sql_update และห่วงการปรับปรุงแต่ละตี preformance จะมีขนาดเล็กมากดูhttp://archives.postgresql.org/pgsql- ผลการดำเนินงาน / 2006-04 / msg00557.php

ค่าใช้จ่ายสูงสุดคือการดำเนินการค้นหาจากรหัสของคุณในด้านฐานข้อมูลค่าดำเนินการมีขนาดเล็กมาก


3
คุณยังคงต้องดำเนินการนี้ในการลองใหม่และมีแนวโน้มที่จะแข่งขันพร้อมกันDELETEเว้นแต่ว่าคุณจะล็อกตารางหรืออยู่ในSERIALIZABLEการแยกธุรกรรมบน PostgreSQL 9.1 หรือสูงกว่า
Craig Ringer

13

ไม่มีคำสั่งง่ายๆที่จะทำ

แนวทางที่ถูกต้องที่สุดคือการใช้ฟังก์ชั่นเช่นเดียวจากเอกสาร

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

บางสิ่งบางอย่างตาม:

update table
set column = x.column
from (values (1,'aa'),(2,'bb'),(3,'cc')) as x (id, column)
where table.id = x.id
returning id;

สมมติว่า id: 2 ถูกส่งคืน:

insert into table (id, column) values (1, 'aa'), (3, 'cc');

แน่นอนมันจะประกันตัวออกไม่ช้าก็เร็ว (ในสภาพแวดล้อมพร้อมกัน) เนื่องจากมีสภาพการแข่งขันที่ชัดเจนในที่นี่ แต่โดยปกติจะใช้งานได้

นี่เป็นบทความอีกต่อไปและครอบคลุมมากขึ้นในหัวข้อ


1
หากใช้ตัวเลือกนี้โปรดตรวจสอบว่ารหัสถูกส่งกลับแม้ว่าการปรับปรุงจะไม่ทำอะไรเลย ฉันเคยเห็นฐานข้อมูลเพิ่มประสิทธิภาพคำค้นหาเช่น "อัปเดตตาราง foo set bar = 4 โดยที่ bar = 4"
2012

10

โดยส่วนตัวฉันได้ตั้งค่า "กฎ" ที่แนบมากับคำสั่งแทรก สมมติว่าคุณมีตาราง "dns" ที่บันทึกจำนวนผู้ใช้ DNS ต่อลูกค้าในแต่ละครั้ง:

CREATE TABLE dns (
    "time" timestamp without time zone NOT NULL,
    customer_id integer NOT NULL,
    hits integer
);

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

CREATE RULE replace_dns AS 
    ON INSERT TO dns 
    WHERE (EXISTS (SELECT 1 FROM dns WHERE ((dns."time" = new."time") 
            AND (dns.customer_id = new.customer_id)))) 
    DO INSTEAD UPDATE dns 
        SET hits = new.hits 
        WHERE ((dns."time" = new."time") AND (dns.customer_id = new.customer_id));

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

อย่างไรก็ตามหากมีเม็ดมีดเกิดขึ้นมากมายตลอดเวลาคุณจะต้องใส่ตัวล็อกตารางไว้รอบคำสั่งแทรก: การล็อค SHARE ROW EXCLUSIVE จะป้องกันการดำเนินการใด ๆ ที่สามารถแทรกลบหรืออัปเดตแถวในตารางเป้าหมายของคุณ อย่างไรก็ตามการอัปเดตที่ไม่ได้อัปเดตคีย์เฉพาะนั้นปลอดภัยดังนั้นหากคุณไม่มีการดำเนินการใด ๆ ให้ใช้การล็อคคำแนะนำแทน

นอกจากนี้คำสั่ง COPY ไม่ได้ใช้ RULES ดังนั้นหากคุณแทรกด้วย COPY คุณจะต้องใช้ทริกเกอร์แทน


9

ฉันใช้ฟังก์ชันนี้ผสาน

CREATE OR REPLACE FUNCTION merge_tabla(key INT, data TEXT)
  RETURNS void AS
$BODY$
BEGIN
    IF EXISTS(SELECT a FROM tabla WHERE a = key)
        THEN
            UPDATE tabla SET b = data WHERE a = key;
        RETURN;
    ELSE
        INSERT INTO tabla(a,b) VALUES (key, data);
        RETURN;
    END IF;
END;
$BODY$
LANGUAGE plpgsql

1
มันมีประสิทธิภาพมากกว่าที่จะทำupdateก่อนแล้วตรวจสอบจำนวนแถวที่อัพเดต (ดูคำตอบของ Ahmad)
a_horse_with_no_name

8

ฉันกำหนดฟังก์ชั่น "upsert" ด้านบนหากคุณต้องการ INSERT และ REPLACE:

`

 CREATE OR REPLACE FUNCTION upsert(sql_insert text, sql_update text)

 RETURNS void AS
 $BODY$
 BEGIN
    -- first try to insert and after to update. Note : insert has pk and update not...

    EXECUTE sql_insert;
    RETURN;
    EXCEPTION WHEN unique_violation THEN
    EXECUTE sql_update; 
    IF FOUND THEN 
        RETURN; 
    END IF;
 END;
 $BODY$
 LANGUAGE plpgsql VOLATILE
 COST 100;
 ALTER FUNCTION upsert(text, text)
 OWNER TO postgres;`

และหลังจากดำเนินการแล้วให้ทำดังนี้:

SELECT upsert($$INSERT INTO ...$$,$$UPDATE... $$)

สิ่งสำคัญคือการใส่เครื่องหมายคอมม่าสองเท่าเพื่อหลีกเลี่ยงข้อผิดพลาดของคอมไพเลอร์

  • ตรวจสอบความเร็ว ...

7

คล้ายกับคำตอบที่นิยมมากที่สุด แต่ทำงานได้เร็วขึ้นเล็กน้อย:

WITH upsert AS (UPDATE spider_count SET tally=1 WHERE date='today' RETURNING *)
INSERT INTO spider_count (spider, tally) SELECT 'Googlebot', 1 WHERE NOT EXISTS (SELECT * FROM upsert)

(ที่มา: http://www.the-art-of-web.com/sql/upsert/ )


3
สิ่งนี้จะล้มเหลวหากทำงานพร้อมกันในสองเซสชันเนื่องจากไม่มีการอัปเดตที่จะเห็นแถวที่มีอยู่ดังนั้นทั้งสองอัปเดตจะเข้าสู่ศูนย์แถวดังนั้นทั้งสองแบบสอบถามจะออกการแทรก
Craig Ringer

6

ฉันมีปัญหาแบบเดียวกันสำหรับการจัดการการตั้งค่าบัญชีเป็นคู่ค่าชื่อ เกณฑ์การออกแบบคือไคลเอนต์ที่แตกต่างกันอาจมีชุดการตั้งค่าที่แตกต่างกัน

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

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

ทางเลือกของการอัพเดตแต่ละแถว - การตรวจสอบข้อยกเว้นจากนั้นแทรก - หรือการรวมกันของโค้ดน่าเกลียดช้าและมักจะแตกเพราะ (ดังกล่าวข้างต้น) ไม่ใช่ข้อยกเว้น SQL มาตรฐานจัดการการเปลี่ยนจาก db เป็น db - หรือแม้กระทั่งปล่อยให้เป็นอิสระ

 #This is pseudo-code - within the application:
 BEGIN TRANSACTION - get transaction lock
 SELECT all current name value pairs where id = $id into a hash record
 create a merge record from the current and update record
  (set intersection where shared keys in new win, and empty values in new are deleted).
 DELETE all name value pairs where id = $id
 COPY/INSERT merged records 
 END TRANSACTION

ยินดีต้อนรับสู่ SO แนะนำที่ดี! :-)
อย่าตั้งคำถาม

1
นี่เป็นเหมือนREPLACE INTOมากกว่าINSERT INTO ... ON DUPLICATE KEY UPDATEซึ่งอาจทำให้เกิดปัญหาหากคุณใช้ทริกเกอร์ คุณจะสิ้นสุดการเรียกใช้การลบและแทรกทริกเกอร์ / กฎแทนที่จะอัปเดต
cHao

5

ตามเอกสาร PostgreSQL ของINSERTคำสั่งการจัดการON DUPLICATE KEYกรณีไม่ได้รับการสนับสนุน ส่วนหนึ่งของไวยากรณ์นั้นเป็นส่วนเสริม MySQL ที่เป็นกรรมสิทธิ์


@Lucian MERGEนั้นเป็นปฏิบัติการ OLAP มากกว่า ดูstackoverflow.com/q/17267417/398670สำหรับคำอธิบาย มันไม่ได้กำหนดซีแมนทิกส์ที่เกิดขึ้นพร้อมกันและคนส่วนใหญ่ที่ใช้มันเพื่อแสดงความไม่พอใจนั้นเพิ่งสร้างข้อบกพร่อง
Craig Ringer

5
CREATE OR REPLACE FUNCTION save_user(_id integer, _name character varying)
  RETURNS boolean AS
$BODY$
BEGIN
    UPDATE users SET name = _name WHERE id = _id;
    IF FOUND THEN
        RETURN true;
    END IF;
    BEGIN
        INSERT INTO users (id, name) VALUES (_id, _name);
    EXCEPTION WHEN OTHERS THEN
            UPDATE users SET name = _name WHERE id = _id;
        END;
    RETURN TRUE;
END;

$BODY$
  LANGUAGE plpgsql VOLATILE STRICT

5

สำหรับการรวมชุดเล็กเข้าด้วยกันฟังก์ชั่นด้านบนก็ใช้ได้ อย่างไรก็ตามหากคุณกำลังรวมข้อมูลจำนวนมากฉันขอแนะนำให้ดูในhttp://mbk.projects.postgresql.org

แนวปฏิบัติที่ดีที่สุดในปัจจุบันที่ฉันรู้คือ:

  1. คัดลอกข้อมูลใหม่ / ที่อัปเดตลงในตารางชั่วคราว (แน่นอนหรือคุณสามารถทำ INSERT ถ้าค่าใช้จ่ายตกลง)
  2. Acquire Lock [ตัวเลือก] (คำแนะนำดีกว่าการล็อคตาราง IMO)
  3. ผสาน (ส่วนที่สนุก)

5

UPDATE จะส่งคืนจำนวนแถวที่แก้ไข หากคุณใช้ JDBC (Java) คุณสามารถตรวจสอบค่านี้กับ 0 และหากไม่มีแถวใดได้รับผลกระทบให้ดำเนินการ INSERT แทน หากคุณใช้ภาษาการเขียนโปรแกรมอื่น ๆ อาจจะสามารถรับจำนวนแถวที่แก้ไขได้โปรดตรวจสอบเอกสารประกอบ

สิ่งนี้อาจไม่สวยงาม แต่คุณมี SQL ที่ง่ายกว่ามากที่ใช้งานเล็กน้อยจากรหัสการโทร แตกต่างกันถ้าคุณเขียนสคริปต์สิบบรรทัดใน PL / PSQL คุณอาจจะต้องทดสอบหน่วยหนึ่งหรือประเภทอื่นเพียงอย่างเดียว


4

แก้ไข:สิ่งนี้ไม่ทำงานตามที่คาดไว้ ซึ่งแตกต่างจากคำตอบที่ยอมรับนี้สร้างการละเมิดคีย์ที่ไม่ซ้ำกันเมื่อกระบวนการที่สองเรียกซ้ำ ๆupsert_fooกันพร้อมกัน

ยูเรก้า! ฉันคิดหาวิธีที่จะทำในแบบสอบถามเดียว: ใช้UPDATE ... RETURNINGเพื่อทดสอบว่าแถวใดได้รับผลกระทบ:

CREATE TABLE foo (k INT PRIMARY KEY, v TEXT);

CREATE FUNCTION update_foo(k INT, v TEXT)
RETURNS SETOF INT AS $$
    UPDATE foo SET v = $2 WHERE k = $1 RETURNING $1
$$ LANGUAGE sql;

CREATE FUNCTION upsert_foo(k INT, v TEXT)
RETURNS VOID AS $$
    INSERT INTO foo
        SELECT $1, $2
        WHERE NOT EXISTS (SELECT update_foo($1, $2))
$$ LANGUAGE sql;

UPDATEจะต้องมีการดำเนินการในขั้นตอนการแยกจากกันเพราะโชคไม่ดีนี้เป็นไวยากรณ์ผิดพลาด:

... WHERE NOT EXISTS (UPDATE ...)

ตอนนี้มันทำงานได้ตามที่ต้องการ:

SELECT upsert_foo(1, 'hi');
SELECT upsert_foo(1, 'bye');
SELECT upsert_foo(3, 'hi');
SELECT upsert_foo(3, 'bye');

1
คุณสามารถรวมมันเป็นหนึ่งคำสั่งถ้าคุณใช้ CTE แบบเขียนได้ แต่เช่นเดียวกับโซลูชันส่วนใหญ่ที่โพสต์ที่นี่สิ่งนี้ผิดและจะล้มเหลวเมื่อมีการอัพเดตพร้อมกัน
Craig Ringer
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.