SQLite UPSERT / UPDATE OR INSERT


102

Tôi cần thực hiện UPSERT / INSERT OR UPDATE đối với Cơ sở dữ liệu SQLite.

Có lệnh INSERT OR REPLACE trong nhiều trường hợp có thể hữu ích. Nhưng nếu bạn muốn giữ lại id của mình với tính năng tự động gia tăng vì khóa ngoại, nó không hoạt động vì nó sẽ xóa hàng, tạo một hàng mới và do đó hàng mới này có một ID mới.

Đây sẽ là bảng:

người chơi - (khóa chính trên id, user_name duy nhất)

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

Câu trả lời:


51

Đây là một câu trả lời muộn. Bắt đầu từ SQLIte 3.24.0, được phát hành vào ngày 4 tháng 6 năm 2018, cuối cùng đã có hỗ trợ cho mệnh đề UPSERT theo cú pháp PostgreSQL.

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

Lưu ý: Đối với những người phải sử dụng phiên bản SQLite cũ hơn 3.24.0, vui lòng tham khảo câu trả lời này bên dưới (do tôi, @MarqueIV đăng).

Tuy nhiên, nếu bạn có tùy chọn nâng cấp, bạn rất nên làm như vậy vì không giống như giải pháp của tôi, giải pháp được đăng ở đây đạt được hành vi mong muốn trong một tuyên bố duy nhất. Ngoài ra, bạn nhận được tất cả các tính năng, cải tiến và sửa lỗi khác thường đi kèm với bản phát hành gần đây hơn.


Hiện tại, chưa có bản phát hành này trong kho lưu trữ Ubuntu.
bl79,

Tại sao tôi không thể sử dụng cái này trên Android? Tôi đã thử db.execSQL("insert into bla(id,name) values (?,?) on conflict(id) do update set name=?"). Mang lại cho tôi một lỗi cú pháp trên từ "trên"
Bastian Voigt

1
@BastianVoigt Vì thư viện SQLite3 được cài đặt trên các phiên bản Android khác nhau cũ hơn 3.24.0. Xem: developer.android.com/reference/android/database/sqlite/… Đáng tiếc là bạn cần một tính năng mới của SQLite3 (hoặc bất kỳ thư viện hệ thống nào khác) trên Android hoặc iOS, bạn cần phải gói một phiên bản SQLite cụ thể trong ứng dụng thay vì dựa vào hệ thống đã cài đặt.
prapin

Thay vì UPSERT, đây không phải là một INDATE vì nó thử chèn trước? ;)
Mark A. Donohoe

@BastianVoigt, vui lòng xem câu trả lời của tôi bên dưới (được liên kết trong câu hỏi ở trên) dành cho các phiên bản cũ hơn 3.24.0.
Mark A. Donohoe

105

Q&A Style

Vâng, sau khi nghiên cứu và đấu tranh với vấn đề trong nhiều giờ, tôi phát hiện ra rằng có hai cách để thực hiện điều này, tùy thuộc vào cấu trúc bảng của bạn và nếu bạn đã kích hoạt các hạn chế về khóa ngoại để duy trì tính toàn vẹn. Tôi muốn chia sẻ điều này ở định dạng rõ ràng để tiết kiệm thời gian cho những người có thể ở trong hoàn cảnh của tôi.


Tùy chọn 1: Bạn có thể đủ khả năng xóa hàng

Nói cách khác, bạn không có khóa ngoại hoặc nếu bạn có khóa ngoại, công cụ SQLite của bạn được định cấu hình để không có ngoại lệ toàn vẹn. Cách để đi là CHÈN HOẶC THAY THẾ . Nếu bạn đang cố gắng chèn / cập nhật trình phát có ID đã tồn tại, công cụ SQLite sẽ xóa hàng đó và chèn dữ liệu bạn đang cung cấp. Bây giờ câu hỏi đặt ra: phải làm gì để giữ liên kết ID cũ?

Giả sử chúng ta muốn UPSERT với dữ liệu user_name = 'steven' và age = 32.

Nhìn vào mã này:

INSERT INTO players (id, name, age)

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

Bí quyết là liên kết với nhau. Nó trả về id của người dùng 'steven' nếu có, và nếu không, nó trả về một id mới mới.


Tùy chọn 2: Bạn không thể xóa hàng

Sau khi xoay sở với giải pháp trước đó, tôi nhận ra rằng trong trường hợp của tôi, điều đó có thể kết thúc việc phá hủy dữ liệu, vì ID này hoạt động như một khóa ngoại cho bảng khác. Bên cạnh đó, tôi đã tạo bảng với mệnh đề ON DELETE CASCADE , có nghĩa là nó sẽ xóa dữ liệu một cách âm thầm. Nguy hiểm.

