Fetch Size and Streaming

What Fetch Size Controls

When a query returns many rows, the JDBC driver pulls them from the server in chunks. Fetch size is the chunk size. Too small means many round trips; too large means a lot of memory per query.

Example

Reading 100,000 order rows for a monthly sales report:

public class Main {
    public static void main(String[] args) {
        int rows = 100_000;
        int[] fetchSizes = {10, 100, 1000, 10_000};
        System.out.println("fetch size | round trips | rows held in driver memory");
        for (int f : fetchSizes) {
            int trips = (rows + f - 1) / f;
            System.out.printf("%10d | %11d | %d%n", f, trips, f);
        }
    }
}

Output

fetch size | round trips | rows held in driver memory
        10 |       10000 | 10
       100 |        1000 | 100
      1000 |         100 | 1000
     10000 |          10 | 10000

Setting It

WhereHow
Globallyspring.jpa.properties.hibernate.jdbc.fetch_size=100
Per query@QueryHints(@QueryHint(name = "org.hibernate.fetchSize", value = "500"))
Oracledriver default is only 10 rows: always raise it for reports
PostgreSQLdriver loads the whole result unless fetch size > 0 and auto-commit is off (inside a transaction)
MySQLloads everything by default; use useCursorFetch=true in the URL plus a fetch size to stream

Streaming Large Results

public interface OrderRepository extends JpaRepository<Order, Long> {

    @QueryHints({
        @QueryHint(name = "org.hibernate.fetchSize", value = "500"),
        @QueryHint(name = "org.hibernate.readOnly", value = "true")
    })
    @Query("select o from Order o where o.createdAt >= :from and o.createdAt < :to")
    Stream<Order> streamForReport(@Param("from") Instant from, @Param("to") Instant to);
}

@Transactional(readOnly = true)                  // the stream needs an open transaction
public void writeMonthlyCsv(Writer out, YearMonth month) {
    try (Stream<Order> orders = orderRepository.streamForReport(start(month), end(month))) {
        orders.forEach(o -> {
            writeCsvLine(out, o);
            entityManager.detach(o);             // keep the persistence context small
        });
    }
}

Note: Always close the stream (try-with-resources). An open stream keeps the JDBC connection and cursor busy.