Boost MyBatis Batch Inserts: Why foreach Slows You Down and How to Fix It

This article explains why using MyBatis foreach for bulk inserts can cause severe performance degradation, demonstrates the underlying parsing overhead, and shows how switching to ExecutorType.BATCH or limiting batch sizes to 20‑50 rows dramatically speeds up insert operations.

Architect's Tech Stack
Architect's Tech Stack
Architect's Tech Stack
Boost MyBatis Batch Inserts: Why foreach Slows You Down and How to Fix It

Recently a job in a project suffered high CPU usage because the MyBatis foreach batch insert generated an extremely long SQL statement, causing severe performance issues.

<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 traditional approach of issuing many separate INSERT statements can be replaced by a single INSERT with multiple VALUES, which reduces round‑trips and defers index updates.

INSERT INTO table1 (field1, field2) VALUES
  ("data1","data2"),
  ("data1","data2"),
  ("data1","data2"),
  ("data1","data2");

MySQL documentation confirms that combining many small operations into one large statement improves insert speed, but the statement size must be kept reasonable.

When the foreach generates 5000+ VALUES, the resulting PreparedStatement becomes huge, leading to exponential growth in parsing time and memory consumption.

Performance curve showing optimal batch size
Performance curve showing optimal batch size

Therefore, if you must use foreach, keep each INSERT statement to about 20‑50 rows to stay near the performance curve’s bottom.

The recommended MyBatis way is to use a batch executor:

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

Or using MyBatis Dynamic SQL’s batch insert support:

SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
    SimpleTableMapper mapper = session.getMapper(SimpleTableMapper.class);
    List<SimpleTableRecord> records = getRecordsToInsert();
    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();
}

A plain JDBC batch example also works:

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();

Testing showed that using ExecutorType.BATCH reduces the total insert time to under 2 seconds for thousands of rows.

In summary, for MyBatis bulk inserts, prefer ExecutorType.BATCH or limit each batch to 20‑50 rows; avoid large foreach‑generated statements.

JavaMyBatisBatch InsertExecutorType.BATCH
Architect's Tech Stack
Written by

Architect's Tech Stack

Java backend, microservices, distributed systems, containerized programming, and more.

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.