Xử lý Duplicate Rows trong MySQL: 3 Tuyệt Chiêu Từ ‘Mì Ăn Liền’ Đến ‘Hạng Nặng’

MySQL tutorial - IT technology blog
MySQL tutorial - IT technology blog

Dữ liệu trùng lặp: Khi Database bỗng dưng “đẻ” thêm rác

Làm việc với database đủ lâu, bạn chắc chắn sẽ gặp tình cảnh trớ trêu: một bảng dữ liệu đáng lẽ mỗi hàng phải là duy nhất nhưng lại xuất hiện những bản ghi giống hệt nhau. Lỗi này thường đến từ code logic phía ứng dụng, thiếu ràng buộc UNIQUE, hoặc sơ suất khi import hàng triệu dòng từ file CSV/Excel.

Hồi mới vào nghề, mình từng gặp một phen hú vía với hệ thống thanh toán. Do thiếu check trùng ở tầng database, một lỗi mạng khiến lệnh insert chạy lặp lại, làm 5.200 khách hàng bị trừ tiền hai lần. Sáng hôm đó, mình đổ mồ hôi hột ngồi rà soát đống dữ liệu thừa để hoàn tiền. Bài học xương máu này khiến mình luôn ám ảnh với tính toàn vẹn dữ liệu và luôn thủ sẵn các kịch bản “dọn rác” định kỳ.

Dưới đây là 3 cách thực tế nhất để bạn quét sạch đống dữ liệu thừa thãi mà không làm treo hệ thống.

Nên chọn phương pháp nào để xóa trùng?

Thực tế không có cách nào là tốt nhất cho mọi trường hợp. Việc lựa chọn tùy thuộc vào phiên bản MySQL bạn đang chạy và quy mô bảng dữ liệu (vài nghìn hay vài chục triệu records).

  • Cách 1: CTE (Common Table Expressions) & ROW_NUMBER(): Hiện đại, dễ đọc, cực kỳ tường minh. Yêu cầu MySQL 8.0+.
  • Cách 2: Self-Join: Kiểu truyền thống nhưng cực kỳ lợi hại. Chạy tốt trên mọi phiên bản từ 5.6 trở lên.
  • Cách 3: Bảng tạm (Temporary Table): “Ăn chắc mặc bền”. Phù hợp cho bảng khổng lồ trên 10GB để tránh lock bảng quá lâu.

Phân tích ưu và nhược điểm kỹ thuật

1. Common Table Expressions (CTE)

Điểm cộng: Code sạch sẽ. Bạn có thể SELECT thử để xem những dòng sắp bị xóa trước khi bấm nút chạy thật.

Điểm trừ: Chỉ hỗ trợ từ MySQL 8.0. Nếu dự án của bạn vẫn đang chạy bản 5.7 thì cách này không hoạt động.

2. Self-Join (Tự liên kết bảng)

Điểm cộng: Không kén phiên bản. Hiệu suất xử lý rất nhanh nếu bạn đã đánh Index cho các cột so sánh.

Điểm trừ: Chỉ cần sai một dấu so sánh (dùng > thay vì <), bạn có thể xóa nhầm bản ghi gốc thay vì bản ghi trùng.

3. Temporary Table (Bảng tạm)

Điểm cộng: An toàn tuyệt đối. Bạn copy dữ liệu sạch ra một nơi rồi mới ghi đè lại, giảm thiểu rủi ro chết đứng hệ thống (database contention).

Điểm trừ: Tốn thêm không gian đĩa cứng và mất thời gian copy nếu bảng có hàng chục triệu dòng.

Tư vấn triển khai thực tế

Dựa trên kinh nghiệm của mình:

  • Nếu bảng dưới 500.000 dòng và dùng MySQL 8.0+: Hãy dùng CTE cho nhàn đầu.
  • Dùng Self-Join nếu bạn đang bảo trì các hệ thống legacy (bản cũ).
  • Ưu tiên Temporary Table nếu bảng nặng vài chục GB hoặc đang có hàng nghìn request mỗi giây.

Hướng dẫn triển khai chi tiết

Giả sử bảng customers của bạn bị trùng email. Chúng ta cần giữ lại bản ghi có id nhỏ nhất và xóa các bản ghi “sinh sau đẻ muộn”.

CREATE TABLE customers (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100),
    email VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Bước 1: Nhận diện “kẻ thù”

Đầu tiên, hãy đếm xem có bao nhiêu nhóm dữ liệu đang bị trùng:

SELECT email, COUNT(email) 
FROM customers 
GROUP BY email 
HAVING COUNT(email) > 1;

Bước 2: Xử lý bằng CTE (MySQL 8.0+)

Chúng ta đánh số thứ tự cho từng nhóm email. Dòng đầu tiên nhận số 1, các dòng trùng sau đó sẽ là 2, 3…

WITH cte AS (
    SELECT id, 
           ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) as row_num
    FROM customers
)
DELETE FROM customers 
WHERE id IN (SELECT id FROM cte WHERE row_num > 1);

Bước 3: Xử lý bằng Self-Join (Phổ biến nhất)

Logic ở đây là so bảng customers với chính nó. Chúng ta tìm các dòng có email giống nhau nhưng ID lớn hơn dòng gốc.

DELETE c1 FROM customers c1
INNER JOIN customers c2 
ON c1.email = c2.email 
WHERE c1.id > c2.id;

Mẹo nhỏ: Nếu muốn giữ lại khách hàng mới nhất, bạn chỉ cần đổi thành c1.id < c2.id.

Bước 4: Cách làm “chắc cốp” bằng Temporary Table

Thường dùng khi bảo trì hệ thống vào đêm khuya để đảm bảo không sai sót:

  1. Lọc danh sách ID duy nhất vào bảng tạm:
CREATE TEMPORARY TABLE unique_ids AS
SELECT MIN(id) as id
FROM customers
GROUP BY email;
  1. Xóa sạch những bản ghi không nằm trong danh sách “an toàn” này:
DELETE FROM customers 
WHERE id NOT IN (SELECT id FROM unique_ids);

Lưu ý “xương máu” trước khi thực thi

Đừng bao giờ quá tự tin vào trình độ SQL của mình. Trước khi bấm Enter cho lệnh DELETE, hãy nhớ:

  1. Luôn Backup: Dùng mysqldump để sao lưu bảng. Chỉ mất 2 phút nhưng cứu được cả sự nghiệp nếu lỡ tay.
  2. Chạy thử SELECT: Thay DELETE bằng SELECT * để xem danh sách sắp bị xóa có đúng ý không.
  3. Kiểm tra Index: Đảm bảo cột email đã được đánh Index. Nếu không, câu lệnh xóa bảng lớn sẽ làm treo server, gây gián đoạn dịch vụ.

Hy vọng những chia sẻ này giúp bạn tự tin dọn dẹp database. Nếu gặp lỗi lạ hay có cách nào tối ưu hơn, hãy để lại bình luận phía dưới để chúng ta cùng thảo luận nhé!

Share: