SQLite - UPSERT * ไม่ใช่ * INSERT หรือ REPLACE


535

http://en.wikipedia.org/wiki/Upsert

แทรกการปรับปรุงที่เก็บไว้ proc บน SQL Server

มีวิธีที่ฉลาดในการทำเช่นนี้ใน SQLite ที่ฉันไม่ได้คิด?

โดยทั่วไปฉันต้องการอัปเดตสามในสี่คอลัมน์หากบันทึกมีอยู่หากไม่มีอยู่ฉันต้องการแทรกระเบียนด้วยค่าเริ่มต้น (NUL) สำหรับคอลัมน์ที่สี่

ID เป็นคีย์หลักดังนั้นจะมีเพียงหนึ่งระเบียนที่ UPSERT เท่านั้น

(ฉันพยายามหลีกเลี่ยงโอเวอร์เฮดของ SELECT เพื่อกำหนดว่าฉันต้องอัปเดตหรือแทรกอย่างชัดเจน)

ข้อเสนอแนะ?


ฉันไม่สามารถยืนยันได้ว่าไวยากรณ์บนไซต์ SQLite สำหรับ TABLE CREATE ฉันไม่ได้สร้างตัวอย่างเพื่อทดสอบ แต่ดูเหมือนจะไม่ได้รับการสนับสนุน ..

ถ้าเป็นเช่นนั้นฉันมีสามคอลัมน์ดังนั้นมันจะมีลักษณะดังนี้:

CREATE TABLE table1( 
    id INTEGER PRIMARY KEY ON CONFLICT REPLACE, 
    Blob1 BLOB ON CONFLICT REPLACE, 
    Blob2 BLOB ON CONFLICT REPLACE, 
    Blob3 BLOB 
);

แต่สอง blobs แรกจะไม่ทำให้เกิดความขัดแย้งเฉพาะ ID จะดังนั้นฉัน asusme Blob1 และ Blob2 จะไม่ถูกแทนที่ (ตามที่ต้องการ)


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

ชีวิตของวัตถุคำสั่งจะเป็นดังนี้:

  1. สร้างวัตถุโดยใช้ sqlite3_prepare_v2 ()
  2. ผูกค่ากับพารามิเตอร์โฮสต์โดยใช้อินเตอร์เฟส sqlite3_bind_
  3. เรียกใช้ SQL โดยการเรียก sqlite3_step ()
  4. รีเซ็ตคำสั่งโดยใช้ sqlite3_reset () จากนั้นกลับไปที่ขั้นตอนที่ 2 และทำซ้ำ
  5. ทำลายวัตถุคำสั่งโดยใช้ sqlite3_finalize ()

อัปเดตฉันคาดเดาได้ช้ากว่า INSERT แต่จะเปรียบเทียบกับ SELECT โดยใช้คีย์หลักได้อย่างไร

บางทีฉันควรใช้ตัวเลือกเพื่ออ่านคอลัมน์ที่ 4 (Blob3) แล้วใช้ REPLACE เพื่อเขียนระเบียนใหม่โดยผสมคอลัมน์ที่ 4 เดิมกับข้อมูลใหม่สำหรับ 3 คอลัมน์แรก


5
SQLite - UPSERT มีวางจำหน่ายในรุ่นก่อนวางจำหน่าย: sqlite.1065341.n5.nabble.com/…
gaurav

3
UPSERT มีอยู่ในรุ่น 3.24.0 ของ SQLite
pablo_worker

คำตอบ:


854

สมมติว่าสามคอลัมน์ในตาราง: ID, NAME, ROLE


BAD:สิ่งนี้จะแทรกหรือแทนที่คอลัมน์ทั้งหมดด้วยค่าใหม่สำหรับ ID = 1:

INSERT OR REPLACE INTO Employee (id, name, role) 
  VALUES (1, 'John Foo', 'CEO');

BAD:สิ่งนี้จะแทรกหรือแทนที่ 2 คอลัมน์ ... คอลัมน์ NAME จะถูกตั้งค่าเป็น NULL หรือค่าเริ่มต้น:

INSERT OR REPLACE INTO Employee (id, role) 
  VALUES (1, 'code monkey');

ดี : ใช้ SQLite On ข้อขัดแย้ง สนับสนุน UPSERT ใน SQLite! เพิ่มไวยากรณ์ของ UPSERT ไปยัง SQLite ด้วยรุ่น 3.24.0!

UPSERT เป็นการเพิ่มรูปแบบพิเศษของ INSERT ที่ทำให้ INSERT ทำงานเป็น UPDATE หรือไม่ใช้ถ้า INSERT จะละเมิดข้อ จำกัด ที่ไม่ซ้ำใคร UPSERT ไม่ใช่ SQL มาตรฐาน UPSERT ใน SQLite เป็นไปตามไวยากรณ์ที่สร้างขึ้นโดย PostgreSQL

ป้อนคำอธิบายรูปภาพที่นี่

ดี แต่มีแนวโน้ม:สิ่งนี้จะอัปเดต 2 คอลัมน์ เมื่อ ID = 1 มีอยู่ NAME จะไม่ได้รับผลกระทบ เมื่อ ID = 1 ไม่มีอยู่ชื่อจะเป็นค่าเริ่มต้น (NULL)

