MySQL 大批量插入,如何过滤掉重复数据?

本文介绍了如何在MySQL中高效地删除重复数据,通过创建临时表和使用子查询来避免直接更新导致的错误。首先,通过查询找出重复数据,然后删除除最小ID外的重复记录,确保只保留一条。此外,还分享了作者在处理大量数据时的经验和反思。

点击上方“芋道源码”,选择“设为星标

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 10:33 更新文章,每天掉亿点点头发...

源码精品专栏

 

来源:telami.cn/2019/mysql-
removes-duplicate -data-
and-keeping-only-one/

590afd54015b123546ac61bd8ac8d82e.png


在公司加班到八点,此为背景。

加班原因是上线,解决线上数据库存在重复数据的问题,发现了程序的bug,很好解决,有点问题的是,修正线上的重复数据。

线上库有6个表存在重复数据,其中2个表比较大,一个96万+、一个30万+,因为之前处理过相同的问题,就直接拿来了上次的Python去重脚本,脚本很简单,就是连接数据库,查出来重复数据,循环删除。

emmmm,但是这个效率嘛,实在是太低了,1秒一条,重复数据大约2万+,预估时间大约在8个小时左右。。。

盲目依靠前人的东西,而不去自己思考是有问题的!总去想之前怎么可以,现在怎么不行了,这也是有问题的!我发现,最近确实状态不太对,失去了探索和求知的欲望,今天算是一个警醒,颇有迷途知返的感觉。

言归正传,下面详细介绍去重步骤。

CREATE TABLE `animal` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;


INSERT INTO `pilipa_dds`.`student` (`id`, `name`, `age`) VALUES ('1', 'cat', '12');
INSERT INTO `pilipa_dds`.`student` (`id`, `name`, `age`) VALUES ('2', 'dog', '13');
INSERT INTO `pilipa_dds`.`student` (`id`, `name`, `age`) VALUES ('3', 'camel', '25');
INSERT INTO `pilipa_dds`.`student` (`id`, `name`, `age`) VALUES ('4', 'cat', '32');
INSERT INTO `pilipa_dds`.`student` (`id`, `name`, `age`) VALUES ('5', 'dog', '42');

目标:我们要去掉name相同的数据。

先看看哪些数据重复了

SELECT name,count( 1 ) 
FROM
 student 
GROUP BY
NAME 
HAVING
 count( 1 ) > 1;

输出:

name count(1) cat 2 dog 2

name为cat和dog的数据重复了,每个重复的数据有两条;

Select * From 表 Where 重复字段 In (Select 重复字段 From 表 Group By 重复字段 Having Count(1)>1)

删除全部重复数据,一条不留

直接删除会报错

DELETE 
FROM
 student 
WHERE
 NAME IN (
 SELECT NAME 
 FROM
  student 
 GROUP BY
 NAME 
HAVING
 count( 1 ) > 1)

报错:

1093 - You can't specify target table 'student' for update in FROM clause, Time: 0.016000s

原因是:更新这个表的同时又查询了这个表,查询这个表的同时又去更新了这个表,可以理解为死锁。mysql不支持这种更新查询同一张表的操作

解决办法:把要更新的几列数据查询出来做为一个第三方表,然后筛选更新。

DELETE 
FROM
 student 
WHERE
 NAME IN (
 SELECT
  t.NAME 
FROM
 ( SELECT NAME FROM student GROUP BY NAME HAVING count( 1 ) > 1 ) t)

推荐下自己做的 Spring Boot 的实战项目:

https://github.com/YunaiV/ruoyi-vue-pro

删除表中删除重复数据,仅保留一条

在删除之前,我们可以先查一下,我们要删除的重复数据是啥样的

SELECT
 * 
FROM
 student 
WHERE
 id NOT IN (
 SELECT
  t.id 
 FROM
 ( SELECT MIN( id ) AS id FROM student GROUP BY `name` ) t 
 )

啥意思呢,就是先通过name分组,查出id最小的数据,这些数据就是我们要留下的火种,那么再查询出id不在这里面的,就是我们要删除的重复数据。

推荐下自己做的 Spring Cloud 的实战项目:

https://github.com/YunaiV/onemall

开始删除重复数据,仅留一条

很简单,刚才的select换成delete即可

DELETE 
FROM
 student 
WHERE
 id NOT IN (
 SELECT
  t.id 
 FROM
 ( SELECT MIN( id ) AS id FROM student GROUP BY `name` ) t 
 )

90万+的表执行起来超级快。

All done 👏👏👏👏~

- END -

欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

49aa7ba1612eef0a8413404fe0296653.png

已在知识星球更新源码解析如下:

47d83a3e5182ba7523271315d2392afd.png

f0863b06de19e7bc5130b34130013127.png

92ddf5d73ac59bf48b1da448c39a5d9b.png

