How do you confirm and quantify that a running Java application is issuing one database query per returned row — what do you switch on or instrument, and what specifically do you measure?
answer
- metric = statements per operation, then scale the data 10x
- org.hibernate.SQL DEBUG + bind logger TRACE
- generate_statistics: entityFetchCount / collectionFetchCount
- datasource-proxy or p6spy = countable and assertable
- pg_stat_statements ranked by CALLS, not time
basics
~20 sMeasure statements per logical operation, not query duration. Turn on SQL logging (org.hibernate.SQL), or Hibernate Statistics for counts, or wrap the DataSource with datasource-proxy/p6spy to count and assert per request. Then vary the row count and see whether the count scales with it.
solid answer
~50 sThe metric is **SQL statements per logical operation**, and the diagnostic test is whether it grows with the number of rows returned. Three levels of instrumentation: 1. **SQL logging** — enable the `org.hibernate.SQL` logger at DEBUG (and the binder logger for parameters). Fastest way to *see* the pattern in development: a burst of identical keyed selects after a list query. Too noisy for production. 2. **Hibernate Statistics** — set `hibernate.generate_statistics=true` and read `getPrepareStatementCount()`, `getEntityLoadCount()`, `getQueryExecutionCount()`, `getCollectionFetchCount()`. `hibernate.session.events.log` logs a per-session summary, which gives you a statement count per request for free. 3. **DataSource-level proxies** — `datasource-proxy` or `p6spy` wrap the real DataSource and count every statement, independent of the ORM. This is what you use to *assert* a budget in tests and to instrument production sampling. Then quantify: run the same operation over 10, 100 and 1,000 rows. Constant statement count means the fetch plan is right; linear growth is the confirmation. APM traces and `pg_stat_statements` call counts corroborate from outside.
code
java · 9 linesStatistics stats = sessionFactory.getStatistics();
stats.clear();
service.loadDashboard(userId);
System.out.println("statements=" + stats.getPrepareStatementCount());
System.out.println("queries=" + stats.getQueryExecutionCount());
System.out.println("entityFetch="+ stats.getEntityFetchCount());
System.out.println("collFetch=" + stats.getCollectionFetchCount());go deeper
Know how to turn on SQL logging and recognise the repeated keyed selects that follow a list query.
Add Hibernate Statistics and the specific counters, and describe the scale-the-data test that proves the count is proportional to rows.
Cover DataSource-level counting for per-request budgets, warn about caching and low-cardinality fixtures skewing results, and corroborate with pg_stat_statements call counts or APM span counts.
Make it systemic: statements-per-request as a first-class metric with alerting, budgets enforced in CI, and a policy that every list endpoint declares its expected statement count.
## Reframe the measurement The defining property of an N+1 is not slowness of any statement but **count proportional to result size**. So every technique below serves one of two purposes: *see the pattern*, or *count per operation and watch it scale*. A useful mental protocol: 1. Pick one logical operation (an endpoint, a job step, a service method). 2. Count statements executed while it runs. 3. Repeat with 10x the data. 4. If the count moved roughly 10x, you have your answer. ## Level 1 — SQL logging (development) Enable the logger `org.hibernate.SQL` at DEBUG to see each statement, and `org.hibernate.orm.jdbc.bind` (older: `org.hibernate.type.descriptor.sql.BasicBinder`) at TRACE for bound parameters. Prefer this over the `hibernate.show_sql` property, which writes to stdout with no logging control. The visual signature is unmistakable: one query with a `where`/`join`, then a run of identical `select ... where id=?` lines. The position of the burst tells you the cause — immediately after the main query means an eagerly-mapped association; interleaved with your own log lines means a lazy association dereferenced in a loop. Limitations: it does not count for you, it does not attribute statements to a request, and it is far too verbose for production. ## Level 2 — Hibernate Statistics Set `hibernate.generate_statistics=true` and read `SessionFactory.getStatistics()`: - `getPrepareStatementCount()` — total statements prepared; the number closest to "round trips". - `getQueryExecutionCount()` — HQL/JPQL/Criteria executions. - `getEntityLoadCount()` / `getEntityFetchCount()` — the second is specifically loads that were *not* part of an explicit query, i.e. exactly the secondary selects of an N+1. - `getCollectionFetchCount()` versus `getCollectionLoadCount()` — same distinction for collections. The pair `entityFetchCount` and `collectionFetchCount` are the sharpest indicators you have inside Hibernate: a large fetch count relative to query count means data is being pulled in outside your queries. Additionally, `hibernate.session.events.log=true` (and the slow-session threshold setting) makes Hibernate log a per-session summary — statements prepared, time in JDBC — which converts to "statements per request" directly. Statistics carry a small overhead; running them permanently in production is usually acceptable and often worth it, but validate that. ## Level 3 — DataSource proxies and assertions Wrapping the `DataSource` with **datasource-proxy** or **p6spy** gives ORM-independent counting and a hook to act on: - count statements in a request scope and log or emit a metric when a threshold is exceeded; - in tests, assert a hard budget: this operation must execute at most 3 statements. Libraries such as QuickPerf exist for this, but a few dozen lines around datasource-proxy is enough. This is the technique that turns detection into **prevention**, because it is the only one that can fail a build. ## Level 4 — outside the process - **APM / distributed tracing.** A trace of a request with 200 database spans is self-diagnosing, and span counts per endpoint make a good dashboard and alert. - **Database-side statement statistics.** `pg_stat_statements` on PostgreSQL and the performance schema on MySQL rank statements by *call count* as well as by time. A primary-key select with tens of millions of calls and a microsecond mean is an N+1 pointing straight at the table involved. - **Connection-pool metrics.** Rising checkout duration without rising database CPU is the classic macroscopic symptom. ## Reading the numbers correctly - **Distinct ids, not rows.** The persistence context deduplicates, so a result set with repeated parents produces fewer statements. Never conclude "no N+1" from a fixture with low cardinality. - **Caching hides it.** A warm second-level cache suppresses statements; measure cold, or clear the cache first. - **Batching changes the shape, not the class of problem.** With batch fetching enabled you see `where id in (?, ?, ?, ...)` — statement count falls to N/size + 1, still proportional to N. - **Attribute to a boundary.** A raw statement count is useless without knowing which operation produced it, so scope the counter to a request/transaction. ## What a strong answer sounds like Name the metric (statements per operation), name at least two mechanisms (Hibernate Statistics counts and a DataSource-level counter), give the scaling test, and finish with the point that detection should be automated in tests so the defect cannot re-enter — not left to someone reading logs.
- Which Hibernate statistic distinguishes rows loaded by your queries from rows pulled in behind your back?getEntityFetchCount and getCollectionFetchCount count loads that were not part of an explicit query — that is, the secondary selects Hibernate issues to initialise associations. Comparing them against getQueryExecutionCount tells you how much of your data access is implicit. A fetch count far larger than the query count is close to a direct measurement of N+1.
- Why is asserting a statement budget in a test better than reviewing SQL logs?Logs require a human to look at the right moment, which means regressions are found after they ship. A budget assertion around a DataSource-level counter fails the build the instant a fetch plan is broken, and it documents the intended cost of the operation in an executable form. It also survives refactoring, because it constrains the outcome rather than the implementation.
saying these in an interview costs you the question
- Looking at the slow-query log or average query duration to find N+1.
- Concluding there is no problem from a fixture with only a handful of distinct associated rows.
- Measuring with a warm second-level cache and reporting the suppressed count.
- Reporting a total statement count without scoping it to a single operation.
- Assuming batch fetching removed the problem because the log now shows IN clauses.