删除 MySQL 表上的重复项是一个常见问题,这通常是由于缺少约束以事先避免这些重复项的结果。但是这个常见的问题通常伴随着特定的需求......确实需要特定的方法。方法应该有所不同,具体取决于例如数据的大小、应该保留的重复条目(通常是第一个或最后一个)、是否有要保留的索引,或者我们是否要执行任何额外的操作对重复数据采取行动。
MySQL 本身也有一些特殊性,例如在执行表 UPDATE 时由于 FROM 原因无法引用同一个表(它会引发 MySQL 错误 #1093)。可以通过使用带有临时表的内部查询来克服此限制(如上面某些方法所建议的那样)。但这种内部查询在处理大数据源时表现不佳。
但是,确实存在一种更好的方法来删除重复项,它既高效又可靠,并且可以轻松适应不同的需求。
一般的想法是创建一个新的临时表,通常添加一个唯一约束以避免进一步重复,并将以前表中的数据插入到新表中,同时注意重复。这种方法依赖于简单的 MySQL INSERT 查询,创建一个新的约束以避免进一步的重复,并且跳过了使用内部查询来搜索重复项和应该保存在内存中的临时表的需要(因此也适合大数据源)。
这就是它的实现方式。假设我们有一个 employee 表,其中包含以下列:
employee (id, first_name, last_name, start_date, ssn)
为了删除具有重复ssn列的行,只保留找到的第一个条目,可以遵循以下过程:
-- create a new tmp_eployee table
CREATE TABLE tmp_employee LIKE employee;
-- add a unique constraint
ALTER TABLE tmp_employee ADD UNIQUE(ssn);
-- scan over the employee table to insert employee entries
INSERT IGNORE INTO tmp_employee SELECT * FROM employee ORDER BY id;
-- rename tables
RENAME TABLE employee TO backup_employee, tmp_employee TO employee;
技术说明
- 第 1 行创建一个新的 tmp_eployee 表,其结构与 employee 表完全相同
- 第 2 行向新的 tmp_eployee 表添加 UNIQUE 约束以避免任何进一步的重复
- 第 3 行按 id 扫描原始 employee 表,将新员工条目插入新 tmp_eployee 表,同时忽略重复条目
- 第 4 行重命名表,以便新的 employee 表包含所有不重复的条目,并且以前数据的备份副本保存在 backup_employee表
⇒ 使用这种方法,160 万个寄存器在不到 200 秒的时间内转换为 6k 个。
Chetan,按照这个过程,您可以快速轻松地删除所有重复项并通过运行创建一个 UNIQUE 约束:
CREATE TABLE tmp_jobs LIKE jobs;
ALTER TABLE tmp_jobs ADD UNIQUE(site_id, title, company);
INSERT IGNORE INTO tmp_jobs SELECT * FROM jobs ORDER BY id;
RENAME TABLE jobs TO backup_jobs, tmp_jobs TO jobs;
当然,这个过程可以进一步修改,以适应删除重复项时的不同需求。下面是一些例子。
✔ 保留最后一个条目而不是第一个条目的变化
有时我们需要保留最后一个重复的条目而不是第一个。
CREATE TABLE tmp_employee LIKE employee;
ALTER TABLE tmp_employee ADD UNIQUE(ssn);
INSERT IGNORE INTO tmp_employee SELECT * FROM employee ORDER BY id DESC;
RENAME TABLE employee TO backup_employee, tmp_employee TO employee;
- 在第 3 行,ORDER BY id DESC 子句使最后一个 ID 优先于其余 ID
✔ 对重复项执行某些任务的变体,例如对找到的重复项进行计数
有时我们需要对找到的重复条目进行一些进一步的处理(例如保持重复计数)。
CREATE TABLE tmp_employee LIKE employee;
ALTER TABLE tmp_employee ADD UNIQUE(ssn);
ALTER TABLE tmp_employee ADD COLUMN n_duplicates INT DEFAULT 0;
INSERT INTO tmp_employee SELECT * FROM employee ORDER BY id ON DUPLICATE KEY UPDATE n_duplicates=n_duplicates+1;
RENAME TABLE employee TO backup_employee, tmp_employee TO employee;
- 在第 3 行,创建了一个新列 n_duplicates
- 在第 4 行,INSERT INTO ... ON DUPLICATE KEY UPDATE 查询用于在发现重复项时执行附加更新(在本例中,增加计数器)
INSERT INTO ... ON DUPLICATE KEY UPDATE 查询可用于对找到的重复项执行不同类型的更新。
✔ 重新生成自动增量字段 id 的变化
有时我们使用自增字段,为了使索引尽可能紧凑,我们可以利用删除重复项在新临时表中重新生成自增字段。
CREATE TABLE tmp_employee LIKE employee;
ALTER TABLE tmp_employee ADD UNIQUE(ssn);
INSERT IGNORE INTO tmp_employee SELECT (first_name, last_name, start_date, ssn) FROM employee ORDER BY id;
RENAME TABLE employee TO backup_employee, tmp_employee TO employee;
- 在第 3 行,不是选择表中的所有字段,而是跳过 id 字段,以便数据库引擎自动生成一个新字段
✔ 更多变化
根据所需的行为,还可以进行许多进一步的修改。例如,以下查询将使用第二个临时表,除了 1) 保留最后一个条目而不是第一个条目; 2) 增加对找到的重复项的计数器;也 3) 重新生成自动增量字段 id,同时保持输入顺序与以前的数据相同。
CREATE TABLE tmp_employee LIKE employee;
ALTER TABLE tmp_employee ADD UNIQUE(ssn);
ALTER TABLE tmp_employee ADD COLUMN n_duplicates INT DEFAULT 0;
INSERT INTO tmp_employee SELECT * FROM employee ORDER BY id DESC ON DUPLICATE KEY UPDATE n_duplicates=n_duplicates+1;
CREATE TABLE tmp_employee2 LIKE tmp_employee;
INSERT INTO tmp_employee2 SELECT (first_name, last_name, start_date, ssn) FROM tmp_employee ORDER BY id;
DROP TABLE tmp_employee;
RENAME TABLE employee TO backup_employee, tmp_employee2 TO employee;