Tuyệt kỹ "Upsert" trong MySQL: Cứu tinh ON DUPLICATE KEY UPDATE cho những đêm đồng bộ dữ liệu
Chào anh em Viblo, lại là mình đây!
Ở bài viết trước về quy trình ETL, mình có nhắc nhẹ đến một kỹ thuật mang tên Upsert (Update or Insert) ở bước Load dữ liệu. Hôm nay, để gỡ rối cho những anh em đang ngày đêm phải đối mặt với các bài toán đồng bộ dữ liệu (Sync Data), mình sẽ đi sâu vào "vũ khí tối thượng" của MySQL để xử lý bài toán này: Mệnh đề ON DUPLICATE KEY UPDATE.
Nếu anh em từng viết những đoạn code dài dằng dặc chỉ để kiểm tra xem một bản ghi đã tồn tại chưa trước khi lưu, thì bài viết này chắc chắn sẽ tiết kiệm cho anh em vài chục dòng code và giải quyết dứt điểm những con bug "Race Condition" cực kỳ khó chịu.
1. Nỗi đau mang tên "Kiểm tra trước khi thêm"
Hãy tưởng tượng một bài toán cực kỳ kinh điển trong các hệ thống thương mại điện tử: Đồng bộ tồn kho (Stock). Mỗi 5 phút, hệ thống WMS (Quản lý kho) sẽ đẩy về cho Backend của bạn một mảng dữ liệu báo cáo số lượng tồn kho mới nhất của các sản phẩm.
Phản xạ tự nhiên của một lập trình viên mới vào nghề (và cả mình ngày xưa) sẽ là viết logic bằng code PHP/Node.js như sau:
- Dùng lệnh SELECT để kiểm tra xem sản phẩm đó (product_id) đã có trong bảng inventory chưa.
- Nếu CÓ (đã tồn tại): Chạy lệnh UPDATE để cập nhật số lượng tồn kho mới.
- Nếu KHÔNG (chưa tồn tại): Chạy lệnh INSERT để tạo dòng mới.
Tại sao cách này lại "kém sang" và nguy hiểm?
- Chậm chạp: Để xử lý 1.000 sản phẩm, bạn phải gọi ít nhất 2.000 câu query (1 Select + 1 Insert/Update cho mỗi sản phẩm). Tốc độ I/O Network sẽ kéo sập hiệu năng.
- Race Condition (Xung đột đồng thời): Giữa cái khoảnh khắc bạn SELECT thấy chưa có dữ liệu, và cái lúc bạn chuẩn bị INSERT, lỡ như có một tiến trình (job/worker) khác cũng vừa nhanh tay INSERT sản phẩm đó vào thì sao? Câu lệnh INSERT của bạn sẽ chết đứng và ném ra lỗi Duplicate entry vì vi phạm khóa chính.
2. Sự xuất hiện của "Vị cứu tinh"
Cộng đồng CSDL gọi thao tác "Nếu chưa có thì Thêm, có rồi thì Sửa" bằng một từ ghép là Upsert. Và trong MySQL, cách chuẩn mực nhất để làm Upsert chính là thêm đuôi ON DUPLICATE KEY UPDATE vào ngay sau câu INSERT thông thường.
Cú pháp trông như thế này:
INSERT INTO inventory (product_id, stock_quantity, updated_at)
VALUES (101, 50, '2026-07-17 14:00:00')
ON DUPLICATE KEY UPDATE
stock_quantity = VALUES(stock_quantity),
updated_at = VALUES(updated_at);
Sự vi diệu đằng sau câu lệnh này: Thay vì Application phải tự đi kiểm tra, chúng ta đẩy trách nhiệm đó xuống cho SQL Engine. Khi chạy câu lệnh trên, MySQL sẽ cố gắng INSERT dòng dữ liệu vào bảng. Nếu êm xuôi (chưa tồn tại), nó thêm mới bình thường. Nhưng nếu MySQL phát hiện ra việc thêm mới này gây vi phạm Khóa chính (Primary Key) hoặc Khóa định danh (Unique Key) (ví dụ product_id 101 đã có người xí chỗ), nó sẽ lập tức "quay xe" và chuyển sang thực thi vế UPDATE ở đằng sau.
Hàm VALUES(tên_cột) (hoặc ở các phiên bản MySQL 8.0.20 trở lên, người ta hay dùng bí danh alias) giúp bạn lấy chính cái giá trị mà bạn định INSERT ban đầu để dùng cho việc UPDATE. Chỉ 1 câu query duy nhất, database tự lo liệu mọi xung đột!
3. Sức mạnh thực sự: Kết hợp với Bulk Insert
Sức mạnh của kỹ thuật này sẽ nhân lên gấp 100 lần khi bạn kết hợp nó với chèn dữ liệu hàng loạt (Bulk Insert). Quay lại bài toán đồng bộ kho, thay vì chạy vòng lặp, mình có thể nhét cả 1.000 sản phẩm vào MỘT câu query duy nhất:
INSERT INTO inventory (product_id, stock_quantity)
VALUES
(101, 50),
(102, 10),
(103, 0),
(104, 150)
ON DUPLICATE KEY UPDATE
stock_quantity = VALUES(stock_quantity);
Tốc độ xử lý lúc này không còn tính bằng giây nữa, mà tính bằng mili-giây. Dữ liệu dù lộn xộn, dù trùng lặp bao nhiêu đi chăng nữa cũng sẽ được "ủi phẳng" một cách hoàn hảo.
4. Phân biệt kẻ mạo danh: REPLACE INTO
Nhiều anh em khi tìm hiểu về Upsert trong MySQL sẽ bắt gặp một câu lệnh khác có vẻ ngắn gọn hơn rất nhiều: REPLACE INTO.
REPLACE INTO inventory (product_id, stock_quantity) VALUES (101, 50);
Trông có vẻ nguy hiểm và ngầu, nhưng TUYỆT ĐỐI HẠN CHẾ SỬ DỤNG NÓ!
Lý do là vì cơ chế hoạt động của REPLACE INTO cực kỳ "cục súc": Nếu nó phát hiện trùng khóa, nó sẽ XÓA (DELETE) dòng dữ liệu cũ đi, và CHÈN (INSERT) một dòng mới toanh vào.
Hậu quả là gì?
- Nếu bảng của bạn có khóa chính là AUTO_INCREMENT, cái ID của dòng dữ liệu sẽ bị thay đổi liên tục mỗi khi cập nhật.
- Nếu bảng này có liên kết Khóa ngoại (Foreign Key) với các bảng khác với tùy chọn ON DELETE CASCADE, thì ôi thôi... toàn bộ dữ liệu liên quan ở các bảng con sẽ bị xóa sạch sẽ theo một cách không thể cứu vãn.
Trong khi đó, ON DUPLICATE KEY UPDATE giữ nguyên dòng dữ liệu cũ, không làm đổi ID, không kích hoạt trigger xóa, nó chỉ âm thầm "sửa" lại đúng những cột mà bạn chỉ định.
Lời kết
Trong hành trình xây dựng các hệ thống Backend, việc nắm rõ những câu lệnh SQL đặc thù như ON DUPLICATE KEY UPDATE chính là thứ phân biệt giữa một hệ thống "chạy được" và một hệ thống "chạy mượt". Nó giúp code của bạn dọn dẹp sạch sẽ những khối if-else vô hồn, giải quyết tận gốc vấn đề đồng bộ, và quan trọng nhất là bảo vệ tính toàn vẹn của dữ liệu trước mọi sự cố.
All rights reserved