SQLite UPSERT / UPDATE หรือ INSERT


105

ฉันจำเป็นต้องทำการ UPSERT / INSERT หรือ UPDATE กับฐานข้อมูล SQLite

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

นี่จะเป็นตาราง:

ผู้เล่น - (คีย์หลักบน id, user_name unique)

|  id   | user_name |  age   |
------------------------------
|  1982 |   johnny  |  23    |
|  1983 |   steven  |  29    |
|  1984 |   pepee   |  40    |

คำตอบ:


59

นี่คือคำตอบที่ล่าช้า เริ่มจาก SQLIte 3.24.0 ซึ่งเผยแพร่เมื่อวันที่ 4 มิถุนายน 2018 ในที่สุดก็มีการรองรับประโยคUPSERTตามรูปแบบของ PostgreSQL

INSERT INTO players (user_name, age)
  VALUES('steven', 32) 
  ON CONFLICT(user_name) 
  DO UPDATE SET age=excluded.age;

หมายเหตุ: สำหรับผู้ที่ต้องใช้ SQLite เวอร์ชันก่อนหน้า 3.24.0 โปรดอ้างอิงคำตอบด้านล่างนี้ (โพสต์โดยฉัน @MarqueIV)

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


ตอนนี้ยังไม่มีรุ่นนี้ในที่เก็บ Ubuntu
bl79

เหตุใดฉันจึงใช้สิ่งนี้บน Android ไม่ได้ ฉันพยายามdb.execSQL("insert into bla(id,name) values (?,?) on conflict(id) do update set name=?")แล้ว ทำให้ฉันมีข้อผิดพลาดทางไวยากรณ์เกี่ยวกับคำว่า "on"
Bastian Voigt

1
@BastianVoigt เนื่องจากไลบรารี SQLite3 ที่ติดตั้งบน Android เวอร์ชันต่างๆนั้นเก่ากว่า 3.24.0 โปรดดู: developer.android.com/reference/android/database/sqlite/…น่าเศร้าที่คุณต้องมีคุณสมบัติใหม่ของ SQLite3 (หรือไลบรารีระบบอื่น ๆ ) บน Android หรือ iOS คุณต้องรวม SQLite เวอร์ชันเฉพาะไว้ในของคุณ แอปพลิเคชันแทนการพึ่งพาระบบที่ติดตั้ง
prapin

แทนที่จะเป็น UPSERT นี่ไม่ใช่ INDATE มากกว่านี้เพราะลองใส่ครั้งแรกหรือเปล่า? ;)
Mark A. Donohoe

@BastianVoigt โปรดดูคำตอบของฉันด้านล่าง (ลิงก์ในคำถามด้านบน) ซึ่งสำหรับเวอร์ชันก่อนหน้า 3.24.0
Mark A. Donohoe

108

สไตล์ถามตอบ

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


ตัวเลือกที่ 1: คุณสามารถลบแถวได้

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

สมมติว่าเราต้องการUPSERTด้วย data user_name = 'steven' และ age = 32

ดูรหัสนี้:

INSERT INTO players (id, name, age)

VALUES (
    coalesce((select id from players where user_name='steven'),
             (select max(id) from drawings) + 1),
    32)

เคล็ดลับอยู่ที่การรวมตัวกัน ส่งคืน id ของผู้ใช้ 'steven' ถ้ามีมิฉะนั้นจะส่งคืน id ใหม่


ตัวเลือกที่ 2: คุณไม่สามารถลบแถวได้

หลังจากลองใช้วิธีแก้ปัญหาก่อนหน้านี้แล้วฉันก็รู้ว่าในกรณีของฉันที่สามารถทำลายข้อมูลได้เนื่องจาก ID นี้ทำงานเป็นคีย์ต่างประเทศสำหรับตารางอื่น นอกจากนี้ฉันสร้างตารางด้วยประโยคON DELETE CASCADEซึ่งหมายความว่าจะลบข้อมูลอย่างเงียบ ๆ อันตราย.

ดังนั้นครั้งแรกที่ผมคิดว่าประโยค IF แต่ SQLite มีเพียงกรณี และไม่สามารถใช้CASEนี้ได้ (หรืออย่างน้อยฉันก็ไม่ได้จัดการ) เพื่อทำการค้นหาUPDATEหนึ่งรายการหาก EXISTS (เลือก id จากผู้เล่นที่ user_name = 'steven') และINSERTหากไม่เป็นเช่นนั้น ไม่ไป.

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

ข้อมูลเหมือนเดิม: user_name = 'steven' และ age = 32