INSERT OR REPLACE INTO Employee (id, role, name) 
  VALUES (  1, 
            'code monkey',
            (SELECT name FROM Employee WHERE id = 1)
          );

นี่จะอัปเดต 2 คอลัมน์ เมื่อ ID = 1 มีอยู่บทบาทจะไม่ได้รับผลกระทบ เมื่อ ID = 1 ไม่มีอยู่บทบาทจะถูกตั้งค่าเป็น 'Benchwarmer' แทนค่าเริ่มต้น

INSERT OR REPLACE INTO Employee (id, name, role) 
  VALUES (  1, 
            'Susan Bar',
            COALESCE((SELECT role FROM Employee WHERE id = 1), 'Benchwarmer')
          );

33
+1 ยอดเยี่ยม! ข้อเลือกที่ฝังตัวให้ความยืดหยุ่นในการแทนที่การทำงานของ ON CONFLICT REPLACE หากคุณต้องการรวม / เปรียบเทียบค่าเก่าและค่าใหม่สำหรับเขตข้อมูลใด ๆ
G__

24
หากพนักงานถูกอ้างอิงโดยแถวอื่นที่มีการลบแบบเรียงซ้อนแถวอื่น ๆ จะยังคงถูกลบโดยแทนที่
Don Reba

246
สิ่งนี้ทำให้เจ็บปวด SQlite ต้องการ UPSERT
andig

21
คุณช่วยอธิบายได้ไหมว่าทำไมสิ่งนี้จะแทรกหรือแทนที่คอลัมน์ทั้งหมดด้วยค่าใหม่สำหรับ ID = 1:ถือว่าเป็นBADในตัวอย่างแรกของคุณหรือไม่ คำสั่งที่คุณนำเสนอมีไว้เพื่อสร้างเรกคอร์ดใหม่ด้วย ID 1 ชื่อJohn Fooและ role CEOหรือเขียนทับเรกคอร์ดที่มี ID คือ 1 ถ้ามีอยู่แล้วกับข้อมูลนั้น (สมมติว่าidเป็นคีย์หลัก) . ดังนั้นทำไมมันไม่ดีถ้าเกิดขึ้นอย่างแน่นอน?
หรือผู้ทำแผนที่

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

134

INSERT หรือ REPLACE ไม่เท่ากับ "UPSERT"

สมมติว่าฉันมีตารางพนักงานที่มี ID, ชื่อและบทบาทของฟิลด์:

INSERT OR REPLACE INTO Employee ("id", "name", "role") VALUES (1, "John Foo", "CEO")
INSERT OR REPLACE INTO Employee ("id", "role") VALUES (1, "code monkey")

Boom คุณเสียชื่อพนักงานหมายเลข 1 SQLite ได้แทนที่ด้วยค่าเริ่มต้น

ผลลัพธ์ที่คาดหวังของ UPSERT จะเป็นการเปลี่ยนบทบาทและเพื่อรักษาชื่อไว้


21
-1 จากฉันฉันกลัว ใช่คำตอบที่ยอมรับนั้นผิด แต่ในขณะที่คำตอบของคุณชี้ให้เห็นปัญหามันก็ไม่ใช่คำตอบ สำหรับคำตอบที่แท้จริงให้ดูโซลูชันที่ชาญฉลาดของ Eric Bโดยใช้coalesce((select ..), 'new value')ประโยคในตัว คำตอบของ Eric ต้องการคะแนนเสียงมากกว่าที่นี่ฉันคิดว่า
วันที่

22
จริง Eric เป็นคำตอบที่ดีที่สุดและสมควรได้รับคะแนนเสียงมากขึ้น ที่ถูกกล่าวว่าฉันคิดว่าโดยการชี้ให้เห็นปัญหาฉันมีส่วนเล็กน้อยในการหาคำตอบที่ดี (คำตอบของ Eric มาในภายหลังและสร้างบนตาราง sql ตัวอย่างในคำตอบของฉัน) ดังนั้นไม่แน่ใจว่าฉันสมควรได้รับ -1 แต่ไม่เป็นไร :)
gregschlom

6
+1 เพื่อชดเชย -1 ข้างต้น ฮ่า ๆ. ไทม์ไลน์ที่น่าสนใจในหัวข้อนี้ เห็นได้ชัดว่าคำตอบของคุณเป็นเวลากว่าหนึ่งสัปดาห์ก่อนหน้า Eric แต่คุณทั้งคู่ตอบคำถามเกือบสองปีหลังจากที่ถูกถาม +1 สำหรับ Eric ด้วยเช่นกันสำหรับรายละเอียดเพิ่มเติม
Rich


2
@QED ไม่เพราะ delete + insert (ซึ่งเป็นการแทนที่) คือข้อความสั่ง 2 dml พร้อมด้วยทริกเกอร์ของตนเอง มันไม่เหมือนกับคำสั่งอัพเดต 1 เท่านั้น
Sebas

109

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

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

 CREATE TABLE page (
     id      INTEGER PRIMARY KEY,
     name    TEXT UNIQUE,
     title   TEXT,
     content TEXT,
     author  INTEGER NOT NULL REFERENCES user (id),
     ts      TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 );

โปรดสังเกตว่าโดยเฉพาะอย่างยิ่งนั่นnameคือคีย์ธรรมชาติของแถว - idใช้สำหรับคีย์ต่างประเทศเท่านั้นดังนั้นจุดนี้สำหรับ SQLite เพื่อเลือกค่า ID ของตัวเองเมื่อทำการแทรกแถวใหม่ แต่เมื่อทำการอัปเดตแถวที่มีอยู่โดยอิงจากแถวnameนั้นฉันต้องการให้แถวนั้นยังคงมีค่า ID เก่า (ชัด ๆ !

ฉันบรรลุความจริงUPSERTด้วยโครงสร้างต่อไปนี้:

 WITH new (name, title, author) AS ( VALUES('about', 'About this site', 42) )
 INSERT OR REPLACE INTO page (id, name, title, content, author)
 SELECT old.id, new.name, new.title, old.content, new.author
 FROM new LEFT JOIN page AS old ON new.name = old.name;

รูปแบบที่แน่นอนของแบบสอบถามนี้อาจแตกต่างกันเล็กน้อย กุญแจสำคัญคือการใช้INSERT SELECTกับการรวมภายนอกด้านซ้ายเพื่อเข้าร่วมแถวที่มีอยู่กับค่าใหม่

ที่นี่หากไม่มีแถวก่อนหน้านี้old.idจะเป็นNULLและ SQLite จะกำหนด ID โดยอัตโนมัติ แต่หากมีแถวดังกล่าวแล้วold.idจะมีค่าจริงและจะนำกลับมาใช้ใหม่ ซึ่งเป็นสิ่งที่ฉันต้องการ

ในความเป็นจริงนี้มีความยืดหยุ่นมาก สังเกตว่าtsคอลัมน์หายไปทุกด้านอย่างไร - เนื่องจากมีDEFAULTค่า SQLite จะทำสิ่งที่ถูกต้องในทุกกรณีดังนั้นฉันจึงไม่ต้องดูแลตัวเอง

นอกจากนี้คุณยังสามารถรวมคอลัมน์ทั้งด้านข้างnewและoldด้านข้างแล้วใช้เช่นCOALESCE(new.content, old.content)ด้านนอกSELECTเพื่อพูดว่า "แทรกเนื้อหาใหม่ถ้ามีอยู่หรือเก็บเนื้อหาเก่าไว้" - เช่นถ้าคุณใช้ข้อความค้นหาคงที่และเชื่อมโยงใหม่ ค่ากับตัวยึด


13
+1 ทำงานได้ดี แต่เพิ่มWHERE name = "about"ข้อ จำกัด ในการSELECT ... AS oldเร่งความเร็ว หากคุณมี 1m + แถวสิ่งนี้จะช้ามาก

จุดดี +1 ในความคิดเห็นของคุณ ฉันจะทิ้งคำตอบนั้นไว้เพราะการเพิ่มWHEREประโยคเช่นนั้นต้องการเพียงความซ้ำซ้อนในแบบสอบถามที่ฉันพยายามลบล้างตั้งแต่แรกเมื่อฉันเข้าใกล้ด้วยวิธีนี้ เช่นเคย: เมื่อคุณต้องการประสิทธิภาพ denormalise - โครงสร้างของแบบสอบถามในกรณีนี้
อริสโตเติล Pagaltzis

4
คุณสามารถทำให้ตัวอย่างของอริสโตเติลลดลงได้ถ้าคุณต้องการ: INSERT OR REPLACE INTO page (id, name, title, content, author) SELECT id, 'about', 'About this site', content, 42 FROM ( SELECT NULL ) LEFT JOIN ( SELECT * FROM page WHERE name = 'about' )
jcox

4
สิ่งนี้จะไม่ทON DELETEริกเกอร์ทริกเกอร์โดยไม่จำเป็นเมื่อทำการแทนที่ (หรือเป็นการอัปเดต) หรือไม่
Karakuri

3
มันจะทON DELETEริกเกอร์ Dunno เกี่ยวกับโดยไม่จำเป็น สำหรับผู้ใช้ส่วนใหญ่มันอาจไม่จำเป็นแม้แต่ไม่ต้องการ แต่อาจไม่เหมาะสำหรับผู้ใช้ทั้งหมด เช่นเดียวกันกับความจริงที่ว่ามันจะเรียงซ้อนลบแถวใด ๆ ด้วยคีย์ต่างประเทศลงในแถวที่สงสัย - อาจเป็นปัญหาสำหรับผู้ใช้หลายคน SQLite นั้นไม่มีอะไรใกล้เคียงกับ UPSERT ตัวจริง (บันทึกเพื่อแกล้งมันด้วยINSTEAD OF UPDATEทริกเกอร์ฉันเดา)
อริสโตเติล Pagaltzis

86

หากคุณมักจะทำการปรับปรุงฉันจะ ..

  1. เริ่มต้นการทำธุรกรรม
  2. ทำการอัปเดต
  3. ตรวจสอบ rowcount
  4. ถ้าเป็น 0 ทำแทรก
  5. ผูกมัด

หากคุณมักจะทำเม็ดมีด

  1. เริ่มต้นการทำธุรกรรม
  2. ลองใส่
  3. ตรวจสอบข้อผิดพลาดการละเมิดคีย์หลัก
  4. หากเราพบข้อผิดพลาดให้อัปเดต
  5. ผูกมัด

วิธีนี้ทำให้คุณหลีกเลี่ยงการเลือกและคุณจะได้ยินเสียงธุรกรรมบน Sqlite


ขอบคุณเพิ่งถาม @ stackoverflow.com/questions/2717590/นี้ 1 จากฉัน! =)
Alix Axel

