skip to content

What does Hibernate's `@Fetch(FetchMode.SUBSELECT)` do on a collection, and what conditions must hold for it to actually take effect?

level: seniorimportance: should knowfreq 38%

answer

  1. Replay the parent query as a subquery
  2. Exactly one extra query, all parents at once
  3. Collections only, never to-one proxies
  4. Dies after clear/detach/new session
  5. Deadly with setMaxResults — subselect ignores the limit

basics

~20 s

When any one collection from a previously-run query's results is initialised, Hibernate loads that collection for all those parents in one query, re-using the original query as a subselect in the WHERE clause. It needs the original query still tracked in the same session, and applies to collections only.

solid answer

~50 s

`@Fetch(FetchMode.SUBSELECT)` is a lazy-loading optimisation for collections. Hibernate remembers the SQL of the query that loaded the parents; the first time any one of those parents' collections is touched, it issues a single query whose predicate re-runs that original query as a subquery: ```sql select ... from order_line where order_id in (select id from orders where status = ?) ``` Every parent's collection is initialised at once — one extra query in total, regardless of how many parents there were, and no duplicated parent rows. The conditions matter. It applies to **collections**, not to `@ManyToOne` proxies. The parents must have been loaded by a query whose SQL Hibernate can replay, and you must still be in the same persistence context; after a clear, detach, or a new session it degrades to ordinary lazy loading. It also does not combine with pagination: `setMaxResults` limits the outer query but the subselect replays the unlimited original, so you can load collections for far more parents than you displayed.

code

java · 11 lines
java
@Entity
class Order {
    @OneToMany(mappedBy = "order")
    @Fetch(FetchMode.SUBSELECT)
    private Set<OrderLine> lines = new HashSet<>();
}

// List<Order> orders = em.createQuery(
//     "select o from Order o where o.status = :s", Order.class)
//     .setParameter("s", NEW).getResultList();
// orders.get(0).getLines();   // initialises lines for ALL orders in one query

go deeper

for a junior

Recall that it loads one collection for all previously-queried parents in a single extra query instead of one query per parent.

for a middle

Describe the replayed-subquery mechanism, that it applies only to collections, and that it requires the same session and a query-loaded parent set.

for a senior

Add the pagination trap, the cost of re-executing an expensive parent query, and a clear rule for when to prefer batch fetching instead.

for a principal

Weigh a fixed two-query profile against database-side re-execution and unbounded child loads, and decide where a mapping-level strategy is acceptable versus a per-query fetch plan.

## What subselect fetching is Subselect fetching answers the same question as batch fetching — "how do I avoid one query per parent when initialising lazy collections?" — with a different trick. Instead of collecting pending identifiers into an `IN (?, ?, ?)` list, Hibernate **replays the query that produced the parents** as a subquery. When a query loads parent entities and any of them has a collection mapped `@Fetch(FetchMode.SUBSELECT)`, Hibernate keeps the original SQL and its parameters associated with the session. The first time any one of those collections is initialised, it emits: ```sql select line.* from order_line line where line.order_id in (select o.id from orders o where o.status = ?) ``` and initialises the collection for **every** parent from the original result, not just the one you touched. Total cost: exactly two queries for the whole traversal, however many parents there were. ## Declaring it ```java @OneToMany(mappedBy = "order") @Fetch(FetchMode.SUBSELECT) private Set<OrderLine> lines; ``` `@Fetch` is a Hibernate annotation, not JPA; there is no standard equivalent. It applies per collection role and is a property of the mapping, not of the query — you cannot turn it on for one query and off for another, which is one of its real drawbacks. ## The conditions **It applies to collections only.** A `@ManyToOne` proxy cannot be subselect-fetched; there is no collection role to replay for. For the to-one direction, batch fetching is the tool. **The parents must come from a query.** The optimisation exists because Hibernate has SQL to replay. If the parent was loaded by `em.find`, or came from the persistence context or the second-level cache, there is no query to re-run — and with one parent there would be nothing to gain anyway. **Same persistence context.** The remembered query lives with the session. Clear the persistence context, detach the entities, or move to a new transaction, and the collection initialises with an ordinary single-parent select. This is a common reason people report "SUBSELECT doesn't work": the initialisation happened after a `clear()` or in a different session from the load. **It re-executes work on the database.** The subquery is the original query again. If the parent query was expensive — several joins, an unindexed filter, a sort — you pay that cost a second time. Batch fetching, by contrast, sends already-known primary keys, which is trivially cheap for the database to satisfy. This is the main reason to prefer batching as a default and reach for subselect deliberately. ## The pagination trap This one deserves its own paragraph because it is a genuine correctness-of-workload problem. Suppose the parent query is paginated with `setMaxResults(20)`. The outer query returns twenty rows, but the subselect that Hibernate replays is the query **without** the limit, so the collection query loads lines for every order matching the filter — potentially hundreds of thousands of rows into a session that only displays twenty parents. The memory and latency profile is catastrophic and the page still renders correctly, so it can go unnoticed until the table grows. With batch fetching, pagination is safe: the IN list contains only the identifiers actually in the page. ## Choosing subselect over batch Subselect wins when: the parent query is cheap and selective, you know you will traverse all the parents' collections, and there are many parents (so a fixed one-extra-query beats N/batchSize queries). Reporting jobs and bulk exports fit well. Batch wins when: results are paginated, only some parents' collections are touched, the parent query is expensive, or you want a single global setting that helps everywhere. That is most of an application, which is why `hibernate.default_batch_fetch_size` is the more common production choice and `@Fetch(FetchMode.SUBSELECT)` is applied surgically. ## Interaction with other strategies Subselect is orthogonal to fetch joins: a query that already fetch-joins the collection initialises it eagerly, and the subselect never fires. It is also independent of `@BatchSize` on the same role — declaring both means the subselect strategy takes over for collections loaded via a replayable query. ## How to answer Lead with the mechanism ("the original query is replayed as a subquery in the IN predicate"), then the payoff ("one extra query for all parents, no duplicate rows"), then the three conditions (collections, query-loaded parents, same session), then the pagination trap. That last point is what distinguishes someone who has used it from someone who has read about it.

  • Why is subselect fetching dangerous when the parent query is paginated?
    Hibernate replays the original query inside the IN subquery without the row limit, because the limit is applied by the outer statement, not by the query text it remembers. The outer page returns twenty parents while the collection query loads children for every matching parent in the table. The page looks correct, so the problem surfaces only as memory pressure and latency as data grows; batch fetching, which sends the page's actual identifiers, is safe here.
  • Can you use FetchMode.SUBSELECT for a @ManyToOne association?
    No. The strategy is defined for collection roles: it initialises the collections belonging to a set of parents produced by a replayable query. A to-one association is an entity proxy, with no collection role to replay, so the annotation has no effect there. Use batch fetching — `@BatchSize` on the target entity class, or the global `hibernate.default_batch_fetch_size` — for the to-one direction.

Rather than listing which houses need post, the courier reuses the original address query — "everyone on the route I just walked" — which is efficient unless the route was longer than the page you were showing.

saying these in an interview costs you the question

  • Thinking SUBSELECT works for @ManyToOne proxies
  • Expecting it to work after the persistence context is cleared or in a new session
  • Assuming it respects setMaxResults on the parent query
  • Believing it makes the collection eager
  • Choosing it by default without noticing the parent query is executed twice

context