Menu

UPSERT trong SQLite: ON CONFLICT DO UPDATE và DO NOTHING

Cách UPSERT hoạt động trong SQLite: mệnh đề ON CONFLICT, bảng excluded, DO NOTHING so với DO UPDATE, và khác biệt với INSERT OR REPLACE.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Chèn, hoặc cập nhật nếu đã có

Một nhu cầu phổ biến: chèn một hàng, nhưng nếu đã có hàng mang cùng khóa thì cập nhật hàng đó. Không có UPSERT, bạn sẽ phải viết một lệnh SELECT trước, rồi rẽ nhánh sang INSERT hoặc UPDATE: hai lần gọi tới cơ sở dữ liệu và một lỗi tranh chấp (race condition) nằm giữa chúng.

UPSERT của SQLite làm việc này trong một câu lệnh:

Lần chạy đầu tiên, hàng được chèn vào. Chạy lại với giá khác và cùng sku, hàng hiện có được cập nhật tại chỗ. Không trùng lặp, không lỗi.

Cấu trúc của ON CONFLICT

Dạng đầy đủ:

INSERT INTO table (...) VALUES (...)
ON CONFLICT(conflict_target) DO UPDATE SET col = expr, ...
WHERE condition;

Có ba phần quan trọng:

  • conflict_target: cột hoặc các cột có ràng buộc UNIQUE hay PRIMARY KEY mà bạn dự đoán sẽ bị xung đột. SQLite dùng nó để chọn index cần theo dõi.
  • DO UPDATE SET ...: thay đổi gì trên hàng hiện có khi xảy ra xung đột. (Hoặc DO NOTHING để bỏ qua âm thầm.)
  • WHERE tùy chọn: điều kiện thêm phải đúng thì lệnh update mới thực sự chạy.

Conflict target phải khớp với một ràng buộc duy nhất có thật. ON CONFLICT(price) sẽ không biên dịch được nếu price không phải duy nhất, vì SQLite không có gì để phát hiện xung đột.

DO NOTHING: chèn nếu chưa có, nếu có thì bỏ qua

Biến thể đơn giản hơn. Hữu ích khi bạn nạp dữ liệu mẫu hoặc ghi lại sự kiện và các bản trùng lặp chỉ cần được bỏ qua lặng lẽ:

Lệnh insert thứ hai trùng event_id và bình thường sẽ báo lỗi UNIQUE constraint failed. Với DO NOTHING, SQLite chỉ đơn giản bỏ qua nó. Không ngoại lệ, không hàng nào bị ảnh hưởng.

Đây là kiểu "insert lặp lại an toàn" mà người ta thường dùng INSERT OR IGNORE để làm. DO NOTHING của UPSERT làm cùng việc đó và kết hợp tốt hơn với các mệnh đề WHERE và RETURNING.

Bảng giả excluded

Khi xung đột xảy ra, bỗng nhiên có hai hàng cùng lúc: hàng hiện có trong bảng, và hàng mới bạn đã cố chèn. SQLite cho bạn cách nói về cả hai.

  • Tên cột trơn (price, name) trỏ tới hàng hiện có.
  • excluded.column trỏ tới hàng đi vào đã bị từ chối.

quantity = quantity + excluded.quantity đọc là "số lượng hiện có cộng với số lượng mới". Sau hai lần insert, A-100 có số lượng 8. Mẫu này, cộng dồn vào một hàng có sẵn, là một trong những mẹo hữu ích nhất của UPSERT.

UPSERT có điều kiện với WHERE

WHERE ở cuối cho phép bỏ qua lệnh update trừ khi một điều kiện nào đó đúng. Nó được tính trên hàng hiện có (và có thể tham chiếu excluded.* cho hàng đi vào):

Hàng mới mang updated_at cũ hơn, nên WHERE sai và lệnh update bị bỏ qua. Hàng hiện có giữ nguyên mức giá mới hơn. Đổi chỗ hai ngày và lệnh update sẽ chạy. Đây là mẫu chuẩn "chỉ ghi đè bằng dữ liệu mới hơn".

Upsert nhiều hàng

VALUES có thể chứa nhiều hàng, và ON CONFLICT áp dụng độc lập cho từng hàng:

A-100 bị xung đột và được cập nhật. A-200 và A-300 là hàng mới và được chèn vào. Một câu lệnh, kết quả vừa chèn vừa cập nhật. Đây là cách gọn gàng để đồng bộ một lô bản ghi từ nguồn bên ngoài.

UPSERT so với INSERT OR REPLACE

