最近新的项目写了不少各种 insertBatch 的代码,一直有人说,批量插入比循环插入效率高很多,那本文就来实验一下,到底是不是真的?

测试环境:

  • SpringBoot 2.5
  • Mysql 8
  • JDK 8
  • Docker

首先,多条数据的插入,可选的方案:

  • foreach循环插入
  • 拼接sql,一次执行
  • 使用批处理功能插入

搭建测试环境`

sql文件:

drop database IF EXISTS test;
CREATE DATABASE test;
use test;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (`id` int(11) NOT NULL,`name` varchar(255) DEFAULT "",`age` int(11) DEFAULT 0,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

应用的配置文件:

server:port: 8081
spring:#数据库连接配置datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&&serverTimezone=UTC&setUnicode=true&characterEncoding=utf8&&nullCatalogMeansCurrent=true&&autoReconnect=true&&allowMultiQueries=trueusername: rootpassword: 123456
#mybatis的相关配置
mybatis:#mapper配置文件mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.aphysia.spingbootdemo.model#开启驼峰命名configuration:map-underscore-to-camel-case: true
logging:level:root: error

启动文件,配置了Mapper文件扫描的路径:

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
@MapperScan("com.aphysia.springdemo.mapper")
public class SpringdemoApplication {public static void main(String[] args) {SpringApplication.run(SpringdemoApplication.class, args);}}

Mapper文件一共准备了几个方法,插入单个对象,删除所有对象,拼接插入多个对象:

import com.aphysia.springdemo.model.User;
import org.apache.ibatis.annotations.Param;import java.util.List;public interface UserMapper {int insertUser(User user);int deleteAllUsers();int insertBatch(@Param("users") List<User>users);
}

Mapper.xml文件如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.aphysia.springdemo.mapper.UserMapper"><insert id="insertUser" parameterType="com.aphysia.springdemo.model.User">insert  into user(id,age) values(#{id},#{age})</insert><delete id="deleteAllUsers">delete from user where id>0;</delete><insert id="insertBatch" parameterType="java.util.List">insert into user(id,age) VALUES<foreach collection="users" item="model" index="index" separator=",">(#{model.id}, #{model.age})</foreach></insert>
</mapper>

测试的时候,每次操作我们都删除掉所有的数据,保证测试的客观,不受之前的数据影响。

不同的测试

1. foreach 插入

先获取列表,然后每一条数据都执行一次数据库操作,插入数据:

@SpringBootTest
@MapperScan("com.aphysia.springdemo.mapper")
class SpringdemoApplicationTests {@AutowiredSqlSessionFactory sqlSessionFactory;@ResourceUserMapper userMapper;static int num = 100000;static int id = 1;@Testvoid insertForEachTest() {List<User> users = getRandomUsers();long start = System.currentTimeMillis();for (int i = 0; i < users.size(); i++) {userMapper.insertUser(users.get(i));}long end = System.currentTimeMillis();System.out.println("time:" + (end - start));}
}

2. 拼接sql插入

其实就是用以下的方式插入数据:

INSERT INTO `user` (`id`, `age`)
VALUES (1, 11),
(2, 12),
(3, 13),
(4, 14),
(5, 15);
    @Testvoid insertSplicingTest() {List<User> users = getRandomUsers();long start = System.currentTimeMillis();userMapper.insertBatch(users);long end = System.currentTimeMillis();System.out.println("time:" + (end - start));}

3. 使用Batch批量插入

MyBatis sessionexecutor type 设为 Batch,使用sqlSessionFactory将执行方式置为批量,自动提交置为false,全部插入之后,再一次性提交:

    @Testpublic void insertBatch(){SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false);UserMapper mapper = sqlSession.getMapper(UserMapper.class);List<User> users = getRandomUsers();long start = System.currentTimeMillis();for(int i=0;i<users.size();i++){mapper.insertUser(users.get(i));}sqlSession.commit();sqlSession.close();long end = System.currentTimeMillis();System.out.println("time:" + (end - start));}

4. 批量处理+分批提交

在批处理的基础上,每1000条数据,先提交一下,也就是分批提交。

    @Testpublic void insertBatchForEachTest(){SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false);UserMapper mapper = sqlSession.getMapper(UserMapper.class);List<User> users = getRandomUsers();long start = System.currentTimeMillis();for(int i=0;i<users.size();i++){mapper.insertUser(users.get(i));if (i % 1000 == 0 || i == num - 1) {sqlSession.commit();sqlSession.clearCache();}}sqlSession.close();long end = System.currentTimeMillis();System.out.println("time:" + (end - start));}

初次结果,明显不对?

运行上面的代码,我们可以得到下面的结果,for循环插入的效率确实很差,拼接的sql效率相对高一点,看到有些资料说拼接sql可能会被mysql限制,但是我执行到1000w的时候,才看到堆内存溢出。

下面是不正确的结果!!!

插入方式 10 100 1000 1w 10w 100w 1000w
for循环插入 387 1150 7907 70026 635984 太久了… 太久了…
拼接sql插入 308 320 392 838 3156 24948 OutOfMemoryError: 堆内存溢出
批处理 392 917 5442 51647 470666 太久了… 太久了…
批处理 + 分批提交 359 893 5275 50270 472462 太久了… 太久了…

拼接sql并没有超过内存

我们看一下mysql的限制:

mysql> show VARIABLES like '%max_allowed_packet%';
+---------------------------+------------+
| Variable_name             | Value      |
+---------------------------+------------+
| max_allowed_packet        | 67108864   |
| mysqlx_max_allowed_packet | 67108864   |
| slave_max_allowed_packet  | 1073741824 |
+---------------------------+------------+
3 rows in set (0.12 sec)

67108864足足600多M,太大了,怪不得不会报错,那我们去改改一下它吧,改完重新测试:

  1. 首先在启动mysql的情况下,进入容器内,也可以直接在Docker桌面版直接点Cli图标进入:
docker exec -it mysql bash
  1. 进入/etc/mysql目录,去修改my.cnf文件:
cd /etc/mysql
  1. 先按照vim,要不编辑不了文件:
apt-get update
apt-get install vim
  1. 修改my.cnf
vim my.cnf
  1. 在最后一行添加max_allowed_packet=20M(按i编辑,编辑完按esc,输入:wq退出)

[mysqld]
pid-file        = /var/run/mysqld/mysqld.pid
socket          = /var/run/mysqld/mysqld.sock
datadir         = /var/lib/mysql
secure-file-priv= NULL
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0# Custom config should go here
!includedir /etc/mysql/conf.d/
max_allowed_packet=2M
  1. 退出容器
# exit
  1. 查看mysql容器id
docker ps -a

  1. 重启mysql
docker restart c178e8998e68

重启成功后查看最大的max_allowed_pactet,发现已经修改成功:

mysql> show VARIABLES like '%max_allowed_packet%';
+---------------------------+------------+
| Variable_name             | Value      |
+---------------------------+------------+
| max_allowed_packet        | 2097152    |
| mysqlx_max_allowed_packet | 67108864   |
| slave_max_allowed_packet  | 1073741824 |
+---------------------------+------------+

我们再次执行拼接sql,发现100w的时候,sql就达到了3.6M左右,超过了我们设置的2M,成功的演示抛出了错误:

org.springframework.dao.TransientDataAccessResourceException:
### Cause: com.mysql.cj.jdbc.exceptions.PacketTooBigException: Packet for query is too large (36,788,583 > 2,097,152). You can change this value on the server by setting the 'max_allowed_packet' variable.
; Packet for query is too large (36,788,583 > 2,097,152). You can change this value on the server by setting the 'max_allowed_packet' variable.; nested exception is com.mysql.cj.jdbc.exceptions.PacketTooBigException: Packet for query is too large (36,788,583 > 2,097,152). You can change this value on the server by setting the 'max_allowed_packet' variable.

批量处理为什么这么慢?

但是,仔细一看就会发现,上面的方式,怎么批处理的时候,并没有展示出优势了,和for循环没有什么区别?这是对的么?

这肯定是不对的,从官方文档中,我们可以看到它会批量更新,不会每次去创建预处理语句,理论是更快的。

然后我发现我的一个最重要的问题:数据库连接 URL地址少了rewriteBatchedStatements=true

如果我们不写,MySQL JDBC 驱动在默认情况下会忽视 executeBatch() 语句,我们期望批量执行的一组 sql 语句拆散,但是执行的时候是一条一条地发给 MySQL 数据库,实际上是单条插入,直接造成较低的性能。我说怎么性能和循环去插入数据差不多。

只有将 rewriteBatchedStatements 参数置为 true, 数据库驱动才会帮我们批量执行 SQL

正确的数据库连接:

jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&&serverTimezone=UTC&setUnicode=true&characterEncoding=utf8&&nullCatalogMeansCurrent=true&&autoReconnect=true&&allowMultiQueries=true&&&rewriteBatchedStatements=true

找到问题之后,我们重新测试批量测试,最终的结果如下:

插入方式 10 100 1000 1w 10w 100w 1000w
for循环插入 387 1150 7907 70026 635984 太久了… 太久了…
拼接sql插入 308 320 392 838 3156 24948(很可能超过sql长度限制) OutOfMemoryError: 堆内存溢出
批处理(重点) 333 323 362 636 1638 8978 OutOfMemoryError: 堆内存溢出
批处理 + 分批提交 359 313 394 630 2907 18631 OutOfMemoryError: 堆内存溢出

从上面的结果来看,确实批处理是要快很多的,当数量级太大的时候,其实都会超过内存溢出的,批处理加上分批提交并没有变快,和批处理差不多,反而变慢了,提交太多次了,拼接sql的方案在数量比较少的时候其实和批处理相差不大,最差的方案就是for循环插入数据,这真的特别的耗时。100条的时候就已经需要1s了,不能选择这种方案。

一开始发现批处理比较慢的时候,真的挺怀疑自己,后面发现是有一个参数,有一种拨开云雾的感觉,知道得越多,不知道的越多。

【作者简介】
秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。

剑指Offer全部题解PDF

2020年我写了什么?

开源编程笔记

数据库批量插入这么讲究的么?相关推荐

  1. 数据库批量插入和存在的问题

    不同数据库批量插入方式效率也迥异,使用不同数据批量插入方式也存在一些需要注意.容易引起错误的点. 用于实验的数据和表 create DATABASE if exists batch_insert_pe ...

  2. mybatis操作Oracle数据库批量插入与更新、运行注意事项、属性含义

    一.项目需求 针对将近300万用户的用电数据进行统计分析,将结果更新保存Oracle数据库.我需要往一个表里面插入数据,数据量总计在500万条左右.一条一条插入的话非常慢,2万条数据近20分钟,后面就 ...

  3. Jmeter向数据库批量插入数据

    Jmeter向数据库批量插入数据 下面介绍一下Jmeter向mysql数据库中插入数据的入门操作 1.新建一个线程组,这是必经步骤: 在测试计划上右键–>添加–>Theaders(User ...

  4. 往Oracel数据库批量插入数据

    往Oracel数据库批量插入数据到目前为止,据我所知,有两种: 第一种,在cmd进入imp命令所在的目录,然后使用imp命令,这种方式批量导入数据速度很快,导入过程会有进程提示,如遇错误,也会告知,如 ...

  5. sql数据库批量插入

    代码如下: /// <summary>/// 批量插入(datatable插入数据库)/// </summary>/// <param name="dtNumI ...

  6. 数据库批量插入数据的三种方法

    一.准备工作 测试环境:SpringBoot项目+MybatisPlus框架+MySQL数据库+Lombok 二.导入依赖 <dependency><groupId>org.s ...

  7. 数据库批量插入Insert ALL into报错ORA-00933:SQL命令未正确结束

    起因 : 批量插入数据库表报错 代码演示 : @Insert("<script> Insert ALL into table(字段1,字段2) values<foreach ...

  8. sqlserver数据库批量插入-SqlBulkCopy

    当想在数据库中插入大量数据时,使用insert 不仅效率低,而且会导致一系列的数据库性能问题 当使用insert语句进行插入数据时.我使用了两种方式: 每次插入数据时,都只插入一条数据库,这个会导致每 ...

  9. python往数据库批量插入多条数据

    1.需要定义构造函数(init),给实例属性赋值(包括host.port.user.password和database) 2.连接数据库 3.插入数据,先在数据库中建好表,并且添加好所需要的字段 4. ...

最新文章

  1. hls fifo_【FCCM2020】HLS 高手对比 Verilog 高手,到底输哪了?
  2. 图解在8086模拟器中运行汇编helloworld程序
  3. 【频谱共享】基于认知无线电的VCG拍卖机制频谱共享算法的MATLAB仿真
  4. Spring mvc整合freemarker详解
  5. 斜杠青年Ruff:区块链只是分内事
  6. 如果波音公司破产,对美国经济的影响有多大?
  7. Python-OpenCV 处理图像(一):基本操作 cv2
  8. 信安考友分享:软考信息安全工程师备考四年,终成正果
  9. html的canvas显示数字,HTML5效果:Canvas 实现圆形进度条并显示数字百分比
  10. 交通部 城轨交通运营管理规定_韩国又松大学热门专业-轨道交通运营与管理专业介绍...
  11. oracle数据库卸载过程,oracle数据库卸载教程,Oracle卸载步骤有哪几步?
  12. 一个小小的笔顺程序,按钮太多,暂时实现了两个
  13. 东北人讲java_东北人,请你讲东北话(内附东北话速成)
  14. edge linux 下载软件,微软Edge浏览器Linux开发版
  15. 前端一键换肤换肤简单探索
  16. CentOS 7.6的64位安装JAVA JDK
  17. python办公自动化之word表格跨页断行-AllowBreakAcrossPages
  18. 常用政务网络查询单打印地址
  19. No executable code found at line。。。
  20. 内存地址重映射的选项_内存控制设置请教

热门文章

  1. html覆盖标签,覆盖内部html标签内容的CSS
  2. Docker搭建Nginx+PHP部署TP6
  3. java事务传播机制事例_Spring事务传播机制
  4. Invalid bound statement (not found): com.xxx.xxx.mapper.xxxMapper.xxx
  5. 数据治理 Python桑基图处理表关系
  6. 巴蜀2904 MMT数
  7. C语言笔试一些简单容易错的题目
  8. VMware不同处理器、硬盘设置——性能对比
  9. ai里怎么做阴影效果_不同场景里的阴影怎么绘制?3种阴影表现方法帮你解惑
  10. 2019年中国展览业发展前景展望