Definition
- 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.
# application.yml
spring:
jpa:
properties:
hibernate:
jdbc.batch_size: 50
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.
spring:
jpa:
properties:
hibernate:
order_inserts: true
order_updates: true
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();
}
}
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.
// 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
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.
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;
}
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
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();
}
}
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.
spring:
jpa:
properties:
hibernate:
generate_statistics: true
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.
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.
// 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
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.
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
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)
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.
---