BIGSERIAL and JPA: IDENTITY vs SEQUENCE generation
A BIGSERIAL (or BIGINT GENERATED ... AS IDENTITY)
column means the database hands out the id. On the JPA side that maps to
@GeneratedValue, and the strategy you pick there decides more than
style: it decides whether Hibernate can batch your inserts.
IDENTITY: the obvious mapping
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
This matches the DDL one-to-one and it's what Schemint generates by default
from a BIGSERIAL. The catch: with IDENTITY, the id only exists
after the row is inserted. Hibernate needs the id to put the entity in the
persistence context, so it must execute each INSERT immediately and read the
generated key back. That disables JDBC batching for inserts, no matter what you
set hibernate.jdbc.batch_size to. Saving 10,000 rows means 10,000
round trips.
For a CRUD service inserting a handful of rows per request, this is irrelevant. It starts to matter in imports, backfills, event ingestion, anywhere you persist collections in bulk.
SEQUENCE: the batching-friendly one
CREATE SEQUENCE orders_seq;
CREATE TABLE orders (
id BIGINT PRIMARY KEY DEFAULT nextval('orders_seq'),
total NUMERIC(12, 2) NOT NULL
);
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "orders_seq")
@SequenceGenerator(name = "orders_seq", sequenceName = "orders_seq",
allocationSize = 50)
private Long id;
With a sequence, Hibernate fetches ids before inserting, so it can
accumulate INSERTs and flush them in JDBC batches. The
allocationSize = 50 is the other half of the trick: Hibernate
reserves 50 ids in one round trip and deals them out locally, so 10,000 inserts
cost 200 sequence calls plus the batched INSERTs, not 20,000 statements.
Two rules keep this honest. The allocationSize must match the
sequence's INCREMENT BY (Hibernate 6 validates this; a mismatch
either errors or, worse, generates colliding ids with the pooled optimizer
misconfigured). And batching still needs the usual switches:
spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_inserts=true
Under the hood BIGSERIAL is just a sequence with a default, so
you can even keep the column as-is and point @SequenceGenerator at
the implicit orders_id_seq. Set INCREMENT BY 50 on it
first, or leave allocationSize at 1 and forgo the pooling.
The strategies to skip
GenerationType.TABLE simulates a sequence with a regular table
plus row locks. It exists for databases without sequences; Postgres isn't one.
It serializes id generation and is strictly worse here.
GenerationType.AUTO delegates the choice to the dialect, which has
changed defaults across Hibernate versions (and once famously fell back to
TABLE); explicit beats portable-by-accident.
Which one, then
- Typical service, low insert volume per transaction: IDENTITY.
Matches the
BIGSERIALDDL, zero extra moving parts. - Bulk inserts on a hot path: SEQUENCE + allocationSize, with batch_size configured. Order-of-magnitude difference is normal; benchmark with your rows, not someone else's.
- Never TABLE, avoid AUTO.
One more consideration: with allocationSize > 1, ids are no longer gapless or strictly arrival-ordered (a restarted app discards the rest of its block). If someone downstream treats ids as a sequence-with-no-holes, they were wrong anyway (rollbacks already create gaps), but SEQUENCE pooling makes it obvious sooner.