Batch fetching in Devicon: eliminating N+1 queries for lazy collections
When looping over a collection of posts, lazy loading causes an N+1 query problem; using Hibernate @BatchSize reduces queries dramatically and improves performance.
10 Mar 2026, 15:20 UTC

Problem: N+1 queries when looping over a collection
When you iterate over a list of Post objects and each post lazily loads its List<Comment>, Hibernate issues one SQL query per post. With 100 posts you end up with 100 round‑trips to the database, which slows the application and can overload the DB.
The N+1 query trap explained
Lazy loading means the association is not fetched until it is first accessed. In a loop that accesses post.getComments() for every post, the first access triggers a query, the next post triggers another, and so on. This pattern is called the N+1 problem because you have the original query (1) plus N additional queries (N = number of posts).
Batch fetching to the rescue
Batch fetching tells Hibernate to load a set of associated entities in a single SQL statement. By annotating the collection with @BatchSize(size = 10), Hibernate will fetch up to 10 Comment rows for a batch of posts, reducing the number of queries dramatically.
How batch fetching works
- Hibernate keeps a batch buffer while iterating over the collection.
- When the buffer reaches the configured size, it executes one query that returns all rows for that batch.
- After the batch is processed, the buffer is cleared and the next posts are grouped.
Concrete example
Assume you have a Post entity with a lazily fetched List<Comment>:
@Entity
public class Post {
@Id
private Long id;
private String title;
@OneToMany
@BatchSize(size = 10) // configure batch size
private List comments;
}
@Entity
public class Comment {
@Id
private Long id;
private String text;
@ManyToOne
private Post post;
}
If you load 100 posts and then iterate:
for (Post p : posts) {
List cs = p.getComments(); // triggers a query per post
}
Without batch fetching this loop generates 101 SQL statements (the initial load of the posts plus 100 queries for each comment list).
With batch fetching enabled, the same loop produces only 11 statements: the initial post load and 10 batches of 10 comments each (the last batch may contain fewer rows).
Choosing the right batch size
Memory is the main trade‑off. A larger batch size reduces the number of round‑trips but holds more rows in memory at once. If the batch size is too big for the heap, you may see OutOfMemoryError or severe GC pauses.
- Start with a modest size (e.g., 10‑20) and monitor heap usage.
- If the number of rows per batch is small, a smaller batch may be more efficient because the overhead of executing a query dominates.
- Very small batches (1‑5) can be counter‑productive because each batch incurs its own query round‑trip.
Where to configure: the @BatchSize annotation belongs on the collection field or on the getter method, not on the entity class itself. It applies only to the annotated association.
Permissions and runtime considerations: the annotation is read at startup; no special database permission is required beyond the normal read access for the query. However, if you run the benchmark from a production‑like environment, ensure the application user has permission to execute the generated SQL.
Verification steps
- Enable Hibernate statistics. Add
hibernate.generate_statistics=truetopersistence.xmlor set the Spring Boot propertyspring.jpa.properties.hibernate.generate_statistics=true. This activates theentity-load-countmetric, which counts how many times an entity is loaded from the database. - Run a benchmark loop. In a test class or a main method, load a realistic number of posts (for example 200) and iterate over them, accessing the comment list. Use the same transaction boundaries as in production to mimic real usage.
- Check the metric. Before enabling batch fetching, note the
entity-load-countvalue. After enabling it, the value should be roughlyceil(postCount / batchSize) + 1. For a batch size of 10 and 200 posts, expect about 21 loads. - Profile memory. Use a JVM profiler (VisualVM, JMC, or similar) while the loop runs. Verify that the heap does not spike beyond the allocated size; a sudden increase may indicate that the batch size is too large.
- Inspect SQL logs. If you have logging enabled for SQL (e.g.,
spring.jpa.show-sql=true), count the distinctSELECTstatements. They should match the expected batch count.
Risks: an incorrectly large batch size can cause the JVM to allocate more memory than available, leading to out‑of‑memory errors. It can also fetch more data than needed, increasing network traffic and potentially exposing sensitive columns if the query selects more columns than the entity actually uses.
Limitations to keep in mind
- Batch fetching only affects lazy collections; eager or non‑lazy associations are untouched, so other N+1 problems may still exist elsewhere in the code.
- Mis‑configuring the size can lead to excessive memory consumption or unnecessary data loading, which may degrade responsiveness.
- Testing with data sizes close to production is essential; a batch size that works for a few thousand rows may fail when the collection grows.
Actionable closing
If you notice your application slowing down while iterating over collections, add @BatchSize to the lazy association, start with a batch size of 10, enable Hibernate statistics, and verify the reduction in SQL statements and heap stability. Adjust the size based on your memory budget and expected row count, then monitor production logs. When the trade‑off between fewer queries and lower memory pressure is balanced, batch fetching becomes a practical way to eliminate the N+1 problem in Devicon‑based applications.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.