Performance degradation of Criteria API queries with cached results in Hibernate 6.x and 7.x versions

Hello, Team!

This ticket is related to this discussion

But the root cause looks clear now, so I created a new ticket.

The issue can be recreated on your Benchmark project

  1. Use the Criteria API instead of JPQL
  2. Increase the count of iterations
  3. Catch the time elapsed only for queries in the loop

	@Benchmark
	public void single(Blackhole bh, EventCounters counters) {
		em = entityManagerFactory.createEntityManager();
		em.getTransaction().begin();
		final Author author = em.createQuery( "from Author", Author.class ).setMaxResults( 1 ).getSingleResult();
        //add start time
        long startTime = System.currentTimeMillis();
        //increase count of iterations to 10000
		for (int i = 0; i < 10000; i++) {

            // Use Criteria API instead of JPQL
            CriteriaBuilder builder = em.getCriteriaBuilder();
            CriteriaQuery<Book> criteria = builder.createQuery(Book.class);
            Root<Book> root = criteria.from(Book.class);
            criteria.select(root);
            List<Predicate> restrictions = new ArrayList<>();
            restrictions.add(root.get("author").in(author));
            criteria.where(restrictions.toArray(new Predicate[0]));
            TypedQuery<Book> query = em.createQuery(criteria);
            query.setHint( "org.hibernate.cacheable", "true");
            final List<Book> books = query.getResultList();

			for ( Book book : books ) {
				if ( bh != null ) {
					bh.consume( book );
				}
			}
			if ( counters != null ) {
				counters.books += books.size();
			}
		}
        long endTime = System.currentTimeMillis();
        long timeElapsed = endTime - startTime;
        //show Time elapsed
        System.out.println("Time elapsed: " + timeElapsed);
		em.getTransaction().commit();
		em.close();
	}

Hibernate 5.6.15

Time elapsed: 173
Time elapsed: 83
Time elapsed: 76
Time elapsed: 74
Time elapsed: 66

Hibernate 7.2.7

Time elapsed: 618
Time elapsed: 241
Time elapsed: 206
Time elapsed: 199
Time elapsed: 189

The difference for each iteration is ~200%, which looks noticeable.
After debugging and profiling that case, we found that the reason is in this method:
org.hibernate.query.sqm.internal.SqmInterpretationsKey#isCacheable
Based on the comments, the behavior looks clear.

  // for now at least, skip caching Criteria-based plans
  // - especially wrt parameters atm; this works with HQL because the
  // parameters are part of the query string; with Criteria, they're not.
  return keySource.isQueryPlanCacheable()
  		// At the moment we cannot cache query plan if there is filter enabled.
  	&& !keySource.getLoadQueryInfluencers().hasEnabledFilters()

Hibernate 6 and 7 never cache the query plan

  1. For Criterria API
  2. If there is filter enabled.
    There are a couple of other cases, but for us first two are important. Mainly, we use Criteria API, but as we have filter enabled - query plan is never cached for our application, even for JPQL.
    In our app, in one web request, we may have dozens of small queries (sometimes > 100), so the overall time difference looks noticeable.

Is there any chance that this behavior will be changed?
Possibly, we can solve this on our side?

Thanks for any suggestions.

Criteria API query caching has been implemented in Hibernate ORM 7.2 via HHH-19556, so if you want that, you will have to upgrade.

Unfortunately, there is nothing you can do about the filters disabling caching yourself, unless you implement your own SqmQueryImpl with custom caching.

You can create a Jira ticket for this and we will look into it. It would be best to link to a PR against the hibernate-orm-benchmark repository that demonstrates the performance issue.

Hi, Christian

Thanks for implementing this caching in the 7.2 version.
When I was debugging the Hibernate code, I have found a property

hibernate.criteria.plan_cache_enabled

which is responsible for the Criteria query plan cache. It is false by default.
When I enabled it, I have got the same performance difference 50% with 5.6.x version as I had for the JPQL query.
Which is much better than 200% that I had before.
It will be great to have this property in the 7.0 Migration Guide, too.

But the issue with “ilter enabled”, which disables caching, is still there for us.

Is there any chance that this behavior will change in the future?

Thanks

Like I already wrote:

I agree that the use case with filtering is problematic and we should fix that, but first, we need you to create the ticket and PR that demonstrates the problem.