-- make sure it exists
INSERT OR IGNORE INTO players (user_name, age) VALUES ('steven', 32); 

-- make sure it has the right data
UPDATE players SET user_name='steven', age=32 WHERE user_name='steven'; 

และนั่นคือทั้งหมด!

แก้ไข

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

-- Try to update any existing row
UPDATE players SET age=32 WHERE user_name='steven';

-- Make sure it exists
INSERT OR IGNORE INTO players (user_name, age) VALUES ('steven', 32); 

10
Ditto ... option 2 ดีมาก ยกเว้นฉันทำในทางกลับกัน: ลองอัปเดตตรวจสอบว่าแถวที่ได้รับผลกระทบ> 0 ถ้าไม่ให้แทรก
Tom Spencer

นั่นเป็นแนวทางที่ดีเช่นกันข้อเสียเปรียบเพียงเล็กน้อยก็คือคุณไม่มี SQL เพียงตัวเดียวสำหรับ "upsert"
bgusach

2
คุณไม่จำเป็นต้องตั้ง user_name ใหม่ในคำสั่ง update ในตัวอย่างโค้ดล่าสุด กำหนดอายุก็พอแล้ว
Serg Stetsuk

72

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

ลองนี่ ...

-- Try to update any existing row
UPDATE players
SET age=32
WHERE user_name='steven';

