Tìm và xoá dữ liệu trùng mà không xoá nhầm bản gốc

Trung cấp 5 phút đọc Cập nhật 30/07/2026 UPA AI Partner

Bảng có 4 dòng, 2 khách. Xoá sai một câu lệnh là mất luôn cả hai. Đây là cách xoá an toàn.

Bài toán

Bảng nguoi_dung có email trùng do nhập từ nhiều nguồn. Cần biết email nào trùng, và xoá bớt chỉ giữ lại một bản.

NGUOI_DUNGidemail1an@x.com2binh@x.com3an@x.com4an@x.comBẢN TRÙNGemailso_lanid_giuan@x.com31Giữ id nhỏ nhất, xoá 3 và 4GROUP BY + HAVING

Bước 1 — tìm giá trị trùng

SELECT email, COUNT(*) AS so_lan
FROM nguoi_dung
GROUP BY email
HAVING COUNT(*) > 1;

Đây là mẫu cố định: gom theo cột cần kiểm tra, lọc nhóm có nhiều hơn một dòng.

Bước 2 — xem đầy đủ các dòng trùng

SELECT *
FROM nguoi_dung
WHERE email IN (
  SELECT email FROM nguoi_dung
  GROUP BY email HAVING COUNT(*) > 1
)
ORDER BY email, id;

Bước 3 — xoá, giữ lại một bản

Cách 1: giữ id nhỏ nhất

DELETE FROM nguoi_dung
WHERE id NOT IN (
  SELECT MIN(id) FROM nguoi_dung GROUP BY email
);

Cách 2: dùng ROW_NUMBER — linh hoạt hơn

WITH danh_so AS (
  SELECT id,
    ROW_NUMBER() OVER (
      PARTITION BY email ORDER BY ngay_tao ASC, id ASC
    ) AS rn
  FROM nguoi_dung
)
DELETE FROM nguoi_dung
WHERE id IN (SELECT id FROM danh_so WHERE rn > 1);

Cách này cho phép bạn chọn bản nào được giữ — bản tạo sớm nhất, bản đầy đủ thông tin nhất, tuỳ tiêu chí.

Luôn chạy SELECT trước DELETE Đổi DELETE FROM thành SELECT * FROM, xem kỹ danh sách sắp xoá, rồi mới đổi lại. Xoá xong không có nút hoàn tác.

Lỗi thường gặp

1. So trùng trên dữ liệu chưa chuẩn hoá

'An@X.com ''an@x.com' là hai giá trị khác nhau. Gom theo LOWER(TRIM(email)) mới bắt được đủ.

2. Quên rằng MySQL không cho DELETE từ chính bảng đang SELECT

-- Lỗi 1093 trên MySQL
DELETE FROM nguoi_dung WHERE id NOT IN (SELECT MIN(id) FROM nguoi_dung GROUP BY email);

-- Bọc thêm một tầng để lách
DELETE FROM nguoi_dung WHERE id NOT IN (
  SELECT * FROM (SELECT MIN(id) FROM nguoi_dung GROUP BY email) t
);

3. Ngăn trùng trong tương lai

CREATE UNIQUE INDEX ux_nguoi_dung_email ON nguoi_dung(LOWER(email));

Xoá xong mà không thêm ràng buộc thì tuần sau bạn lại làm lại từ đầu.

Luyện tập

  • LeetCode 182 — Duplicate Emails
  • LeetCode 196 — Delete Duplicate Emails
  • LeetCode 1050 — Actors and Directors Who Cooperated At Least Three Times
  • LeetCode 1729 — Find Followers Count

Tóm lại

  • GROUP BY … HAVING COUNT(*) > 1 là mẫu tìm trùng chuẩn.
  • ROW_NUMBER cho phép chọn bản nào được giữ lại.
  • Luôn chạy SELECT trước khi đổi thành DELETE.
  • Chuẩn hoá chuỗi trước khi so trùng.
  • Thêm UNIQUE INDEX để không phải làm lại.

Bạn vừa học xong tìm và xoá dữ liệu trùng

Sẵn sàng luyện tập chưa?

Làm bài tập tìm và xoá dữ liệu trùng trên dữ liệu thật, chấm điểm ngay khi bạn bấm chạy.

Bắt đầu luyện tập →