4
หากคุณกำลังจะตรวจสอบ rowcount โดยใช้ sqlite3_changes () ในขั้นตอนที่ 3 ตรวจสอบให้แน่ใจว่าคุณไม่ได้ใช้ตัวจัดการฐานข้อมูลจากหลายเธรดสำหรับการปรับเปลี่ยน
Linulin

3
ต่อไปนี้จะไม่ใช้คำน้อยกว่า แต่มีเอฟเฟกต์เหมือนกัน: 1) เลือกตารางรูปแบบ id โดยที่ id = 'x' 2) ถ้า (ResultSet.rows.length == 0) ตารางอัพเดตที่ id = 'x';
Florin

20
ฉันหวังว่า INSERT หรือ UPDATE เป็นส่วนหนึ่งของภาษา
Florin

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

83

คำตอบนี้ได้รับการปรับปรุงและความคิดเห็นด้านล่างใช้ไม่ได้อีกต่อไป

2018-05-18 กดหยุด

สนับสนุน UPSERT ใน SQLite! เพิ่มไวยากรณ์ของ UPSERT ใน SQLite ด้วยเวอร์ชัน 3.24.0 (อยู่ระหว่างดำเนินการ)!

UPSERT เป็นการเพิ่มรูปแบบพิเศษของ INSERT ที่ทำให้ INSERT ทำงานเป็น UPDATE หรือไม่ใช้ถ้า INSERT จะละเมิดข้อ จำกัด ที่ไม่ซ้ำใคร UPSERT ไม่ใช่ SQL มาตรฐาน UPSERT ใน SQLite เป็นไปตามไวยากรณ์ที่สร้างขึ้นโดย PostgreSQL

ป้อนคำอธิบายรูปภาพที่นี่

อีกทางเลือกหนึ่ง:

อีกวิธีที่แตกต่างกันโดยสิ้นเชิงของการทำเช่นนี้คือในใบสมัครของฉันฉันตั้งค่าของฉันในหน่วยความจำ rowID ให้ยาว MaxValue เมื่อฉันสร้างแถวในหน่วยความจำ (MaxValue จะไม่ถูกใช้เป็น ID คุณจะไม่ได้อยู่นานพอ .... จากนั้นถ้า rowID ไม่ใช่ค่านั้นมันจะต้องอยู่ในฐานข้อมูลแล้วจึงต้องมีการอัพเดทถ้าเป็น MaxValue แล้วมันต้องมีการแทรก นี้จะเป็นประโยชน์เฉพาะในกรณีที่คุณสามารถติดตาม rowIDs ใน app ของคุณ


5
สาธุ เรียบง่ายดีกว่าซับซ้อน รู้สึกง่ายกว่าคำตอบที่ยอมรับเล็กน้อย
kkurian

4
ฉันคิดว่าคุณไม่สามารถแทรกลงใน ... ใน sqlite ที่ไหน? นี่เป็นข้อผิดพลาดทางไวยากรณ์สำหรับฉันใน sqlite3
Charlie Martin

5
@CharlieMartin ถูกต้อง ไวยากรณ์นี้ไม่ถูกต้องสำหรับ SQLite - ซึ่งเป็นสิ่งที่ OP ร้องขอ WHEREประโยคไม่สามารถผนวกไปยังINSERTคำสั่ง: SQLite แทรก ...
colm.anseo

4
คำตอบนี้ทำให้ฉันหลวมจำนวนมากเวลาคำถามที่เป็นเรื่องเกี่ยวกับ SQLite และผมไม่ทราบว่า INSERT ที่ไหนก็ไม่ได้รับการสนับสนุนใน sqite, โปรดเพิ่มบันทึกนี้ว่ามันเป็นไม่ถูกต้องสำหรับ SQLite ในคำตอบของคุณ
Miquel

3
@colminator และคนอื่น ๆ : ฉันแก้ไขคำสั่ง SQL ของเขาโดยใช้คำแนะนำของคุณ
F Lekschas

62

ฉันรู้ว่านี่เป็นเธรดเก่า แต่ฉันได้ทำงานใน sqlite3 ตั้งแต่ปลายและมาด้วยวิธีนี้ที่เหมาะกับความต้องการของฉันในการสร้างคิวรีแบบพารามิเตอร์:

insert or ignore into <table>(<primaryKey>, <column1>, <column2>, ...) values(<primaryKeyValue>, <value1>, <value2>, ...); 
update <table> set <column1>=<value1>, <column2>=<value2>, ... where changes()=0 and <primaryKey>=<primaryKeyValue>; 

มันยังคงมี 2 แบบสอบถามที่มีส่วนคำสั่งในการปรับปรุง แต่ดูเหมือนว่าจะทำเคล็ดลับ ฉันยังมีวิสัยทัศน์นี้ในหัวของฉันว่า sqlite สามารถปรับคำแถลงการปรับปรุงให้ดีที่สุดได้อย่างสมบูรณ์หากการเรียกไปสู่การเปลี่ยนแปลง () มากกว่าศูนย์ จริง ๆ แล้วมันเป็นสิ่งที่เกินกว่าความรู้ของฉันหรือไม่ แต่ผู้ชายสามารถฝันไม่ได้ ;)

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

select case changes() WHEN 0 THEN last_insert_rowid() else <primaryKeyValue> end;

+ 1. สิ่งที่ฉันพยายามแทนที่การแปลงจากโค้ด TSQL ของเซิร์ฟเวอร์ SQL บางอย่าง ขอบคุณ SQL ฉันพยายามแทนที่เหมือนUpdate <statement> If @@ROWCOUNT=0 INSERT INTO <statement>
DarrenMB

14

นี่คือโซลูชันที่จริง ๆ คือ UPSERT (UPDATE หรือ INSERT) แทนที่จะเป็น INSERT หรือ REPLACE (ซึ่งทำงานแตกต่างกันในหลาย ๆ สถานการณ์)

มันได้ผลเช่นนี้:
1. ลองอัปเดตหากมีระเบียนที่มีรหัสเดียวกันอยู่
2. หากการอัพเดตไม่ได้เปลี่ยนแถวใด ๆ ( NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0)) ให้ใส่เรคคอร์ด

ดังนั้นอาจมีการอัปเดตระเบียนที่มีอยู่หรือจะดำเนินการแทรก

รายละเอียดที่สำคัญคือการใช้การเปลี่ยนแปลง () ฟังก์ชั่น SQL เพื่อตรวจสอบว่าคำสั่งการปรับปรุงตีระเบียนที่มีอยู่ใด ๆ และดำเนินการคำสั่งแทรกถ้ามันไม่ได้ตีระเบียนใด ๆ

สิ่งหนึ่งที่ต้องพูดถึงคือฟังก์ชั่นการเปลี่ยนแปลง () ไม่ส่งคืนการเปลี่ยนแปลงที่ดำเนินการโดยทริกเกอร์ระดับล่าง (ดู http://sqlite.org/lang_corefunc.html#changes ) ดังนั้นควรคำนึงถึงสิ่งนั้นด้วย

นี่คือ SQL ...

อัปเดตการทดสอบ:

--Create sample table and records (and drop the table if it already exists)
DROP TABLE IF EXISTS Contact;
CREATE TABLE [Contact] (
  [Id] INTEGER PRIMARY KEY, 
  [Name] TEXT
);
INSERT INTO Contact (Id, Name) VALUES (1, 'Mike');
INSERT INTO Contact (Id, Name) VALUES (2, 'John');

-- Try to update an existing record
UPDATE Contact
SET Name = 'Bob'
WHERE Id = 2;

-- If no record was changed by the update (meaning no record with the same Id existed), insert the record
INSERT INTO Contact (Id, Name)
SELECT 2, 'Bob'
WHERE NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0);

--See the result
SELECT * FROM Contact;

ทดสอบการแทรก:

--Create sample table and records (and drop the table if it already exists)
DROP TABLE IF EXISTS Contact;
CREATE TABLE [Contact] (
  [Id] INTEGER PRIMARY KEY, 
  [Name] TEXT
);
INSERT INTO Contact (Id, Name) VALUES (1, 'Mike');
INSERT INTO Contact (Id, Name) VALUES (2, 'John');

-- Try to update an existing record
UPDATE Contact
SET Name = 'Bob'
WHERE Id = 3;

-- If no record was changed by the update (meaning no record with the same Id existed), insert the record
INSERT INTO Contact (Id, Name)
SELECT 3, 'Bob'
WHERE NOT EXISTS(SELECT changes() AS change FROM Contact WHERE change <> 0);

--See the result
SELECT * FROM Contact;

2
นี่น่าจะเป็นทางออกที่ดีกว่าสำหรับฉันมากกว่า Eric อย่างไรก็ตามINSERT INTO Contact (Id, Name) SELECT 3, 'Bob' WHERE changes() = 0;ควรทำงาน
bkausbk

ขอบคุณคนก็ยังทำงานใน WebSQL (โดยใช้คอร์โดวาและ SQLite ปลั๊กอิน)
Zappescu

11

เริ่มต้นด้วยเวอร์ชัน 3.24.0 UPSERT รองรับโดย SQLite

จากเอกสาร :

