在工作中经常遇到批量新增数据到数据库的业务,我们都知道大数据量操作时,批量操作会带来性能的提升,但选择哪种批量操作会更快呢?带着疑问,我从以下两种方案入手,探究数据库批量操作的性能问题。
1、使用
PreparedStatement
预编译SQL
功能,循环操作2、生成批量插入
SQL
,单次操作注:以下为了简便,使用方案一和方案二代替描述
本地数据库,规避网络IO耗时问题。
测试数据量
10000条用户模拟数据
方案一优于方案二
@Transactional(rollbackFor = Exception.class) public void foreachInsert() { long startTime = System.currentTimeMillis(); SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); UserMapper mapper = sqlSession.getMapper(UserMapper.class); for (User user : users) { mapper.save(user); sqlSession.commit(); long endTime = System.currentTimeMillis(); log.info("单条插入总耗时 {} ms", (endTime - startTime));<insert id="save"> insert into user(`name`, age, email) values(#{name},#{age},#{email}) </insert>
解析:该方案使用
PreparedStatement
的预编译功能,可以减少SQL
处理时间,包括SQL
语句的解析、语法检查以及生成代码等操作。一般来说,处理时间要比执行SQL
的时间长,从而提高效率。long startTime = System.currentTimeMillis(); userMapper.saveBatch(users); long endTime = System.currentTimeMillis(); log.info("批量插入总耗时 {} ms", (endTime - startTime));
<insert id="saveBatch"> insert into user(`name`, age, email) values <foreach collection="users" item="user" separator=","> (#{user.name},#{user.age},#{user.email}) </foreach> </insert>
解析:该方案使用整合
SQL
的,达到批量处理的目的,减少了网络IO次数,提升了性能。但本例在本地数据库测试,这里不探讨网络IO影响。2022-09-21 20:36:07.429 INFO 12484 --- [nio-8080-exec-2] c.c.m.service.impl.UserServiceImpl : 单条插入总耗时 311 ms 2022-09-21 20:36:11.657 INFO 12484 --- [nio-8080-exec-1] c.c.m.service.impl.UserServiceImpl : 批量插入总耗时 410 ms 2022-09-21 20:37:30.958 INFO 11844 --- [nio-8080-exec-2] c.c.m.service.impl.UserServiceImpl : 单条插入总耗时 310 ms 2022-09-21 20:37:32.406 INFO 11844 --- [nio-8080-exec-1] c.c.m.service.impl.UserServiceImpl : 批量插入总耗时 418 ms
由此可以看出测试结果也符合预期。
这里需要注意:数据库连接需要多配置一个参数
rewriteBatchedStatements=true
,没有配置的话测试结果将不满足预期。因为MySQL
连接驱动默认情况下会将我们方案一中SQL
一条条发给数据库,批量插入变为单条插入。在不考虑网络IO的情况下,使用
PreparedStatement
预编译SQL
功能,循环操作 快于 生成批量插入SQL
,单次操作。
Mybatis Plus
批量操作方法saveBatch
是如何操作的性能如何?我们看看源码:public static <E> boolean executeBatch(Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) { Assert.isFalse(batchSize < 1, "batchSize must not be less than one"); return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, sqlSession -> { int size = list.size(); int idxLimit = Math.min(batchSize, size); int i = 1; for (E element : list) { consumer.accept(sqlSession, element); if (i == idxLimit) { sqlSession.flushStatements(); idxLimit = Math.min(idxLimit + batchSize, size);
解析:先通过数据分片(防止数据超出
MySQL
限制同时在解析长SQL
时也会非常耗时),默认每次1000条数据,然后将数据一条条插入,这里sqlSession
我们继续进入方法executeBatch(Class<?> entityClass, Log log, Consumer<SqlSession> consumer)
方法SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); try { consumer.accept(sqlSession); sqlSession.commit(!transaction); return true;