Vì vậy, đầu tiên tôi nghĩ đến một mệnh đề IF, nhưng SQLite chỉ có CASE . Và không thể sử dụng CASE này (hoặc ít nhất là tôi không quản lý nó) để thực hiện một truy vấn CẬP NHẬT nếu TỒN TẠI (chọn id từ những người chơi mà user_name = 'steven') và CHÈN nếu không. Không đi.

Và sau đó, cuối cùng tôi đã sử dụng tính vũ phu, thành công. Logic là, đối với mỗi UPSERT mà bạn muốn thực hiện, trước tiên hãy thực hiện CHÈN HOẶC BỎ QUA để đảm bảo rằng có một hàng với người dùng của chúng tôi, sau đó thực hiện truy vấn CẬP NHẬT với chính xác dữ liệu mà bạn đã cố gắng chèn.

Dữ liệu tương tự như trước: user_name = 'steven' và 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'; 

Và đó là tất cả!

BIÊN TẬP

Như Andy đã nhận xét, cố gắng chèn trước và sau đó cập nhật có thể dẫn đến việc kích hoạt trình kích hoạt thường xuyên hơn dự kiến. Theo tôi, đây không phải là vấn đề an toàn dữ liệu, nhưng đúng là việc kích hoạt các sự kiện không cần thiết có rất ít ý nghĩa. Do đó, một giải pháp cải tiến sẽ là:

-- 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 ... tùy chọn 2 là tuyệt vời. Ngoại trừ, tôi đã làm theo cách khác: thử cập nhật, kiểm tra xem rowAffected> 0, nếu không thì thực hiện chèn.
Tom Spencer

Đó cũng là một cách tiếp cận khá tốt, nhược điểm nhỏ duy nhất là bạn không có duy nhất một SQL cho "upsert".
bgusach

2
bạn không cần đặt lại user_name trong câu lệnh cập nhật trong mẫu mã cuối cùng. Nó đủ để thiết lập độ tuổi.
Serg Stetsuk

72

Đây là một cách tiếp cận không yêu cầu 'bỏ qua' brute-force sẽ chỉ hoạt động nếu có vi phạm chính. Cách này hoạt động dựa trên bất kỳ điều kiện nào bạn chỉ định trong bản cập nhật.

Thử cái này...

-- 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);

Làm thế nào nó hoạt động

'Nước sốt ma thuật' ở đây được sử dụng Changes()trong Wheremệnh đề. Changes()đại diện cho số hàng bị ảnh hưởng bởi hoạt động cuối cùng, trong trường hợp này là cập nhật.

Trong ví dụ trên, nếu không có thay đổi nào từ bản cập nhật (tức là bản ghi không tồn tại) thì Changes()= 0 do đó Wheremệnh đề trong Insertcâu lệnh đánh giá là true và một hàng mới được chèn với dữ liệu đã chỉ định.

Nếu Update đã cập nhật một hàng hiện có, thì Changes()= 1 (hoặc chính xác hơn, không phải bằng 0 nếu nhiều hơn một hàng được cập nhật), vì vậy mệnh đề 'Where' trong Inserthiện tại đánh giá là false và do đó sẽ không có chèn nào diễn ra.

Cái hay của điều này là không cần dùng vũ lực, cũng như không cần xóa, sau đó chèn lại dữ liệu một cách không cần thiết, điều này có thể dẫn đến rối tung các khóa xuôi dòng trong các mối quan hệ khóa ngoại.

Ngoài ra, vì nó chỉ là một Wheređiều khoản tiêu chuẩn , nó có thể dựa trên bất kỳ điều gì bạn xác định, không chỉ các vi phạm chính. Tương tự như vậy, bạn có thể sử dụng Changes()kết hợp với bất kỳ thứ gì khác mà bạn muốn / cần ở bất kỳ nơi nào cho phép các biểu thức.


1
Nó hiệu quả tuyệt vời đối với tôi. Tôi chưa thấy giải pháp này ở bất kỳ nơi nào khác cùng với tất cả các ví dụ CHÈN HOẶC THAY THẾ, nó linh hoạt hơn nhiều cho trường hợp sử dụng của tôi.
csab

@MarqueIV và nếu có hai mục phải được cập nhật hoặc chèn vào thì sao? ví dụ: cái đầu tiên đã được cập nhật và cái thứ hai không tồn tại. trong trường hợp như vậy Changes() = 0sẽ trở lại sai và hai hàng sẽ làm INSERT OR REPLACE
Andriy Antonov