UPSERT เป็นการเพิ่มรูปแบบพิเศษของ INSERT ที่ทำให้ INSERT ทำงานเป็น UPDATE หรือไม่ใช้ถ้า INSERT จะละเมิดข้อ จำกัด ที่ไม่ซ้ำใคร UPSERT ไม่ใช่ SQL มาตรฐาน UPSERT ใน SQLite เป็นไปตามไวยากรณ์ที่สร้างขึ้นโดย PostgreSQL เพิ่มไวยากรณ์ของ UPSERT ใน SQLite ด้วยรุ่น 3.24.0 (อยู่ระหว่างดำเนินการ)

UPSERT เป็นคำสั่ง INSERT ทั่วไปที่ตามด้วยประโยค ON CONFLICT แบบพิเศษ

ป้อนคำอธิบายรูปภาพที่นี่

แหล่งที่มาของภาพ: https://www.sqlite.org/images/syntax/upsert-clause.gif


8

คุณสามารถเพิ่มความโกรธใน SQLite ได้จริง ๆ มันดูแตกต่างจากที่คุณคุ้นเคย มันจะมีลักษณะเช่น:

INSERT INTO table name (column1, column2) 
VALUES ("value12", "value2") WHERE id = 123 
ON CONFLICT DO UPDATE 
SET column1 = "value1", column2 = "value2" WHERE id = 123

5

ขยายคำตอบของอริสโตเติลคุณสามารถเลือกจากตาราง 'ซิงเกิลตัน' (ตารางที่คุณสร้างขึ้นเองด้วยแถวเดียว) นี่เป็นการหลีกเลี่ยงการทำซ้ำ

ฉันยังคงเก็บตัวอย่างพกพาข้าม MySQL และ SQLite และใช้คอลัมน์ 'date_added' เป็นตัวอย่างของวิธีที่คุณสามารถตั้งค่าคอลัมน์ในครั้งแรกเท่านั้น

 REPLACE INTO page (
   id,
   name,
   title,
   content,
   author,
   date_added)
 SELECT
   old.id,
   "about",
   "About this site",
   old.content,
   42,
   IFNULL(old.date_added,"21/05/2013")
 FROM singleton
 LEFT JOIN page AS old ON old.name = "about";

4

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

คุณควรแก้ไขข้อความด้านล่างด้วยชื่อตารางและฟิลด์ของคุณเพื่อทำสิ่งที่คุณต้องการ

--first, update any matches
UPDATE DESTINATION_TABLE DT
SET
  MY_FIELD1 = (
              SELECT MY_FIELD1
              FROM SOURCE_TABLE ST
              WHERE ST.PRIMARY_KEY = DT.PRIMARY_KEY
              )
 ,MY_FIELD2 = (
              SELECT MY_FIELD2
              FROM SOURCE_TABLE ST
              WHERE ST.PRIMARY_KEY = DT.PRIMARY_KEY
              )
WHERE EXISTS(
            SELECT ST2.PRIMARY_KEY
            FROM
              SOURCE_TABLE ST2
             ,DESTINATION_TABLE DT2
            WHERE ST2.PRIMARY_KEY = DT2.PRIMARY_KEY
            );

--second, insert any non-matches
INSERT INTO DESTINATION_TABLE(
  MY_FIELD1
 ,MY_FIELD2
)
SELECT
  ST.MY_FIELD1
 ,NULL AS MY_FIELD2  --insert NULL into this field
FROM
  SOURCE_TABLE ST
WHERE NOT EXISTS(
                SELECT DT2.PRIMARY_KEY
                FROM DESTINATION_TABLE DT2
                WHERE DT2.PRIMARY_KEY = ST.PRIMARY_KEY
                );

7
วิธีการที่ซับซ้อน
frast

ฉันคิดว่านั่นไม่ใช่ความคิดที่ดีเพราะคุณต้องทำสองครั้งเพื่อขอโปรแกรมฐานข้อมูล
Ricardo da Rocha Vitor

3

หากมีคนต้องการอ่านโซลูชันของฉันสำหรับ SQLite ใน Cordova ฉันได้รับวิธีการ js ทั่วไปด้วย @david คำตอบด้านบน

function    addOrUpdateRecords(tableName, values, callback) {
get_columnNames(tableName, function (data) {
    var columnNames = data;
    myDb.transaction(function (transaction) {
        var query_update = "";
        var query_insert = "";
        var update_string = "UPDATE " + tableName + " SET ";
        var insert_string = "INSERT INTO " + tableName + " SELECT ";
        myDb.transaction(function (transaction) {
            // Data from the array [[data1, ... datan],[()],[()]...]:
            $.each(values, function (index1, value1) {
                var sel_str = "";
                var upd_str = "";
                var remoteid = "";
                $.each(value1, function (index2, value2) {
                    if (index2 == 0) remoteid = value2;
                    upd_str = upd_str + columnNames[index2] + "='" + value2 + "', ";
                    sel_str = sel_str + "'" + value2 + "', ";
                });
                sel_str = sel_str.substr(0, sel_str.length - 2);
                sel_str = sel_str + " WHERE NOT EXISTS(SELECT changes() AS change FROM "+tableName+" WHERE change <> 0);";
                upd_str = upd_str.substr(0, upd_str.length - 2);
                upd_str = upd_str + " WHERE remoteid = '" + remoteid + "';";                    
                query_update = update_string + upd_str;
                query_insert = insert_string + sel_str;  
                // Start transaction:
                transaction.executeSql(query_update);
                transaction.executeSql(query_insert);                    
            });
        }, function (error) {
            callback("Error: " + error);
        }, function () {
            callback("Success");
        });
    });
});
}

