Back to Batch Processing
Q2Question 2 of 2

What are JDBC batch insert configuration settings?

Key Concepts
  • hibernate.jdbc.batch_size groups N INSERT/UPDATE statements into a single JDBC batch, cutting round trips by up to Nx.
  • hibernate.order_inserts reorders queued inserts so same-table statements are adjacent, which is required for JDBC batching.
  • hibernate.order_updates does the same reordering for UPDATE statements.
  • GenerationType.IDENTITY forces Hibernate to execute each INSERT immediately to read the auto-generated key, silently disabling batching.
  • GenerationType.SEQUENCE lets Hibernate pre-fetch ids and defer inserts, enabling efficient JDBC batching.
  • On PostgreSQL, adding reWriteBatchedInserts=true to the JDBC URL rewrites batched inserts into multi-row INSERT statements.
  • Use hibernate.generate_statistics=true to verify that batches are actually forming.

Definition

`batch_size` groups statements per round trip; `order_inserts` makes same-table statements batch together; IDENTITY ids force immediate inserts and kill batching, so use SEQUENCE.
  • hibernate.jdbc.batch_size -- the number of statements Hibernate accumulates before sending them as one JDBC batch via addBatch()/executeBatch().
  • hibernate.order_inserts -- reorders pending inserts so statements targeting the same table are adjacent, which is required for them to batch.
  • hibernate.order_updates -- same reordering for UPDATE statements.
  • GenerationType.IDENTITY -- auto-increment; forces immediate INSERT to read the generated key, silently disabling batching.
  • GenerationType.SEQUENCE -- pre-fetches ids from a database sequence; inserts can be deferred and batched.
  • allocationSize -- how many ids Hibernate grabs per sequence call; match to batch_size to minimize sequence round trips.

Sending one statement per round trip is like a courier at National Bank of India driving to the branch and back for every single document. JDBC batching is loading fifty documents into the van and making one trip. The batch size is how many documents fit in the van. Ordering inserts is sorting the documents so all the ones for the same department are bundled together. IDENTITY is a rule that every document must be hand-stamped at the counter the moment it arrives, so the courier can never load a full van.

Understanding the Concepts

What is JDBC batching and why does it matter?

Without batching, Hibernate sends each INSERT as its own JDBC execute, so inserting 10,000 rows means 10,000 network round trips. With hibernate.jdbc.batch_size = 50, Hibernate accumulates up to 50 statements and sends them with JDBC's batch API, cutting round trips by up to 50x.

YAML
# application.yml
spring:
  jpa:
    properties:
      hibernate:
        jdbc.batch_size: 50
TEXT
Without batching (10,000 inserts):
  10,000 round trips x 1ms each = ~10 seconds

With batch_size=50:
  200 batches x 1ms each = ~0.2 seconds (50x faster)

Why is order_inserts required for batching to work?

Batching only works when consecutive statements use the same prepared statement. If your action queue interleaves inserts into different tables, JDBC cannot batch them. order_inserts reorders the queue so all inserts for one table are grouped.

YAML
spring:
  jpa:
    properties:
      hibernate:
        order_inserts: true
        order_updates: true
JAVA
package com.bank.service;

import com.bank.model.Account;
import com.bank.model.Holding;
import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;

@Service
public class InterleavedInsertDemo {

    private final EntityManager em;

    public InterleavedInsertDemo(EntityManager em) {
        this.em = em;
    }

    @Transactional
    public void createPortfolioWithHoldings() {
        // Without order_inserts, queue is:
        // INSERT account, INSERT holding, INSERT account, INSERT holding
        // JDBC cannot batch because the prepared statement keeps changing

        // With order_inserts, queue becomes:
        // INSERT account, INSERT account, INSERT holding, INSERT holding
        // Each group batches together!

        for (int i = 0; i < 50; i++) {
            em.persist(new Account("SAV" + i, "Customer " + i, BigDecimal.ZERO));
            em.persist(new Holding("SYM" + i, 100, new BigDecimal("1000.00")));
        }
        em.flush();
    }
}
TEXT
Without order_inserts:
  Account INSERT, Holding INSERT, Account INSERT, Holding INSERT...
  Each switch breaks the batch = 100 individual round trips

