以前面试时,有人问我mybatis批量插入数据怎么做?我说用foreach,他说不对。回去查了一下,原来是ExecutorType.BATCH,涨知识了。
今天心血来潮测试了一下,发现好像并不是那么回事…
先说结论:我测试的是,ExecutorType.BATCH远不如foreach速度快!但愿是我代码写错了!
环境准备
建表:
CREATE TABLE `result` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`title` varchar(255) DEFAULT NULL COMMENT '标题',
`content` varchar(255) DEFAULT NULL COMMENT '内容',
`source_url` varchar(255) DEFAULT NULL COMMENT '内容来源',
`img_url` varchar(255) DEFAULT NULL COMMENT '封面图片',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2398003 DEFAULT CHARSET=utf8mb4 COMMENT='结果表';
dao:
@Repository
public interface BaiDuResultDao {
int insert(BaiDuResult record);
int insertBatch(List<BaiDuResult> list);
mapper.xml
<insert id="insert" parameterType="com.meng.entity.BaiDuResult" >
insert into result (id, title, content,
source_url, img_url, create_time,
update_time, del_flag)
values (#{id,jdbcType=BIGINT}, #{title,jdbcType=VARCHAR}, #{content,jdbcType=VARCHAR},
#{sourceUrl,jdbcType=VARCHAR}, #{imgUrl,jdbcType=VARCHAR}, #{createTime,jdbcType=TIMESTAMP},
#{updateTime,jdbcType=TIMESTAMP}, #{delFlag,jdbcType=TINYINT})
</insert>
<insert id="insertBatch" parameterType="com.meng.entity.BaiDuResult" >
insert into result (id, title, content,
source_url, img_url, create_time,
update_time, del_flag)
values
<foreach collection="list" item="result" separator=",">
(
#{result.id,jdbcType=BIGINT}, #{result.title,jdbcType=VARCHAR}, #{result.content,jdbcType=VARCHAR},
#{result.sourceUrl,jdbcType=VARCHAR}, #{result.imgUrl,jdbcType=VARCHAR}, #{result.createTime,jdbcType=TIMESTAMP},
#{result.updateTime,jdbcType=TIMESTAMP}, #{result.delFlag,jdbcType=TINYINT}
)
</foreach>
测试过程
- 先用每次插入单条数据的方式插入10万条数据
@Test
public void test1Insert(){
long begin = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
BaiDuResult result = new BaiDuResult();
result.setTitle("这是标题" + i);
result.setContent("这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容 " + i);
result.setCreateTime(new Date());
result.setUpdateTime(new Date());
dao.insert(result);
}
long end = System.currentTimeMillis();
System.out.println("耗时多少秒(end - begin)/1000 = " + (end - begin) / 1000);
/**
* 耗时多少秒(end - begin)/1000 = 159
*/
}
结果是耗时159秒
- 使用foreach的方式插入
@Test
public void test2ForEachInsert(){
long begin = System.currentTimeMillis();
List<BaiDuResult> list = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
BaiDuResult result = new BaiDuResult();
result.setTitle("这是标题" + i);
result.setContent("这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容 " + i);
result.setCreateTime(new Date());
result.setUpdateTime(new Date());
list.add(result);
if(i % 1000 == 0){
dao.insertBatch(list);
list.clear();
}
}
if(list.size() > 0){
dao.insertBatch(list);
list.clear();
}
long end = System.currentTimeMillis();
System.out.println("耗时多少秒(end - begin)/1000 = " + (end - begin) / 1000);
/**
* 耗时多少秒(end - begin)/1000 = 7 -- 十万数据,7秒
* 耗时多少秒(end - begin)/1000 = 55 -- 百万数据,55秒
*/
}
结果惊人!!! 百万数据插入也只是用了55秒!!!!
注意:foreach插入时,每次执行的数据不能太多,否则报错:
3、使用ExecutorType.BATCH
@Test
public void test4BatchInsert(){
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false);
BaiDuResultDao batchDao = sqlSession.getMapper(BaiDuResultDao.class);
long begin = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
BaiDuResult result = new BaiDuResult();
result.setTitle("这是标题" + i);
result.setContent("这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容这是内容 " + i);
result.setCreateTime(new Date());
result.setUpdateTime(new Date());
batchDao.insert(result);
}
sqlSession.commit();
sqlSession.close();
long end = System.currentTimeMillis();
System.out.println("耗时多少秒(end - begin)/1000 = " + (end - begin) / 1000);
/**
*耗时多少秒(end - begin)/1000 = 151 这种方式不对啊,太慢了
*/
}
结果并不是我认为的那样!!!除非是我的代码写错了!!!
我也怀疑是我的代码写错了,我试着去百度,结果TMD那么多写mybatis批量插入的文章,几乎都是复制粘贴,完全一样!代码甚至都不完整!自己压根就没试过,就把文章复制过来,当成自己的,鲁迅的拿来主义还真是研究的透透的!MD!
有一篇文章倒是和我的测试结果类似 foreach 和 数据库批量执行 效率比较
分析
我尝试着去翻mybatis官网 mybatis官网,发现对于ExecutorType.BATCH的描述是这样的:
这里是引用你可能对 ExecutorType 参数感到陌生。这个枚举类型定义了三个值:
ExecutorType.SIMPLE:该类型的执行器没有特别的行为。它为每个语句的执行创建一个新的预处理语句。
ExecutorType.REUSE:该类型的执行器会复用预处理语句。
ExecutorType.BATCH:该类型的执行器会批量执行所有更新语句,如果 SELECT 在多个更新中间执行,将在必要时将多条更新语句分隔开来,以方便理解。
这里只是说会批量执行语句,但没有说速度快
这种方式是把多条语句一起执行,所以它的速度比你一条一条执行的快(但我的测试并没有快的多明显),而foreach是把多条数据拼成一条sql ,这样的
insert into table (con1,con2…) values (value1 , value2),(value1 , value2),(value1 , value2)…
这也就解释了为什么foeach执行的数据太多报错的问题,因为拼成的sql太长了,所以要控制这个批量执行的数量,找到速度最快的数量配置。
如果使用原生的JDBC批量插入,速度如何?
原生JDBC批量插入
- 一条一条插入
@Test
public void test6JDBCBatchInsertOne() throws Exception{
long begin = System.currentTimeMillis();
Connection connection = DriverManager.getConnection("jdbc:mysql://192.168.233.136:3306/mydata","root","123456");
PreparedStatement ps = connection.prepareStatement(
" insert into result (title, content, create_time, update_time) values (?, ?, ?, ?)");
for (int i = 0; i < 100000; i++) {
ps.setString(1 ,"title" + i);
ps.setString(2 ,"contentcontentcontentcontentcontentcontentcontentcontent " + i);
ps.setDate(3 ,new java.sql.Date(new Date().getTime()));
ps.setDate(4 ,new java.sql.Date(new Date().getTime()));
ps.execute();
}
connection.close();
long end = System.currentTimeMillis();
System.out.println("耗时多少秒(end - begin)/1000 = " + (end - begin) / 1000);
/**
*耗时多少秒(end - begin)/1000 = 158
*/
}
可以看到,这样的一条一条插入,和mybatis的一条一条插入,速度差不多
- 使用addBatch()批量插入
@Test
public void test6JDBCBatchInsert2() throws Exception{
long begin = System.currentTimeMillis();
Connection connection = DriverManager.getConnection("jdbc:mysql://192.168.233.136:3306/mydata","root","123456");
connection.setAutoCommit(false);
PreparedStatement ps = connection.prepareStatement(
" insert into result (title, content, create_time, update_time) values (?, ?, ?, ?)");
for (int i = 0; i < 100000; i++) {
ps.setString(1 ,"title" + i);
ps.setString(2 ,"contentcontentcontentcontentcontentcontentcontentcontent " + i);
ps.setDate(3 ,new java.sql.Date(new Date().getTime()));
ps.setDate(4 ,new java.sql.Date(new Date().getTime()));
ps.addBatch();
if(i%1000 == 0){
ps.executeBatch();
ps.clearBatch();
}
}
connection.commit();
connection.close();
long end = System.currentTimeMillis();
System.out.println("耗时多少秒(end - begin)/1000 = " + (end - begin) / 1000);
/**
*耗时多少秒(end - begin)/1000 = 102
*/
}
可以看到,速度有明显提升,但是和mybatis的foreach比,它就是个弟弟
总结
批量插入速度最快的方式是foreach,但是要控制每次插入的数量,否则就是个坑
以上结论接受反驳,但要拿出实质的证据