Boost MyBatis Batch Insert Speed: Avoid foreach Pitfalls and Use ExecutorType.BATCH

This article explains why MyBatis foreach‑based batch inserts become extremely slow with thousands of rows, demonstrates how the generated giant SQL statement hurts performance, and shows how switching to ExecutorType.BATCH or limiting rows per INSERT dramatically reduces insertion time.

macrozheng
macrozheng
macrozheng
Boost MyBatis Batch Insert Speed: Avoid foreach Pitfalls and Use ExecutorType.BATCH

Recently a job in a project suffered high CPU usage because batch inserting data with MyBatis took a long time. The mapper configuration used a <foreach> loop, similar to the following demo:

<insert id="batchInsert" parameterType="java.util.List">
    insert into USER (id, name) values
    <foreach collection="list" item="model" index="index" separator=",">
        (#{model.id}, #{model.name})
    </foreach>
</insert>

The idea behind this method is to replace many individual INSERT INTO `table1` ... VALUES (...); statements with a single INSERT that contains multiple VALUES clauses, as recommended by MySQL documentation to reduce round‑trips and defer index updates.

However, when the table has many columns (20+) and the batch contains thousands of rows (5000+), the generated SQL becomes huge and the insert takes up to 14 minutes.

Of course don't combine ALL of them, if the amount is HUGE. Say you have 1000 rows you need to insert, then don't do it one at a time. You shouldn't equally try to have all 1000 rows in a single query. Instead break it into smaller sizes.

The <foreach> approach creates a single massive SQL statement rather than a true batch, leading to several drawbacks:

Some databases (e.g., Oracle) do not support such statements.

The number of parameters can exceed the database limit (around 2000 by default), causing errors.

Instead of iterating in the MyBatis XML, execute a simple insert inside a Java for loop and set the session executor to ExecutorType.BATCH:

SqlSession session = sessionFactory.openSession(ExecutorType.BATCH);
for (Model model : list) {
    session.insert("insertStatement", model);
}
session.flushStatements();

With the default ExecutorType.SIMPLE, a new PreparedStatement is created for each execution, and because the statement contains a <foreach> element it cannot be cached, causing repeated parsing and placeholder mapping that grows exponentially with the number of rows.

Internally, it still generates the same single insert statement with many placeholders as the JDBC code above. MyBatis can cache PreparedStatement , but the presence of &lt;foreach /&gt; prevents caching; the statement varies with parameters, so MyBatis must evaluate the foreach part and parse the SQL each time, which is costly for large statements.

Therefore, if you must use <foreach>, limit the number of VALUES per statement to about 20‑50 rows, which yields the best performance according to the curve shown below.

MyBatis documentation actually recommends a different batch‑insert method using MyBatis Dynamic SQL:

SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
    SimpleTableMapper mapper = session.getMapper(SimpleTableMapper.class);
    List<SimpleTableRecord> records = getRecordsToInsert(); // not shown

    BatchInsert<SimpleTableRecord> batchInsert = insert(records)
            .into(simpleTable)
            .map(id).toProperty("id")
            .map(firstName).toProperty("firstName")
            .map(lastName).toProperty("lastName")
            .map(birthDate).toProperty("birthDate")
            .map(employed).toProperty("employed")
            .map(occupation).toProperty("occupation")
            .build()
            .render(RenderingStrategy.MYBATIS3);

    batchInsert.insertStatements().forEach(mapper::insert);
    session.commit();
} finally {
    session.close();
}

This approach sets the session executor to BATCH and executes many insert statements, similar to the plain JDBC batch example:

Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydb?useUnicode=true&characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true", "root", "root");
connection.setAutoCommit(false);
PreparedStatement ps = connection.prepareStatement("insert into tb_user (name) values(?)");
for (int i = 0; i < stuNum; i++) {
    ps.setString(1, name);
    ps.addBatch();
}
ps.executeBatch();
connection.commit();
connection.close();

Experiments show that using ExecutorType.BATCH reduces the total insertion time to under 2 seconds.

Conclusion: For MyBatis batch inserts, prefer the ExecutorType.BATCH approach; if you must use <foreach>, keep each batch to roughly 20‑50 rows to achieve optimal performance.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaMyBatisBatch InsertSQL PerformanceExecutorType.BATCH
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.