With order_inserts:
  50 Account INSERTs batched, then 50 Holding INSERTs batched
  = 2 round trips

Why does IDENTITY generation kill batching?

With GenerationType.IDENTITY, the primary key is generated by the database via auto-increment. Hibernate must execute each INSERT immediately to read the generated key back. Since inserts cannot be deferred, they cannot be batched.

JAVA
// IDENTITY: batching silently disabled
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// persist(entity) -> INSERT runs NOW -> id read back
// Cannot defer -> cannot batch

// SEQUENCE: batching works
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "g")
@SequenceGenerator(name = "g", sequenceName = "account_seq", allocationSize = 50)
private Long id;
// persist(entity) -> id pre-fetched from sequence -> INSERT deferred
// 50 inserts batched into one round trip
TEXT
IDENTITY (50 inserts):
  persist -> INSERT -> read id -> persist -> INSERT -> read id ...
  = 50 round trips, batch_size setting IGNORED

SEQUENCE (50 inserts, allocationSize=50):
  1 sequence call -> 50 ids pre-fetched
  50 persists -> INSERTs deferred
  flush -> 50 inserts in 1 JDBC batch
  = 2 round trips total

How do all the settings work together?

The complete configuration for efficient batch inserts combines SEQUENCE generation, batch_size, order_inserts, and the flush+clear pattern.

JAVA
package com.bank.model;

import jakarta.persistence.*;
import java.math.BigDecimal;

@Entity
@Table(name = "accounts")
public class Account {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "acct_gen")
    @SequenceGenerator(name = "acct_gen", sequenceName = "account_seq",
                       allocationSize = 50)
    private Long id;
    private String holderName;
    private BigDecimal balance;
}
YAML
spring:
  jpa:
    properties:
      hibernate:
        jdbc.batch_size: 50
        order_inserts: true
        order_updates: true
        generate_statistics: true
  datasource:
    url: jdbc:postgresql://localhost:5432/bankdb?reWriteBatchedInserts=true
JAVA
package com.bank.batch;

import com.bank.model.Account;
import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;

@Service
public class EfficientBatchInsert {

    private static final int BATCH = 50;
    private final EntityManager em;

    public EfficientBatchInsert(EntityManager em) {
        this.em = em;
    }

    @Transactional
    public void importAccounts(int count) {
        for (int i = 0; i < count; i++) {
            em.persist(new Account("Customer " + i, BigDecimal.ZERO));
            if ((i + 1) % BATCH == 0) {
                em.flush();
                em.clear();
            }
        }
        em.flush();
    }
}
TEXT
Complete pipeline for 10,000 inserts:
1. SEQUENCE: 200 calls (50 ids each) = 200 round trips for ids
2. flush every 50: 200 JDBC batches of 50 inserts = 200 round trips
3. Total: ~400 round trips instead of 10,000
4. With reWriteBatchedInserts: each batch becomes a multi-row INSERT
5. Memory: bounded at 50 entities (flush+clear)

How do you verify batching is working?

Enable Hibernate statistics to see actual batch counts. If the statistics show N individual executions instead of N/batch_size batches, something is wrong.

YAML
spring:
  jpa:
    properties:
      hibernate:
        generate_statistics: true
TEXT
Expected statistics output (10,000 inserts, batch_size=50):
  HHH000117: 200 JDBC batches executed
  statements executed: 200 (not 10,000)

If you see:
  statements executed: 10,000
  -> Batching is NOT working!
  -> Check: IDENTITY ids? Missing order_inserts? batch_size not set?

What about PostgreSQL reWriteBatchedInserts?

On PostgreSQL, adding reWriteBatchedInserts=true to the JDBC URL tells the driver to rewrite a JDBC batch of individual INSERT statements into a single multi-row INSERT, which is significantly faster.

TEXT
JDBC URL:
jdbc:postgresql://localhost:5432/bankdb?reWriteBatchedInserts=true

Without rewrite (batch of 50):
  INSERT INTO accounts VALUES (1, 'A', 0);
  INSERT INTO accounts VALUES (2, 'B', 0);
  ... 48 more ...
  (50 statements in one batch)

