Chuyện gì đã xảy ra
Đang thực hiện batch INSERT — script migration, import dữ liệu, query thủ công — và MySQL báo lỗi này:
ERROR 1062 (23000): Duplicate entry '42' for key 'PRIMARY'
Con số trong dấu ngoặc kép là ID đã tồn tại trong bảng. MySQL từ chối chèn một hàng có khóa chính đã bị chiếm, và rollback hàng đó (hoặc toàn bộ transaction nếu bạn đang trong một transaction).
Ba tình huống gây ra lỗi này hầu như mọi lúc. Thứ nhất, bạn đang chèn các hàng với giá trị ID tường minh va chạm với dữ liệu hiện có. Thứ hai, bộ đếm AUTO_INCREMENT bị lệch — thường xảy ra sau khi khôi phục cơ sở dữ liệu hoặc chỉnh sửa ID thủ công. Thứ ba, bạn đang chạy script chèn dữ liệu mà không kiểm tra xem các hàng đó đã tồn tại chưa.
Quá trình debug
1. Tìm ID nào đang xung đột
Thông báo lỗi cho bạn biết trực tiếp — con số trong dấu ngoặc kép là giá trị bị trùng lặp. Đối với bulk insert khi bạn muốn xem toàn cảnh:
-- Kiểm tra những gì đã có trong bảng tại ID đó
SELECT id FROM your_table WHERE id = 42;
-- Kiểm tra giá trị AUTO_INCREMENT hiện tại
SHOW TABLE STATUS LIKE 'your_table'\G
Nhìn vào Auto_increment trong kết quả đầu ra. Nếu nó thấp hơn ID lớn nhất hiện có, đó chính là vấn đề của bạn.
2. Tìm ID lớn nhất thực tế trong bảng
SELECT MAX(id) FROM your_table;
So sánh điều này với giá trị Auto_increment ở trên. Giả sử MAX(id) là 500 nhưng Auto_increment hiển thị 100 — MySQL sẽ bắt đầu gán ID từ 100 và ngay lập tức va chạm với các hàng đã tồn tại.
3. Kiểm tra xem bạn có đang chèn ID tường minh không
Nếu INSERT của bạn trông như thế này:
INSERT INTO users (id, name, email) VALUES (42, 'Alice', 'alice@example.com');
Bạn đang chỉ định ID thủ công. Hàng 42 đã tồn tại? Nó sẽ thất bại. Điều này thường gặp trong script migration và import dữ liệu khi ID đến thẳng từ cơ sở dữ liệu nguồn.
Giải pháp
Cách 1: Đặt lại AUTO_INCREMENT về giá trị đúng
Bạn đã khôi phục cơ sở dữ liệu, truncate và seed lại, hoặc thay đổi ID thủ công? Bộ đếm auto_increment có thể đã lỗi thời.
-- Đặt giá trị tường minh
ALTER TABLE your_table AUTO_INCREMENT = 501;
-- Hoặc để MySQL tự tính:
ALTER TABLE your_table AUTO_INCREMENT = 1;
-- MySQL sẽ đặt thành MAX(id)+1 khi giá trị bạn cung cấp quá thấp
Sau đó, các lần chèn mới không có ID tường minh sẽ bắt đầu từ giá trị an toàn.
Cách 2: Dùng INSERT IGNORE để bỏ qua bản ghi trùng lặp
Đang chạy script có thể chèn lại các hàng đã tồn tại và bạn chỉ muốn bỏ qua chúng một cách im lặng:
INSERT IGNORE INTO users (id, name, email)
VALUES (42, 'Alice', 'alice@example.com');
Các hàng trùng lặp bị bỏ qua một cách im lặng. Không có lỗi, không có rollback. Chỉ dùng cách này khi việc bỏ qua là hành vi đúng — không phải khi bạn muốn cập nhật các hàng đã tồn tại.
Cách 3: Dùng INSERT ... ON DUPLICATE KEY UPDATE
Cần cập nhật hàng đã tồn tại nếu nó đã có? Đó là mẫu upsert:
INSERT INTO users (id, name, email)
VALUES (42, 'Alice', 'alice@example.com')
ON DUPLICATE KEY UPDATE
name = VALUES(name),
email = VALUES(email);
Đối với công việc đồng bộ và script idempotent, hãy biến đây thành mặc định của bạn. Một query xử lý cả hàng mới lẫn cập nhật — không cần logic điều kiện.
Cách 4: Dùng REPLACE INTO (cẩn thận)
REPLACE INTO xóa hàng xung đột trước, sau đó chèn hàng mới. Nó hoạt động, nhưng lưu ý: nó đặt lại bất kỳ cột nào không có trong INSERT của bạn, kích hoạt DELETE triggers, và có thể thay đổi ID hàng nội bộ trong các trường hợp đặc biệt.
REPLACE INTO users (id, name, email)
VALUES (42, 'Alice', 'alice@example.com');
Chỉ dùng cách này khi bạn chắc chắn về các tác dụng phụ đó.
Cách 5: Bỏ ID tường minh khỏi INSERT
Không thực sự cần giá trị ID cụ thể? Chỉ cần xóa id khỏi INSERT và để MySQL xử lý việc gán:
-- Trước đây (thất bại nếu id=42 đã tồn tại)
INSERT INTO users (id, name, email) VALUES (42, 'Alice', 'alice@example.com');
-- Sau đây (MySQL gán id tiếp theo có sẵn)
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
Kiểm tra bản sửa lỗi
-- 1. Xác nhận AUTO_INCREMENT hợp lý
SHOW TABLE STATUS LIKE 'your_table'\G
-- Auto_increment phải là MAX(id) + 1 hoặc cao hơn
-- 2. Chèn một hàng kiểm tra không có ID tường minh
INSERT INTO users (name, email) VALUES ('Test', 'test@example.com');
SELECT LAST_INSERT_ID();
-- Phải trả về ID mới cao hơn max hiện tại
-- 3. Chạy lại query gốc bị thất bại
-- Bây giờ phải thành công
Ngăn lỗi này trong các migration tương lai
Xây dựng những thói quen này và bạn sẽ hiếm khi gặp lại lỗi này:
- Sau khi khôi phục database dump, chạy
ALTER TABLE t AUTO_INCREMENT = 1trên mọi bảng auto-increment. MySQL tự động sửa thành max+1. - Trong script migration chèn dữ liệu seed với ID tường minh, thêm guard:
INSERT INTO ... SELECT ... WHERE NOT EXISTS (...). - Đối với công việc đồng bộ kéo dữ liệu từ nguồn bên ngoài, mặc định dùng
ON DUPLICATE KEY UPDATE— nó bao gồm cả hàng mới và cập nhật trong một query duy nhất. - Đang dùng ORM? Đảm bảo nó không chèn lại các đối tượng đã được lưu. Kiểm tra xem entity đã có ID được gán chưa trước khi gọi save hoặc create.
Tham khảo nhanh
-- Tìm hàng xung đột
SELECT * FROM your_table WHERE id = <duplicate_value>;
-- Kiểm tra và sửa AUTO_INCREMENT
SHOW TABLE STATUS LIKE 'your_table'\G
ALTER TABLE your_table AUTO_INCREMENT = 1;
-- Bỏ qua bản trùng lặp khi bulk insert
INSERT IGNORE INTO ...
-- Mẫu upsert
INSERT INTO ... ON DUPLICATE KEY UPDATE ...;