Thông thường, một UPSERT phải hoạt động trên một bản ghi. Nếu bạn đang nói rằng bạn biết chắc nó đang hoạt động trên nhiều bản ghi, thì hãy thay đổi kiểm tra số lượng cho phù hợp.
Mark A. Donohoe

Điều tồi tệ là nếu hàng tồn tại, phương thức cập nhật phải được thực hiện bất kể hàng đó có thay đổi hay không.
Jimi,

1
Tại sao đó là một điều xấu? Và nếu dữ liệu không thay đổi, tại sao bạn lại gọi UPSERTngay từ đầu? Nhưng ngay cả như vậy, đó là một điều tốt khi cập nhật xảy ra, thiết lập Changes=1hoặc nếu không INSERTcâu lệnh sẽ kích hoạt không chính xác, điều mà bạn không muốn.
Mark A. Donohoe

25

Vấn đề với tất cả các câu trả lời được trình bày, nó hoàn toàn thiếu tính đến các yếu tố kích hoạt (và có thể là các tác dụng phụ khác). Giải pháp như

INSERT OR IGNORE ...
UPDATE ...

dẫn đến cả hai trình kích hoạt được thực thi (đối với chèn và sau đó đối với cập nhật) khi hàng không tồn tại.

Giải pháp thích hợp là

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

trong trường hợp đó chỉ có một câu lệnh được thực thi (khi hàng tồn tại hoặc không).


1
Tôi thấy điểm của bạn. Tôi sẽ cập nhật câu hỏi của tôi. Nhân tiện, tôi không hiểu tại sao lại UPDATE OR IGNOREcần thiết, vì cập nhật sẽ không bị lỗi nếu không tìm thấy hàng nào.
bgusach

1
khả năng đọc? Tôi có thể thấy mã của Andy đang hoạt động trong nháy mắt. Của bạn thân tôi đã phải nghiên cứu một phút để tìm ra.
Brandan

6

Để có một UPSERT thuần túy không có lỗ (dành cho lập trình viên) không chuyển tiếp trên các khóa duy nhất và các khóa khác:

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

SELECT thay đổi () sẽ trả về số lượng cập nhật được thực hiện trong yêu cầu cuối cùng. Sau đó kiểm tra xem giá trị trả về từ các thay đổi () có phải là 0 hay không, nếu có thì thực hiện:

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

Điều này tương đương với những gì @fiznool đề xuất trong bình luận của anh ấy (mặc dù tôi sẽ đi tìm giải pháp của anh ấy). Tất cả đều ổn và thực sự hoạt động tốt, nhưng bạn không có một câu lệnh SQL duy nhất. UPSERT không dựa trên PK hoặc các khóa duy nhất khác không có ý nghĩa gì đối với tôi.
bgusach

4

Bạn cũng có thể chỉ cần thêm mệnh đề BẬT MẶC BẰNG THAY THẾ vào ràng buộc duy nhất user_name của bạn và sau đó chỉ cần CHÈN, để nó cho SQLite để tìm ra những gì cần làm trong trường hợp xung đột. Xem: https://sqlite.org/lang_conflict.html .

Cũng lưu ý câu liên quan đến trình kích hoạt xóa: Khi chiến lược giải quyết xung đột REPLACE xóa các hàng để thỏa mãn một ràng buộc, xóa trình kích hoạt sẽ kích hoạt nếu và chỉ khi trình kích hoạt đệ quy được bật.


1

Tùy chọn 1: Chèn -> Cập nhật

Nếu bạn muốn tránh cả hai changes()=0INSERT OR IGNOREngay cả khi bạn không đủ khả năng xóa hàng - Bạn có thể sử dụng logic này;

Đầu tiên, chèn (nếu không tồn tại) và sau đó cập nhật bằng cách lọc với khóa duy nhất.

Thí dụ

-- 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';

Về trình kích hoạt

Lưu ý: Tôi chưa kiểm tra nó để xem trình kích hoạt nào đang được gọi, nhưng tôi giả sử như sau:

nếu hàng không tồn tại

  • CHÈN TRƯỚC
  • CHÈN bằng INSTEAD OF
  • SAU KHI CHÈN
  • CẬP NHẬT TRƯỚC
  • CẬP NHẬT bằng INSTEAD OF
  • SAU KHI CẬP NHẬT

nếu hàng tồn tại

  • CẬP NHẬT TRƯỚC
  • CẬP NHẬT bằng INSTEAD OF
  • SAU KHI CẬP NHẬT

Tùy chọn 2: Chèn hoặc thay thế - giữ ID của riêng bạn

bằng cách này, bạn có thể có một lệnh SQL duy nhất

-- 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);

Chỉnh sửa: thêm tùy chọn 2.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.