skip to content

Why does using an auto-increment (IDENTITY) column for entity identifiers stop Hibernate from batching INSERT statements, and what changes if you switch to a database sequence?

level: middleimportance: must knowfreq 55%

answer

  1. Persistence context is keyed by id
  2. IDENTITY: INSERT runs inside persist()
  3. Nothing pending at flush -> nothing to batch
  4. SEQUENCE: id first, insert queued, batch at flush
  5. batch_size unset = no batching at all

basics

~20 s

With IDENTITY the id only exists once the row is inserted, but Hibernate needs an id to key the entity in the persistence context. So it executes the INSERT immediately at persist(), one statement per entity — nothing is queued, so nothing can batch. A sequence gives the id up front, letting inserts queue and batch at flush.

solid answer

~50 s

Hibernate's persistence context is a map keyed by entity identifier, and `persist()` must return a managed instance, so the id has to be known immediately. With an identity column the only way to learn it is to run the INSERT and read the generated key — so Hibernate abandons write-behind for that entity and issues the INSERT inside `persist()`. That has two effects. First, inserts cannot be collected into a JDBC batch: they have already been sent, one round trip each. Second, insert ordering and other flush-time optimisations (`hibernate.order_inserts`) no longer apply, and rows appear in the database — and hold locks — from `persist()` rather than from flush. With SEQUENCE Hibernate calls `nextval` first (and with an allocation size, one call serves many entities), assigns the id, and queues the INSERT until flush, where `hibernate.jdbc.batch_size` groups statements into batches. On a bulk-insert path that is often an order-of-magnitude difference in round trips.

code

java · 16 lines
java
@Entity
public class Event {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "event_seq")
    @SequenceGenerator(name = "event_seq", sequenceName = "event_seq", allocationSize = 50)
    private Long id;
}

// hibernate.jdbc.batch_size = 50
// hibernate.order_inserts = true
// hibernate.order_updates = true

for (int i = 0; i < 10_000; i++) {
    em.persist(new Event(...));
    if (i % 50 == 0) { em.flush(); em.clear(); }
}

go deeper

for a junior

Know the core fact: with an auto-increment id Hibernate must insert immediately to learn the id, so inserts go out one by one.

for a middle

Explain the persistence-context-keyed-by-id constraint, what write-behind normally buys, and that a sequence restores queuing plus batching when batch_size is set.

for a senior

Quantify round trips, mention order_inserts, flush/clear windowing, earlier lock acquisition with identity, and the MySQL-specific dead end.

for a principal

Treat it as a throughput and contention decision across engines: where ids are minted determines how much work can be deferred and coalesced, and whether bulk paths should bypass the ORM entirely.

## The constraint that forces the behaviour A persistence context is a first-level cache keyed by `(entity type, identifier)`. Two invariants follow: an entity cannot be managed without an identifier, and `persist()` returns an instance that *is* managed. So at `persist()` time Hibernate needs the id. For SEQUENCE, TABLE, UUID and assigned ids, the id can be obtained without writing the row. For IDENTITY it cannot: the value is allocated by the engine as part of executing the INSERT, and comes back through JDBC's generated-keys mechanism. Hibernate therefore special-cases identity generation — the classic `IdentityGenerator` performs the insert itself. ## What is lost **Batching.** JDBC batching means adding many parameter sets to one `PreparedStatement` and sending them together. That only works if the statements are still pending. With IDENTITY there is nothing pending at flush — the rows went out one at a time, each a network round trip and a separate statement execution. Persisting 10,000 entities is 10,000 round trips instead of, say, 200 batches of 50. **Write-behind and ordering.** Hibernate normally accumulates the action queue and can reorder it: `hibernate.order_inserts` groups inserts by entity type so that same-table rows land next to each other and batch well; `order_updates` does the same for updates. Identity inserts have already been executed, so they cannot participate. **Deferred visibility.** Ordinarily nothing hits the database until flush, which keeps row locks short and lets you `persist()` speculatively inside a transaction. With IDENTITY the INSERT (and its locks, its unique-constraint checks, its triggers) happens at `persist()`. It is still transactional — a rollback removes the row — but the lock is held for longer, and constraint violations surface earlier and in a different place than developers expect. **Cascade cost.** Persisting a parent with a large cascaded child collection means one immediate INSERT per child. ## What a sequence buys `select nextval('order_seq')` costs a round trip too — but only one per *block* when `allocationSize > 1`, because Hibernate's pooled optimizer hands out a range of ids from memory. With `allocationSize = 50`, inserting 10,000 rows costs 200 sequence calls plus batched inserts. The inserts themselves are queued as actions and executed at flush, where Hibernate groups identical SQL into JDBC batches sized by `hibernate.jdbc.batch_size` (nothing batches at all if that property is unset — a common reason people see no improvement after switching). For maximum effect, combine it with `order_inserts`, `order_updates`, and periodic `flush()` + `clear()` so the persistence context does not grow without bound. Note what batching does *not* fix: it reduces round trips and statement-execution overhead, not the work the database does per row. Index maintenance, triggers and constraint checks are unchanged. ## Version nuance The "IDENTITY disables batching" rule is a statement about the classic behaviour, and it is what interviewers expect. Newer Hibernate versions (6.5 and later) can batch identity inserts on dialects whose driver reliably returns generated keys for batched statements; support is dialect-dependent and not universal. Say the rule, then note the nuance — and in practice verify with SQL logging or statement counters rather than assuming. ## Practical guidance On engines with sequences (PostgreSQL, Oracle, H2, SQL Server, MariaDB), prefer SEQUENCE for anything that inserts in volume. On MySQL, where sequences do not exist, the realistic options are IDENTITY (accepting per-row inserts) or the table generator (which adds its own contention); for genuine bulk loads, many teams bypass the ORM entirely with a native multi-row INSERT or a `LOAD DATA`-style path. If you must keep IDENTITY, remember that updates and deletes still batch normally — only inserts are affected.

  • You switched to SEQUENCE but the SQL log still shows one INSERT per row. What would you check?
    First, whether `hibernate.jdbc.batch_size` is set — with no batch size Hibernate never batches, whatever the id strategy. Second, whether the log is simply printing each parameter set (batched statements still log per execution unless you count them at the JDBC level). Third, whether something forces an early flush between persists — a query that triggers auto-flush, or an explicit flush per iteration — which breaks the batch into single statements.
  • Does an IDENTITY id strategy also prevent batching of UPDATE and DELETE statements?
    No. The restriction is specific to inserts, because only inserts need the generated key up front. Updates and deletes are queued in the action list and flushed normally, so they batch according to `hibernate.jdbc.batch_size` and benefit from `hibernate.order_updates`.

IDENTITY is like posting each letter the moment you write it because only the post office can tell you its tracking number; a sequence lets you number the envelopes yourself and drop the whole sack off at once.

saying these in an interview costs you the question

  • Saying batching is disabled because identity columns lock the table
  • Thinking the INSERT still waits for flush with IDENTITY
  • Expecting batching to work without setting hibernate.jdbc.batch_size
  • Believing batching reduces the database's per-row work rather than round trips
  • Claiming a sequence call per insert makes SEQUENCE slower than IDENTITY, ignoring allocation blocks

context