1. 简介

在本篇文章中,我们将深入探讨 Hibernate 提供的查询计划缓存(Query Plan Cache)及其对性能的影响。

2. 查询计划缓存机制

在执行 JPQL 或 Criteria 查询之前,Hibernate 会将这些查询语句解析为抽象语法树(Abstract Syntax Tree, AST),然后基于 AST 生成对应的 SQL 语句。这个解析过程是耗时的,为了提升性能,Hibernate 引入了 QueryPlanCache

对于原生 SQL 查询(Native Query),Hibernate 也会提取参数信息和返回类型,并将其封装为 ParameterMetadata 存入缓存中。

每次执行查询时,Hibernate 会先检查缓存中是否存在对应的查询计划:

  • ✅ 如果命中缓存,则直接使用;
  • ❌ 如果未命中,则重新编译查询计划并将其缓存起来供后续使用。

3. 缓存配置项

查询计划缓存的行为可以通过以下两个配置项进行控制:

  • hibernate.query.plan_cache_max_size:控制查询计划缓存的最大条目数,默认值为 2048
  • hibernate.query.plan_parameter_metadata_max_size:控制 ParameterMetadata 缓存的最大条目数,默认值为 128

⚠️ 如果应用执行的查询数量超过了缓存大小,Hibernate 就需要频繁地重新编译查询计划,导致整体查询性能下降。

4. 测试环境搭建

俗话说得好:“性能优化不能靠嘴,得看数据。”所以我们来实测一下不同缓存配置对查询编译时间的影响。

4.1. 实体类定义

我们使用两个实体类进行测试:DeptEmployeeDepartment

@Entity
public class DeptEmployee {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;

    private String employeeNumber;

    private String title;

    private String name;

    @ManyToOne
    private Department department;

   // standard getters and setters
}
@Entity
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;

    private String name;

    @OneToMany(mappedBy="department")
    private List<DeptEmployee> employees;

    // standard getters and setters
}

4.2. 待测查询语句

我们选取了以下三个典型的 HQL 查询用于测试:

  • findEmployeesByDepartmentName
session.createQuery("SELECT e FROM DeptEmployee e " +
  "JOIN e.department WHERE e.department.name = :deptName")
  .setMaxResults(30)
  .setHint(QueryHints.HINT_FETCH_SIZE, 30);
  • findEmployeesByDesignation
session.createQuery("SELECT e FROM DeptEmployee e " +
  "WHERE e.title = :designation")
  .setHint(QueryHints.SPEC_HINT_TIMEOUT, 1000);
  • findDepartmentOfAnEmployee
session.createQuery("SELECT e.department FROM DeptEmployee e " +
  "JOIN e.department WHERE e.employeeNumber = :empId");

5. 性能测试分析

5.1. JMH 测试状态类

我们使用 JMH 进行基准测试,测试中通过改变缓存大小来观察编译时间的变化。

@State(Scope.Thread)
public static class QueryPlanCacheBenchMarkState {
    @Param({"1", "2", "3"})
    public int planCacheSize;
    
    public Session session;

    @Setup
    public void stateSetup() throws IOException {
       session = initSession(planCacheSize);
    }

    private Session initSession(int planCacheSize) throws IOException {
        Properties properties = HibernateUtil.getProperties();
        properties.put("hibernate.query.plan_cache_max_size", planCacheSize);
        properties.put("hibernate.query.plan_parameter_metadata_max_size", planCacheSize);
        SessionFactory sessionFactory = HibernateUtil.getSessionFactoryByProperties(properties);
        return sessionFactory.openSession();
    }
    //teardown...
}

5.2. 基准测试方法

测试方法如下,主要测量 Hibernate 编译查询语句的平均耗时:

@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(1)
@Warmup(iterations = 2)
@Measurement(iterations = 5)
public void givenQueryPlanCacheSize_thenCompileQueries(
  QueryPlanCacheBenchMarkState state, Blackhole blackhole) {

    Query query1 = findEmployeesByDepartmentNameQuery(state.session);
    Query query2 = findEmployeesByDesignationQuery(state.session);
    Query query3 = findDepartmentOfAnEmployeeQuery(state.session);

    blackhole.consume(query1);
    blackhole.consume(query2);
    blackhole.consume(query3);
}

5.3. 测试结果

运行上述基准测试后,我们得到如下结果图表:

plan cache

从图中可以看出:

  • 当缓存大小为 1 时,平均编译时间为 709 微秒
  • 当缓存大小增加到 2 时,时间下降至 409 微秒
  • 当缓存大小为 3 时,编译时间骤降至 0.637 微秒

✅ 显然,适当增大查询计划缓存可以显著减少查询编译开销。

6. 使用 Hibernate 统计信息监控缓存

Hibernate 提供了 Statistics 接口来监控缓存效果,其中两个关键指标是:

  • getQueryPlanCacheHitCount:缓存命中次数
  • getQueryPlanCacheMissCount:缓存未命中次数

📌 如果命中率高而未命中率低,说明大多数查询都复用了缓存中的计划,性能表现良好。

7. 结论

本文讲解了 Hibernate 查询计划缓存的机制与优化策略。合理配置缓存大小,能有效提升查询性能,避免不必要的重复编译开销。

如需获取本文完整源码,请访问:GitHub 项目地址


原始标题:Hibernate Query Plan Cache