INSERT OR REPLACE trông như làm cùng một việc. Thực ra không phải.

notes đã mất. INSERT OR REPLACE xóa hẳn hàng 1 rồi chèn một hàng mới: mọi cột bạn không liệt kê đều bị đặt về NULL hoặc giá trị mặc định. Nó còn kích hoạt trigger DELETE và lan dây chuyền qua các khóa ngoại có ON DELETE.

UPSERT giữ nguyên hàng:

notes vẫn còn đó. Chỉ những cột ghi trong SET mới thay đổi. Hãy mặc định dùng UPSERT; chỉ dùng INSERT OR REPLACE khi bạn thực sự muốn ngữ nghĩa xóa rồi chèn lại.

Nhiều conflict target

Nếu một hàng có thể xung đột trên nhiều ràng buộc, bạn có thể nối nhiều mệnh đề ON CONFLICT:

Ràng buộc nào bị vi phạm trước sẽ thắng, và nhánh DO UPDATE tương ứng sẽ chạy. Trong thực tế, phần lớn bảng chỉ có một conflict target rõ ràng, là khóa chính hoặc một cột duy nhất, và bạn hiếm khi cần nhiều hơn một mệnh đề.

Những lỗi thường gặp

Vài điều hay làm người ta vấp:

  • Không có unique index phù hợp thì không có UPSERT. ON CONFLICT(col) yêu cầu col là PRIMARY KEY hoặc có ràng buộc UNIQUE. Nếu không, SQLite báo lỗi "no such constraint".
  • DO UPDATE không chạy nếu không có xung đột. Nó là phương án thay thế cho lệnh insert, không phải hành vi bổ sung. Lần đầu một khóa xuất hiện, chỉ lệnh insert chạy.
  • excluded chỉ đọc được. Bạn có thể đọc từ nó nhưng không ghi vào được. Đích của SET luôn là hàng hiện có.
  • Rowid INTEGER PRIMARY KEY tự sinh. Nếu bạn không truyền id, mỗi lần insert sẽ nhận một id mới, nên chẳng có gì để xung đột. UPSERT chỉ có ý nghĩa khi cột xung đột có giá trị xác định do nơi gọi truyền vào.

Tiếp theo: RETURNING

UPSERT không cho bạn biết hàng nào được chèn và hàng nào được cập nhật, hay giá trị cuối cùng của chúng ra sao. Để biết điều đó, bạn cần mệnh đề RETURNING: nó trả về các hàng bị ảnh hưởng ngay trong cùng câu lệnh, không cần thêm một lệnh SELECT. Đó là trang tiếp theo.

Câu hỏi thường gặp

UPSERT trong SQLite là gì?

UPSERT là một lệnh INSERT chuyển thành UPDATE (hoặc không làm gì) khi nó lẽ ra sẽ vi phạm một ràng buộc UNIQUE hoặc PRIMARY KEY. Bạn viết nó dưới dạng INSERT ... ON CONFLICT(column) DO UPDATE SET ... hoặc DO NOTHING. SQLite hỗ trợ cú pháp này từ phiên bản 3.24.0 (năm 2018).

Bảng excluded trong UPSERT của SQLite là gì?

excluded là một bảng giả đặc biệt chứa hàng mà bạn đã cố chèn vào. Bên trong DO UPDATE SET ..., bạn tham chiếu hàng hiện có bằng tên cột, và hàng bị từ chối bằng excluded.column. Vì vậy SET price = excluded.price nghĩa là 'ghi đè price bằng giá trị mà lệnh INSERT mới mang tới'.

INSERT OR REPLACE và UPSERT khác nhau thế nào?

INSERT OR REPLACE xóa hàng bị xung đột rồi chèn một hàng hoàn toàn mới: việc này kích hoạt trigger DELETE, làm đổ dây chuyền các khóa ngoại có ON DELETE CASCADE, và đặt mọi cột về giá trị mặc định. UPSERT cập nhật hàng hiện có tại chỗ, nên chỉ những cột bạn ghi trong SET mới thay đổi. Hãy ưu tiên UPSERT, trừ khi bạn thực sự muốn xóa rồi chèn lại.

Có upsert nhiều hàng cùng lúc trong SQLite được không?

Được. INSERT INTO t(...) VALUES (...), (...), (...) ON CONFLICT(col) DO UPDATE SET ... chạy bình thường. Mỗi hàng được kiểm tra riêng với conflict target, và hàng excluded bên trong DO UPDATE trỏ tới đúng hàng đi vào đã gây ra xung đột.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu