Query Cache

Caching Query Results

The query cache stores the result ids of a query, keyed by the query text and its parameter values. On a hit, Hibernate takes those ids and loads each entity from the L2 cache. So the query cache is only useful together with an L2-cached entity.

Enable

application.properties

spring.jpa.properties.hibernate.cache.use_query_cache=true
public interface CategoryRepository extends JpaRepository<Category, Long> {

    @QueryHints(@QueryHint(name = "org.hibernate.cacheable", value = "true"))
    List<Category> findAllByOrderByNameAsc();
}

Invalidation Is Per Table

Hibernate remembers the last time each table was changed (the update timestamps region). Any insert, update or delete on products through Hibernate makes every cached query that touches products stale. The next run goes to the database again.

Example

A simulation of the query cache and the per-table timestamp check:

import java.util.*;

public class Main {
    record Entry(List<Long> ids, long cachedAt) {}

    static long clock = 0;
    static final Map<String, Long> tableUpdatedAt = new HashMap<>(Map.of("products", 0L));
    static final Map<String, Entry> queryCache = new HashMap<>();

    static List<Long> query(String sql) {
        Entry e = queryCache.get(sql);
        if (e != null && e.cachedAt() > tableUpdatedAt.get("products")) {
            System.out.println("query cache HIT  -> ids " + e.ids());
            return e.ids();
        }
        System.out.println("query cache MISS -> running SQL: " + sql);
        List<Long> ids = List.of(3L, 7L, 9L);
        queryCache.put(sql, new Entry(ids, ++clock));
        return ids;
    }

    static void updateProduct(long id) {
        tableUpdatedAt.put("products", ++clock);
        System.out.println("UPDATE products ... where id=" + id + "  (products table timestamp changed)");
    }

    public static void main(String[] args) {
        String sql = "select id from products where category_id=? [cat=2]";
        query(sql);
        query(sql);
        updateProduct(42);        // a product that is not even in the result
        query(sql);
        query(sql);
    }
}

Output

query cache MISS -> running SQL: select id from products where category_id=? [cat=2]
query cache HIT  -> ids [3, 7, 9]
UPDATE products ... where id=42  (products table timestamp changed)
query cache MISS -> running SQL: select id from products where category_id=? [cat=2]
query cache HIT  -> ids [3, 7, 9]

When Not to Use It

  • Tables written often (orders, cart items, stock): the cache is invalidated all the time and only adds overhead.
  • Queries with many different parameter values (search boxes): almost every call is a miss and memory fills with useless entries.
  • Projection or DTO queries over large results: prefer an application-level cache (@Cacheable with Caffeine/Redis, covered in the Spring Boot material).

Tip: Rule of thumb: query cache only for small, stable lookups such as "all categories" or "all active payment methods".