With rewrite (batch of 50):
  INSERT INTO accounts VALUES (1,'A',0),(2,'B',0),...,(50,'Z',0);
  (one multi-row INSERT -- even faster)

What are common mistakes with batch insert configuration?

The most common mistake is configuring batch_size but using IDENTITY generation, which silently disables batching.

JAVA
// WRONG: batch_size set but IDENTITY kills it
// hibernate.jdbc.batch_size=50 -- IGNORED!
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

// WRONG: batch_size set but order_inserts missing
// Interleaved tables break batching even with batch_size

// WRONG: allocationSize=1 on sequence
@SequenceGenerator(name="g", sequenceName="seq", allocationSize=1)
// 10,000 inserts = 10,000 sequence calls!

// CORRECT: all settings aligned
@SequenceGenerator(name="g", sequenceName="seq", allocationSize=50)
// hibernate.jdbc.batch_size=50
// hibernate.order_inserts=true
TEXT
Common mistakes:
1. IDENTITY + batch_size -- batching silently disabled
2. Missing order_inserts -- interleaved tables break batches
3. allocationSize=1 -- one sequence call per insert
4. Not verifying with generate_statistics
5. @DynamicUpdate + batching -- varying SQL breaks batch grouping

What should a senior engineer know?

A senior engineer verifies batch formation with statistics, matches allocationSize to batch_size, uses reWriteBatchedInserts on PostgreSQL, and knows that @DynamicUpdate conflicts with batching because it produces varying SQL per entity.

TEXT
Senior checklist:
1. SEQUENCE generation with allocationSize matching batch_size
2. hibernate.jdbc.batch_size, order_inserts, order_updates all set
3. PostgreSQL: reWriteBatchedInserts=true
4. Verify with generate_statistics=true
5. @DynamicUpdate conflicts with batching (varying SQL)
6. Combine with flush+clear for memory management
7. Consider per-batch commits for very large loads
8. @Version optimistic locking still works with batching

Visual Overview

DIAGRAM
  JDBC Batching Pipeline:

  ┌─────────────┐   ┌──────────────┐   ┌─────────────┐
  │  SEQUENCE    │   │  persist()   │   │  flush()    │
  │  pre-fetch   │──►│  queue INSERTs│──►│  JDBC batch │
  │  50 ids      │   │  (50 entities)│   │  50 stmts   │
  └─────────────┘   └──────────────┘   └──────┬──────┘
                                               │
                          ┌────────────────────┤
                          │                    │
                    Without rewrite      With rewrite
                    (50 individual       (1 multi-row
                     INSERT stmts)       INSERT stmt)
DIAGRAM
  Configuration alignment:

  @SequenceGenerator(allocationSize = 50)
          │
          ▼
  hibernate.jdbc.batch_size = 50
          │
          ▼
  flush() every 50 entities
          │
          ▼
  All three aligned = optimal batching

Key Takeaway

JDBC batching groups many INSERT or UPDATE statements into a single round trip instead of paying network latency for every statement. Enable it with hibernate.jdbc.batch_size, which tells Hibernate to accumulate that many statements and send them together. But batching only works when consecutive statements use the same prepared statement, so order_inserts and order_updates must reorder the queue to group same-table statements.

The most important pitfall is IDENTITY generation. With an auto-increment primary key, Hibernate must execute each INSERT immediately to read the generated id, so batching is silently disabled regardless of batch_size. The fix is SEQUENCE generation with a pooled allocationSize that matches the batch size, so Hibernate pre-fetches ids and can defer and batch the inserts.

At National Bank of India, a transaction-history backfill of millions of rows was crawling because it did one insert per round trip, and even after Rajesh set batch_size, nothing improved because the records used IDENTITY ids. Switching to SEQUENCE, enabling order_inserts, and adding reWriteBatchedInserts=true on PostgreSQL let the batches form, confirmed by Hibernate statistics showing the statement count drop far below the row count.

Remember one sentence: batch_size groups statements, order_inserts makes them batchable, IDENTITY kills batching silently, and SEQUENCE with matching allocationSize is the fix.

---

Share
Use ← → arrow keys to navigate