UPSERT nghĩa là "insert, hoặc update nếu nó đã tồn tại." PostgreSQL triển khai điều này với INSERT ... ON CONFLICT — insert một row, nhưng nếu nó vi phạm một unique constraint, thì thay vào đó update row đang tồn tại (hoặc không làm gì). Nó xử lý nhu cầu "insert-or-update" phổ biến một cách atomic.
Vấn đề mà UPSERT giải quyết
Bạn muốn insert một row, nhưng nó có thể đã tồn tại (theo một unique key):
❌ một INSERT thông thường sẽ thất bại với lỗi unique-violation nếu nó tồn tại
❌ check-then-insert (SELECT, rồi INSERT hoặc UPDATE) có RACE CONDITION
(một transaction khác có thể insert vào giữa lúc bạn check và insert)
→ ON CONFLICT thực hiện nó một cách atomic trong MỘT statement (không có race condition)
ON CONFLICT DO UPDATE — insert hoặc update
INSERT INTO users (email, name, login_count)
VALUES ('[email protected]', 'Ann', 1)
ON CONFLICT (email) -- nếu một row với email này đã tồn tại...
DO UPDATE SET -- ...thì UPDATE nó thay vào đó
name = EXCLUDED.name, -- EXCLUDED = các giá trị bạn đã cố insert
login_count = users.login_count + 1; -- cũng có thể tham chiếu row đang tồn tại
Nếu việc insert sẽ xung đột trên unique constraint email, Postgres update row đang tồn tại thay vào đó. EXCLUDED tham chiếu đến các giá trị bạn đã cố insert, và bạn cũng có thể tham chiếu row đang tồn tại (users.login_count) — cho phép logic như tăng một counter.
ON CONFLICT DO NOTHING — insert hoặc bỏ qua
INSERT INTO users (email, name) VALUES ('[email protected]', 'Ann')
ON CONFLICT (email) DO NOTHING; -- nếu nó tồn tại, chỉ bỏ qua (không có lỗi)
DO NOTHING âm thầm bỏ qua các row bị xung đột — hữu ích cho "insert nếu chưa có" mà không gây lỗi.
Một ứng dụng thực tế phổ biến: counter và idempotent insert
-- theo dõi lượt xem trang — insert ở lần xem đầu, tăng ở các lần sau
INSERT INTO page_views (page_id, views) VALUES (5, 1)
ON CONFLICT (page_id) DO UPDATE SET views = page_views.views + 1;
Tại sao điều này quan trọng
UPSERT (INSERT ... ON CONFLICT) là một tính năng PostgreSQL quan trọng và có giá trị thực tiễn, giải quyết nhu cầu "insert-or-update" rất phổ biến một cách gọn gàng và an toàn, nên việc hiểu nó hữu ích cho các thao tác dữ liệu thực tế.
Nhu cầu này — insert một row có thể đã tồn tại (theo một unique key) — phát sinh liên tục (upsert các bản ghi, counter, idempotent insert, đồng bộ dữ liệu), và các giải pháp thay thế ngây thơ đều có vấn đề: một INSERT thông thường thất bại với lỗi nếu row tồn tại, còn cách check-then-insert (SELECT rồi INSERT-or-UPDATE) có race condition (một transaction đồng thời khác có thể insert vào giữa lúc bạn check và insert của bạn, gây ra lỗi hoặc mất cập nhật). ON CONFLICT giải quyết điều này một cách atomic trong một statement duy nhất — loại bỏ race condition, đây là lợi ích về tính đúng đắn then chốt của nó cho các ứng dụng đồng thời.
Hiểu cả hai dạng — DO UPDATE (insert hoặc update, với EXCLUDED tham chiếu các giá trị đã cố insert và khả năng tham chiếu row đang tồn tại cho logic như tăng counter) và DO NOTHING (insert hoặc âm thầm bỏ qua) — bao quát các pattern phổ biến (upsert bản ghi, idempotent insert, duy trì counter/aggregate).
Vì insert-or-update là một thao tác thường xuyên và thực hiện nó đúng đắn và an toàn (atomic, không có race condition) là quan trọng cho tính toàn vẹn dữ liệu trong các ứng dụng đồng thời, nên hiểu UPSERT — nó giải quyết gì, lợi ích về atomicity/race-condition, và các dạng DO UPDATE/DO NOTHING — là kiến thức PostgreSQL có giá trị, được áp dụng thường xuyên, cung cấp một giải pháp sạch sẽ và an toàn cho một nhu cầu phổ biến mà nếu không thì dễ gây lỗi, khiến nó trở thành một tính năng thực tiễn và quan trọng cho các ứng dụng thực.
