skip to content

After enabling JDBC batching in Hibernate, how do you actually prove that statements are being batched, given that hibernate.show_sql prints the same number of lines either way?

level: seniorimportance: should knowfreq 34%

answer

  1. show_sql logs preparation, not transmission
  2. datasource-proxy / p6spy report batch=true + size
  3. TRACE org.hibernate.engine.jdbc.batch.internal
  4. pg_stat_statements = round-trip truth
  5. size stuck at 1 -> identity id or no ordering

basics

~20 s

Statement logging prints each statement as Hibernate prepares it, so it never shows batching. Prove it with a proxying datasource such as datasource-proxy or p6spy that reports batch size, with TRACE logging on Hibernate's JDBC batch internals, or by counting round trips at the database.

solid answer

~50 s

`hibernate.show_sql` and the `org.hibernate.SQL` logger emit a line when Hibernate **prepares** a statement, not when it is sent. Batched or not, ten inserts produce ten lines, so the log can never confirm or deny batching. Options that can: - **A proxying datasource.** `datasource-proxy` or `p6spy` wrap the real `DataSource` and log actual JDBC calls, including whether the execution was a batch and how many entries it held. This is the most direct evidence and the one to reach for in a test. - **Hibernate's batch internals at TRACE.** The classes under `org.hibernate.engine.jdbc.batch.internal` log the batch size when a batch is executed. - **Database-side counting.** Compare statement or call counts before and after — for example PostgreSQL's `pg_stat_statements`, MySQL's general log, or simply the network round-trip count. This also reveals whether a driver rewrite flag actually collapsed the batch. - **A regression test.** Assert an expected batch count with a proxying datasource so an identifier-mapping change cannot silently disable batching later. Wall-clock timing is a hint, not proof.

code

properties · 6 lines
properties
# prints one line per prepared statement - proves nothing about batching
hibernate.show_sql=true
logging.level.org.hibernate.SQL=DEBUG

# reports the size of each executed batch
logging.level.org.hibernate.engine.jdbc.batch.internal=TRACE

go deeper

for a junior

Know that statement logging cannot show batching and that a proxying datasource or driver-level logging can.

for a middle

Name a concrete tool and what its output looks like, plus the batch-internals TRACE logger.

for a senior

Cover database-side counting, the effect of driver rewrite flags, and turning the check into a regression test.

for a principal

Frame it as observability for a silent optimisation: what signal exists, where it is asserted, and how a mapping change is prevented from regressing throughput unnoticed.

## Why the obvious tool does not work `hibernate.show_sql=true` (and the equivalent `org.hibernate.SQL` logger at DEBUG) logs the SQL string at the point Hibernate obtains or prepares the statement. Batching changes only how the parameter sets are transmitted, not how many statements Hibernate prepares logically. So the log looks identical whether the ten inserts went out as ten round trips or as one batch of ten. Candidates who claim they "saw in the log that it batched" have usually seen nothing of the sort, and this is a favourite follow-up in interviews. The generic conclusion is worth stating explicitly: to verify a **transport-level** optimisation you must observe at or below the transport, which means the JDBC layer, the driver, or the database. ## The reliable techniques ### 1. A proxying DataSource `datasource-proxy` and `p6spy` sit between the connection pool and the driver, intercepting real JDBC calls. Their log lines carry an explicit batch indicator and the number of entries — for example a single entry reporting `batch=true, batchSize=50` instead of fifty separate executions. Because it is programmatic, it also supports assertions: `datasource-proxy` lets a test collect the executions and assert that the write path issued, say, four batched executions rather than two hundred singles. That turns "is batching on?" from a manual investigation into a regression test, which matters because a single mapping change — switching an identifier to an auto-increment column — silently disables insert batching with no error anywhere. ### 2. Hibernate's own batch logging Hibernate's batch implementation lives under `org.hibernate.engine.jdbc.batch.internal`. Raising that package to TRACE (in Hibernate 5, `BatchingBatch`; in Hibernate 6, the `BatchImpl`) produces a line each time a batch is executed, including its size. It is cheap, requires no extra dependency, and is a good first check in a local run. It is also noisy and version-sensitive in class naming, so it is a diagnostic tool rather than something to assert on in tests. ### 3. Count at the database The database has the final word on how many statements arrived. On PostgreSQL, `pg_stat_statements` shows the `calls` count per normalised statement before and after a run; on MySQL, the general query log does the same more bluntly. This view has a unique advantage: it also shows the effect of driver-side rewriting. With PostgreSQL's `reWriteBatchedInserts=true` or MySQL's `rewriteBatchedStatements=true`, a batch of fifty inserts can arrive as one multi-`VALUES` statement, which shows up here and nowhere in the application logs. ### 4. Timing, carefully A large drop in wall-clock time for a fixed workload strongly suggests fewer round trips, especially if the improvement scales with network latency — run the same job against a local and a remote database and compare. But timing changes for many reasons (caches, plans, warm-up), so treat it as corroboration rather than evidence. ## What to look for, concretely When batching is working you should see: a small number of executions each carrying up to `batch_size` entries; batch sizes near the configured maximum rather than 1 or 2; and a database-side statement count much lower than the row count if rewriting is enabled. Two diagnostic patterns recur: - **Batch size stuck at 1 for inserts, while updates batch fine.** Almost always an identity/auto-increment identifier, which forces each insert to execute inside `persist()`. - **Many batches of 1–3 across mixed entity types.** Statement ordering is off; a batch ends whenever the SQL string changes. ## Make it permanent Batching is quiet when it breaks — no exception, no warning, only a slower job. On a write path where throughput matters, encode the expectation as a test with a proxying datasource asserting the number of batched executions for a known workload. That test fails on the pull request that changes an identifier strategy or interleaves a new entity type into the loop, which is exactly when you want to hear about it, rather than during the next month-end run.

  • A proxying datasource shows batch sizes of 1 for inserts but 50 for updates on the same entities. What does that point to?
    An identity or auto-increment identifier strategy. Hibernate must execute the insert immediately inside `persist()` to read the generated key, so inserts never reach the flush-time queue where batching happens, while updates are produced at flush and batch normally. Switching to a sequence with an allocation size restores insert batching.
  • You enabled batching and the batch sizes look right, but the database still records one statement per row. What would explain that?
    Batching reduces round trips but does not merge statements unless the driver rewrites them. MySQL's `rewriteBatchedStatements=true` and PostgreSQL's `reWriteBatchedInserts=true` collapse batched inserts into a single multi-VALUES statement; without them the server still executes each statement individually even though they were shipped together.

saying these in an interview costs you the question

  • Claiming the SQL log shows whether batching happened
  • Assuming a faster run alone proves batching
  • Not knowing any tool below Hibernate's logging that can observe JDBC calls
  • Verifying once manually and never protecting it with a test
  • Expecting the database statement count to drop without a driver rewrite flag

context