-- If no update happened (i.e. the row didn't exist) then insert one
INSERT INTO players (user_name, age)
SELECT 'steven', 32
WHERE (Select Changes() = 0);

มันทำงานอย่างไร

'ซอสวิเศษ' ที่นี่ใช้Changes()ในWhereประโยค Changes()แสดงถึงจำนวนแถวที่ได้รับผลกระทบจากการดำเนินการล่าสุดซึ่งในกรณีนี้คือการอัปเดต

ในตัวอย่างข้างต้นหากไม่มีการเปลี่ยนแปลงใด ๆ จากการอัปเดต (กล่าวคือไม่มีเรกคอร์ด) แล้วChanges()= 0 ดังนั้นWhereอนุประโยคในInsertคำสั่งจะประเมินเป็นจริงและมีการแทรกแถวใหม่ด้วยข้อมูลที่ระบุ

ถ้าUpdate ไม่ปรับปรุงแถวที่มีอยู่แล้วChanges()= 1 (หรือมากกว่าได้อย่างถูกต้องไม่เป็นศูนย์ถ้ามีมากกว่าหนึ่งแถวได้รับการปรับปรุง) ดังนั้น 'ที่ไหน' ประโยคในInsertขณะนี้ประเมินเป็นเท็จและทำให้ไม่มีการแทรกที่จะเกิดขึ้น

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

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


1
สิ่งนี้ได้ผลดีสำหรับฉัน ฉันไม่เคยเห็นโซลูชันนี้จากที่อื่นพร้อมกับตัวอย่าง INSERT OR REPLACE ทั้งหมดมันยืดหยุ่นกว่าสำหรับกรณีการใช้งานของฉัน
csab

@MarqueIV แล้วถ้ามีสองรายการจะต้องอัพเดทหรือแทรก? ตัวอย่างเช่นเฟิร์สได้รับการอัปเดตและอันที่สองไม่มีอยู่ ในกรณีเช่นนี้Changes() = 0จะส่งคืนเท็จและสองแถวจะแทรกหรือแทนที่
Andriy Antonov

โดยปกติแล้ว UPSERT ควรทำหน้าที่ในบันทึกเดียว หากคุณกำลังบอกว่าคุณรู้แน่ว่ามีการใช้งานมากกว่าหนึ่งระเบียนให้เปลี่ยนการตรวจนับตามนั้น
Mark A. Donohoe

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

1
ทำไมถึงเป็นสิ่งที่ไม่ดี? แล้วถ้าข้อมูลยังไม่เปลี่ยนแปลงทำไมคุณถึงโทรมาUPSERTตั้งแต่แรก? แต่ถึงกระนั้นก็เป็นเรื่องดีที่การอัปเดตจะเกิดขึ้นการตั้งค่าChanges=1มิฉะนั้นINSERTคำสั่งจะเริ่มทำงานอย่างไม่ถูกต้องซึ่งคุณไม่ต้องการ
Mark A. Donohoe

25

ปัญหาเกี่ยวกับคำตอบที่นำเสนอทั้งหมดขาดการพิจารณาทริกเกอร์ (และอาจเป็นผลข้างเคียงอื่น ๆ ) วิธีแก้ปัญหาเช่น

INSERT OR IGNORE ...
UPDATE ...

นำไปสู่ทริกเกอร์ทั้งสองที่ดำเนินการ (สำหรับการแทรกและจากนั้นสำหรับการอัปเดต) เมื่อไม่มีแถว

วิธีแก้ปัญหาที่เหมาะสมคือ

UPDATE OR IGNORE ...
INSERT OR IGNORE ...

ในกรณีนั้นจะดำเนินการคำสั่งเดียวเท่านั้น (เมื่อมีแถวอยู่หรือไม่)


1
ฉันเห็นประเด็นของคุณ ฉันจะอัปเดตคำถามของฉัน ยังไงซะฉันก็ไม่UPDATE OR IGNOREจำเป็นเพราะเหตุใดการอัปเดตจะไม่ขัดข้องหากไม่พบแถว
bgusach

1
อ่านง่าย? ฉันสามารถดูว่าโค้ดของ Andy กำลังทำอะไรได้อย่างรวดเร็ว ขอแสดงความนับถือฉันต้องศึกษาสักครู่เพื่อคิดออก
Brandan

6

หากต้องการมี UPSERT ที่บริสุทธิ์โดยไม่มีรู (สำหรับโปรแกรมเมอร์) ที่ไม่ถ่ายทอดคีย์ที่ไม่ซ้ำกันและคีย์อื่น ๆ :

UPDATE players SET user_name="gil", age=32 WHERE user_name='george'; 
SELECT changes();

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

INSERT INTO players (user_name, age) VALUES ('gil', 32); 

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

4

คุณยังสามารถเพิ่มอนุประโยค ON CONFLICT REPLACE ให้กับข้อ จำกัด เฉพาะ user_name ของคุณจากนั้นเพียงแค่ใส่เข้าไปปล่อยให้ SQLite ดูว่าจะทำอย่างไรในกรณีที่มีข้อขัดแย้ง ดู: https://sqlite.org/lang_conflict.html

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


2

ตัวเลือกที่ 1: แทรก -> อัปเดต

หากคุณต้องการหลีกเลี่ยงทั้งสองอย่างchanges()=0และINSERT OR IGNOREแม้ว่าคุณจะไม่สามารถลบแถวได้ - คุณสามารถใช้ตรรกะนี้ได้

ขั้นแรกให้แทรก (หากไม่มี) แล้วอัปเดตโดยกรองด้วยคีย์เฉพาะ

ตัวอย่าง

-- Table structure
CREATE TABLE players (
    id        INTEGER       PRIMARY KEY AUTOINCREMENT,
    user_name VARCHAR (255) NOT NULL
                            UNIQUE,
    age       INTEGER       NOT NULL
);

-- Insert if NOT exists
INSERT INTO players (user_name, age)
SELECT 'johnny', 20
WHERE NOT EXISTS (SELECT 1 FROM players WHERE user_name='johnny' AND age=20);

-- Update (will affect row, only if found)
-- no point to update user_name to 'johnny' since it's unique, and we filter by it as well
UPDATE players 
SET age=20 
WHERE user_name='johnny';

เกี่ยวกับทริกเกอร์

หมายเหตุ: ฉันยังไม่ได้ทดสอบเพื่อดูว่าทริกเกอร์ใดถูกเรียก แต่ฉันถือว่าสิ่งต่อไปนี้:

ถ้าไม่มีแถว

  • ก่อนใส่
  • แทรกโดยใช้ INSTEAD OF
  • หลังจากใส่
  • ก่อนอัปเดต
  • อัปเดตโดยใช้ INSTEAD OF
  • หลังอัพเดท

ถ้ามีแถว

  • ก่อนอัปเดต
  • อัปเดตโดยใช้ INSTEAD OF
  • หลังอัพเดท

ตัวเลือกที่ 2: แทรกหรือแทนที่ - เก็บ ID ของคุณเอง

ด้วยวิธีนี้คุณสามารถมีคำสั่ง SQL คำสั่งเดียว

-- Table structure
CREATE TABLE players (
    id        INTEGER       PRIMARY KEY AUTOINCREMENT,
    user_name VARCHAR (255) NOT NULL
                            UNIQUE,
    age       INTEGER       NOT NULL
);

-- Single command to insert or update
INSERT OR REPLACE INTO players 
(id, user_name, age) 
VALUES ((SELECT id from players WHERE user_name='johnny' AND age=20),
        'johnny',
        20);

แก้ไข: เพิ่มตัวเลือก 2

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