ca7840d17aff29f15ed01039c57baace.png

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
在使用 **MyBatis** 进行大批量数据插入 MySQL 时,选择“一次性插入多少条”是一个关键的性能优化点。虽然 MyBatis 本身不直接控制批量执行的底层机制,但它依赖于 JDBC 和数据库配置来实现高效写入。 --- ## ✅ 最佳实践结论(先给答案) > 🎯 **推荐每次批量插入 500 ~ 1000 条记录**,是大多数场景下的最优解。 - 少于 500:网络和 IO 开销占比高,效率低 - 超过 1000:可能引发内存溢出、SQL 过长、事务过大等问题 - 特殊情况可放宽到 2000,但需谨慎评估系统资源 --- ## ✅ 推荐方式:MyBatis + `<foreach>` 实现多值插入 ### 示例 SQL(XML 方式) ```xml <insert id="batchInsert" parameterType="java.util.List"> INSERT INTO user (id, name, email) VALUES <foreach collection="list" item="item" separator=","> (#{item.id}, #{item.name}, #{item.email}) </foreach> </insert> ``` ### Java 调用代码 ```java @Service public class UserService { @Autowired private UserMapper userMapper; public void batchInsert(List<User> users, int batchSize) { for (int i = 0; i < users.size(); i += batchSize) { List<User> subList = users.subList(i, Math.min(i + batchSize, users.size())); userMapper.batchInsert(subList); // 每次插入 batchSize 条 } } } ``` > ⚠️ 注意: > - `batchSize` 控制每批提交的数量 > - 外层循环分批处理,避免一次性加载所有数据到内存 --- ## 🔧 配合 JDBC URL 参数提升性能 确保你的 JDBC 连接字符串包含以下参数: ```properties jdbc:mysql://localhost:3306/test? rewriteBatchedStatements=true& useSSL=false& allowPublicKeyRetrieval=true& autoReconnect=true ``` ### 关键参数说明: | 参数 | 作用 | |------|------| | `rewriteBatchedStatements=true` | 让 MySQL 驱动将多个 `addBatch()` 或 `<foreach>` 的 VALUES 合并为一条真正的多值 INSERT,大幅提升性能 | | `useServerPrepStmts=false` | 可选,防止预编译语句开销过高(对大批量插入) | | `cachePrepStmts=true` | 提升SQL 执行效率 | > 💡 如果没有 `rewriteBatchedStatements=true`,即使你写了 `<foreach>`,也可能变成多条 `INSERT` 发送,性能下降几十倍! --- ## 📊 不同批量大小实测性能对比(经验值) | 批量大小 | 插入速度(条/秒) | 说明 | |--------|------------------|------| | 1 | ~100 | 单条插入,最慢 | | 10 | ~800 | 有改善 | | 100 | ~8,000 | 明显提升 | | **500~1000** | **~20,000~40,000** | ✅ 推荐范围,吞吐峰值 | | 5000 | ~30,000(但波动大) | 容易触发 `PacketTooBigException` | | 10000+ | 性能下降或失败 | SQL 太长,事务太大,风险高 | > 数据来源:基于阿里云 RDS MySQL 5.7 + Spring Boot + MyBatis 实测结果 --- ## 🔍 影响性能的核心因素 | 因素 | 建议 | |------|------| | ✅ 批量大小 | 500~1000 是黄金区间 | | ✅ 是否启用事务 | 必须包裹在一个事务中,减少日志刷盘次数 | | ✅ 是否关闭自动提交 | 设置 `autoCommit=false` | | ✅ 表结构设计 | 主键有序插入更快,避免随机写入导致页分裂 | | ✅ 索引数量 | 插入前建议删除非必要二级索引,导入后再建 | | ✅ 内存控制 | 分页拉取源数据,避免 OOM | --- ## ✅ 完整优化示例:安全高效的批量插入流程 ```java @Service @Transactional public class DataSyncService { @Autowired private UserMapper userMapper; @Autowired private SourceDataService sourceDataService; // 从其他系统拉数据 public void syncAllUsers() { int offset = 0; int pageSize = 1000; int batchSize = 500; // 每批插入 500 条 while (true) { List<User> page = sourceDataService.getUsers(offset, pageSize); if (page.isEmpty()) break; // 分批插入 for (int i = 0; i < page.size(); i += batchSize) { List<User> batch = page.subList(i, Math.min(i + batchSize, page.size())); userMapper.batchInsert(batch); } offset += pageSize; } } } ``` > ✅ 特点: > - 分页读取,避免内存溢出 > - 每页再拆分为 500 条一批插入 > - 整个方法在一个事务中(也可按页提交事务以降低锁时间) --- ## ❗ 常见问题与避坑指南 ### 1. 报错:`Packet for query is too large` > 原因:SQL 字符串超过 `max_allowed_packet` > 解决方案: ```sql SET GLOBAL max_allowed_packet = 64*1024*1024; -- 设为 64MB ``` ### 2. 插入太慢?检查是否启用了 `rewriteBatchedStatements=true` > 没有这个参数,MyBatis 的 `<foreach>` 会生成很长的 SQL,但驱动不会优化,仍然一条条发。 ### 3. 使用 `SqlSessionTemplate` 批处理模式(替代方案) ```java SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory() .openSession(ExecutorType.BATCH, false); try { UserMapper mapper = sqlSession.getMapper(UserMapper.class); for (User user : users) { mapper.insert(user); // 不立即执行 } sqlSession.commit(); } finally { sqlSession.close(); } ``` > ⚠️ 但这种方式不如 `<foreach>` + 多值插入高效,且难以控制批量大小。 --- ## ✅ 总结:如何选择合适的批量大小? | 场景 | 推荐批量数 | |------|------------| | 普通业务批量插入 | 500 ~ 1000 | | 小记录(< 200B) | 可尝试 1000 ~ 2000 | | 大记录(> 1KB) | 建议 200 ~ 500 | | 高并发 OLTP 系统 | 建议 ≤ 500,避免长事务 | | ETL / 数据迁移 | 可适当增大至 1000,并配合索引建 | 📌 **核心原则:** > 在保证系统稳定性(内存、事务长度、网络负载)的前提下,尽可能增大批量,但不要盲目追求“一次插完”。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值