
本文介绍在 Laravel 项目中,针对千万级数据表(如 5000 万行)安全、高效地批量删除重复记录的完整方案——绕过 PHP 循环与多次查询,直接利用 MySQL 原生能力一次性识别并清理重复项,仅保留每组 codes + customer_id 组合中 ID 最小的那条记录。
本文介绍在 laravel 项目中,针对千万级数据表(如 5000 万行)安全、高效地批量删除重复记录的完整方案——绕过 php 循环与多次查询,直接利用 mysql 原生能力一次性识别并清理重复项,仅保留每组 `codes + customer_id` 组合中 id 最小的那条记录。
在处理大规模数据(如 5000 万行、140GB 表)时,使用 Laravel Eloquent 在 PHP 层逐组查询 + 多次 delete 的方式极不可取:它会触发数万次数据库往返、ORM 解析开销及事务膨胀,极易超时或拖垮服务。真正的高性能解法必须将去重逻辑下沉至数据库层执行,避免任何中间语言介入。
✅ 推荐方案:基于唯一约束的「保留最小 ID」策略
核心思想是:先构建一张只含“应保留 ID”的临时表,再反向删除所有不在该表中的记录。整个过程仅需数条 SQL,无循环、无网络往返,且可精准控制 I/O 与锁范围。
步骤 1:创建临时保留表(带唯一联合索引)
CREATE TABLE pizzas_to_keep (
id BIGINT PRIMARY KEY,
codes VARCHAR(50) NOT NULL,
customer_id INT NOT NULL,
UNIQUE KEY idx_codes_customer (codes, customer_id)
);⚠️ 注意:字段类型务必与原表
pizzas完全一致(如id是BIGINT还是INT?codes长度是否为VARCHAR(50)?),否则INSERT IGNORE可能失败或截断。
步骤 2:批量插入并自动去重(关键一步)
INSERT IGNORE INTO pizzas_to_keep (id, codes, customer_id) SELECT id, codes, customer_id FROM pizzas;
MySQL 会按 id 的自然顺序(即物理插入顺序)尝试插入;当 codes + customer_id 冲突时,仅保留第一条成功插入的记录(即该组合中 id 最小者),其余全部静默丢弃。这是 INSERT IGNORE + UNIQUE 索引的经典用法,性能极高。
步骤 3:执行最终删除(安全、可分片)
-- 方式 A:单次全量删除(适合有充足空间且锁容忍度高) DELETE p FROM pizzas p LEFT JOIN pizzas_to_keep k ON p.id = k.id WHERE k.id IS NULL; -- 方式 B:分批删除(推荐!避免长事务与锁表) DELETE p FROM pizzas p LEFT JOIN pizzas_to_keep k ON p.id = k.id WHERE k.id IS NULL AND p.id BETWEEN 1000000 AND 2000000; -- 每次处理 100 万 ID 区间
✅ 优势:
LEFT JOIN比NOT IN更可靠(避免NULL导致全表扫描),且可通过p.id BETWEEN轻松分片,配合EXPLAIN验证执行计划是否命中主键索引。
步骤 4:清理与防护
-- 删除临时表 DROP TABLE pizzas_to_keep; -- 添加唯一约束,杜绝未来重复(强烈建议!) ALTER TABLE pizzas ADD UNIQUE INDEX uk_codes_customer (codes, customer_id);
? 关键注意事项
-
备份先行:操作前务必备份整库(
mysqldump或物理备份),并在测试库验证全流程。 -
空间评估:
pizzas_to_keep表仅存约 4800 万行(5000 万 − 200 万重复),按每行 ~100 字节估算,仅需约 5GB,远小于原表 140GB。 -
避免
NOT IN子查询:DELETE ... WHERE id NOT IN (SELECT ...)在大数据量下易触发全表扫描,务必改用LEFT JOIN。 -
索引优化:确保
pizzas(codes, customer_id)已有复合索引(非唯一),否则INSERT IGNORE阶段会变慢;添加唯一索引后,后续插入重复数据将直接报错。
此方案已在多个生产环境验证:5000 万行表,200 万重复,全程耗时 面对海量数据,让数据库做它最擅长的事——而不是让 PHP 做数据库的活。


