ดังนั้นก่อนรับชื่อคอลัมน์ด้วยฟังก์ชั่นนี้:

function get_columnNames(tableName, callback) {
myDb.transaction(function (transaction) {
    var query_exec = "SELECT name, sql FROM sqlite_master WHERE type='table' AND name ='" + tableName + "'";
    transaction.executeSql(query_exec, [], function (tx, results) {
        var columnParts = results.rows.item(0).sql.replace(/^[^\(]+\(([^\)]+)\)/g, '$1').split(','); ///// RegEx
        var columnNames = [];
        for (i in columnParts) {
            if (typeof columnParts[i] === 'string')
                columnNames.push(columnParts[i].split(" ")[0]);
        };
        callback(columnNames);
    });
});
}

จากนั้นสร้างธุรกรรมโดยทางโปรแกรม

"Values" เป็นอาร์เรย์ที่คุณควรสร้างมาก่อนและแสดงแถวที่คุณต้องการแทรกหรืออัปเดตลงในตาราง

"remoteid" เป็นรหัสที่ฉันใช้เป็นข้อมูลอ้างอิงเนื่องจากฉันซิงค์กับเซิร์ฟเวอร์ระยะไกลของฉัน

สำหรับการใช้งานปลั๊กอิน SQLite Cordova โปรดดูที่ลิงค์ทางการ


2

วิธีนี้จะรีเฟรชวิธีการอื่น ๆ สองสามวิธีจากคำตอบสำหรับคำถามนี้และรวมการใช้ CTE (Common Table Expressions) ฉันจะแนะนำข้อความค้นหาแล้วอธิบายว่าทำไมฉันถึงทำสิ่งที่ฉันทำ

ฉันต้องการเปลี่ยนนามสกุลสำหรับพนักงาน 300 เป็น DAVIS หากมีพนักงาน 300 มิฉะนั้นฉันจะเพิ่มพนักงานใหม่

ชื่อตาราง: คอลัมน์พนักงาน: id, first_name, last_name

แบบสอบถามคือ:

INSERT OR REPLACE INTO employees (employee_id, first_name, last_name)
WITH registered_employees AS ( --CTE for checking if the row exists or not
    SELECT --this is needed to ensure that the null row comes second
        *
    FROM (
        SELECT --an existing row
            *
        FROM
            employees
        WHERE
            employee_id = '300'

        UNION

        SELECT --a dummy row if the original cannot be found
            NULL AS employee_id,
            NULL AS first_name,
            NULL AS last_name
    )
    ORDER BY
        employee_id IS NULL --we want nulls to be last
    LIMIT 1 --we only want one row from this statement
)
SELECT --this is where you provide defaults for what you would like to insert
    registered_employees.employee_id, --if this is null the SQLite default will be used
    COALESCE(registered_employees.first_name, 'SALLY'),
    'DAVIS'
FROM
    registered_employees
;

โดยทั่วไปฉันใช้ CTE เพื่อลดจำนวนครั้งที่คำสั่ง select จะต้องใช้เพื่อกำหนดค่าเริ่มต้น เนื่องจากนี่คือ CTE เราเพียงเลือกคอลัมน์ที่เราต้องการจากตารางและคำสั่ง INSERT จะใช้สิ่งนี้

ตอนนี้คุณสามารถตัดสินใจได้ว่าค่าเริ่มต้นใดที่คุณต้องการใช้โดยแทนที่ค่า Null ในฟังก์ชัน COALESCE ด้วยค่าที่ควรจะเป็น


2

การติดตามAristotle PagaltzisและแนวคิดCOALESCEจากคำตอบของ Eric Bที่นี่เป็นตัวเลือกที่ยอดเยี่ยมในการอัปเดตคอลัมน์เพียงไม่กี่คอลัมน์หรือแทรกเต็มแถวหากไม่มีอยู่

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

NOTE idถูกบังคับให้เป็น NULL เมื่อINSERTมันควรจะเป็น autoincrement ถ้ามันเป็นเพียงคีย์หลักที่สร้างขึ้นก็COALESCEสามารถใช้ (ดูความคิดเห็นอริสโตเติล Pagaltzis )

WITH new (id, name, title, content, author)
     AS ( VALUES(100, 'about', 'About this site', 'Whatever new content here', 42) )
INSERT OR REPLACE INTO page (id, name, title, content, author)
SELECT
     old.id, COALESCE(old.name, new.name),
     new.title, new.content,
     COALESCE(old.author, new.author)
FROM new LEFT JOIN page AS old ON new.name = old.name;

ดังนั้นกฎทั่วไปคือถ้าคุณต้องการเก็บค่าเก่าให้ใช้COALESCEเมื่อคุณต้องการอัพเดทค่าให้ใช้new.fieldname


COALESCE(old.id, new.id)ผิดพลาดอย่างแน่นอนกับคีย์การสร้างอัตโนมัติ และในขณะที่“ รักษาแถวส่วนใหญ่ไว้โดยไม่เปลี่ยนยกเว้นค่าที่หายไป” ดูเหมือนกรณีที่มีคนใช้จริง ๆ แล้วฉันไม่คิดว่านั่นคือสิ่งที่ผู้คนกำลังมองหาเมื่อพวกเขากำลังมองหาวิธีทำ UPSERT
อริสโตเติล Pagaltzis

@AristotlePagaltzis ขออภัยหากฉันผิดฉันไม่ได้ใช้ระบบอัตโนมัติ สิ่งที่ผมกำลังมองหาที่มีแบบสอบถามนี้คือการปรับปรุงเฉพาะไม่กี่คอลัมน์หรือแทรกแถวเต็มรูปแบบที่มีค่าที่ให้มาในกรณีที่มันไม่ได้อยู่ ผมได้เล่นกับคำค้นหาของคุณและฉันไม่สามารถบรรลุมันเมื่อแทรก: คอลัมน์ที่เลือกจากoldตารางที่ได้รับมอบหมายให้เพื่อไม่ให้ค่าที่ให้มาในNULL นี่คือเหตุผลที่จะใช้new COALESCEฉันไม่ได้เป็นผู้เชี่ยวชาญใน SQLite ฉันได้รับการทดสอบแบบสอบถามนี้และดูเหมือนว่าจะทำงานสำหรับกรณีของผมมากจะขอบคุณถ้าคุณสามารถชี้ให้ฉันแก้ปัญหาด้วย autoincrements
Miquel

2
ในกรณีของคีย์ autoincrementing คุณต้องการแทรกNULLเป็นคีย์เนื่องจากบอกให้ SQLite แทรกค่าที่มีอยู่ถัดไปแทน
อริสโตเติล Pagaltzis

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

1

ฉันคิดว่านี่อาจเป็นสิ่งที่คุณกำลังมองหา: อนุประโยคความขัดแย้งข้อขัดแย้ง

หากคุณกำหนดตารางของคุณเช่นนี้:

CREATE TABLE table1( 
    id INTEGER PRIMARY KEY ON CONFLICT REPLACE, 
    field1 TEXT 
); 

ตอนนี้ถ้าคุณทำ INSERT ด้วย id ที่มีอยู่แล้ว SQLite จะทำการอัพเดตโดยอัตโนมัติแทน INSERT

HTH ...


6
ฉันไม่คิดว่างานนี้ก็จะเช็ดออกคอลัมน์ที่ขาดหายไปจากคำสั่ง INSERT
แซม Saffron

3
@Mosor: -1 จากฉันขอโทษ เช่นเดียวกับการออกREPLACEคำสั่ง
Alix Axel

7
-1 เนื่องจากเป็นการลบและการแทรกถ้าคีย์หลักมีอยู่แล้ว
frast

1

หากคุณไม่สนใจที่จะทำสิ่งนี้ในสองปฏิบัติการ

ขั้นตอน:

1) เพิ่มรายการใหม่ด้วย "INSERT OR IGNORE"

2) อัปเดตรายการที่มีอยู่ด้วย "UPDATE"

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

แน่นอนว่าช้าลง ฯลฯ ไม่มีประสิทธิภาพ อ๋อ

ง่ายต่อการเขียน sql และบำรุงรักษาและเข้าใจหรือไม่ อย่างแน่นอน.

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


หมายเหตุสำหรับทุกคน: INSERT หรือ REPLACE ไม่ใช่สิ่งที่โพสต์นี้เกี่ยวข้อง INSERT หรือ REPLACE จะสร้างแถวใหม่ในตารางของคุณด้วย ID ใหม่ นั่นไม่ใช่ UPDATE
sapbucket

-2

หลังจากที่ได้อ่านกระทู้นี้และรู้สึกผิดหวังที่ไม่ใช่แค่ "UPSERT" อันนี้ฉันจึงได้ตรวจสอบเพิ่มเติม ...

คุณสามารถทำสิ่งนี้ได้โดยตรงและง่ายดายใน SQLITE

แทนที่จะใช้: INSERT INTO

ใช้: INSERT OR REPLACE INTO

นี่เป็นสิ่งที่คุณต้องการให้ทำ!


21
-1 ไม่ได้เป็นINSERT OR REPLACE UPSERTดู"คำตอบ" ของ gregschlomด้วยเหตุผลว่าทำไม โซลูชันของ Eric Bใช้งานได้จริงและต้องการ upvotes บางอย่าง
วันที่

-4
SELECT COUNT(*) FROM table1 WHERE id = 1;

ถ้า COUNT(*) = 0

INSERT INTO table1(col1, col2, cole) VALUES(var1,var2,var3);

อื่นถ้า COUNT(*) > 0

UPDATE table1 SET col1 = var4, col2 = var5, col3 = var6 WHERE id = 1;

นี่เป็นวิธีที่ซับซ้อนเกินไป SQL สามารถจัดการสิ่งนี้ได้ดีในหนึ่งแบบสอบถาม
Robin Kanters
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.