数据库批量操作哪种方法更快?

在工作中经常遇到批量新增数据到数据库的业务,我们都知道大数据量操作时,批量操作会带来性能的提升,但选择哪种批量操作会更快呢?带着疑问,我从以下两种方案入手,探究数据库批量操作的性能问题。

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;