Hibernate Query Plan Cache – Hibernate查询计划缓存

最后修改: 2019年 2月 16日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.绪论

In this quick tutorial, we’ll explore the query plan cache provided by Hibernate and its impact on performance.

在这个快速教程中,我们将探讨Hibernate提供的查询计划缓存以及它对性能的影响。

2. Query Plan Cache

2.查询计划缓存

Every JPQL query or Criteria query is parsed into an Abstract Syntax Tree (AST) prior to execution so that Hibernate can generate the SQL statement. Since query compilation takes time, Hibernate provides a QueryPlanCache for better performance.

每个JPQL查询或Criteria查询在执行前都被解析为抽象语法树(AST),以便Hibernate能够生成SQL语句。由于查询编译需要时间,Hibernate提供了一个QueryPlanCache以提高性能。

For native queries, Hibernate extracts information about the named parameters and query return type and stores it in the ParameterMetadata.

对于本地查询,Hibernate提取有关命名参数和查询返回类型的信息,并将其存储在ParameterMetadata

For every execution, Hibernate first checks the plan cache, and only if there’s no plan available, it generates a new plan and stores the execution plan in the cache for future reference.

对于每一次执行,Hibernate首先检查计划缓存,只有当没有可用的计划时,它才会生成一个新的计划,并将执行计划存储在缓存中供将来参考。

3. Configuration

3.配置

The query plan cache configuration is controlled by the following properties:

查询计划缓存的配置是由以下属性控制的。

  • hibernate.query.plan_cache_max_size – controls the maximum number of entries in the plan cache (defaults to 2048)
  • hibernate.query.plan_parameter_metadata_max_size – manages the number of ParameterMetadata instances in the cache (defaults to 128)

So, if our application executes more queries than the size of query plan cache, Hibernate will have to spend extra time in compiling queries. Hence, overall query execution time will increase.

因此,如果我们的应用程序执行的查询数量超过了查询计划缓存的大小,Hibernate将不得不花费额外的时间来编译查询。因此,整体的查询执行时间会增加。

4. Setting Up the Test Case

4.设置测试案例

As the saying goes in the industry, when it comes to performance, never trust the claims. So, let’s test how the query compilation time varies as we change the cache settings.

正如业内人士所言,当涉及到性能时,永远不要相信那些说法。所以,让我们测试一下,当我们改变缓存设置时,查询的编译时间是如何变化的

4.1. Entity Classes Involved in Test

4.1.测试中涉及的实体类

Let’s start by having a look at the entities we’ll use in our example, DeptEmployee and Department:

让我们先看看我们将在例子中使用的实体,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. Hibernate Queries Involved in Test

4.2.测试中涉及的Hibernate查询

We’re interested in measuring the overall query compilation time only, so, we can pick any combination of valid HQL queries for our test.

我们只对测量整个查询的编译时间感兴趣,所以,我们可以选择任何有效的HQL查询组合进行测试。

For the purpose of this article, we’ll be using the following three queries:

为了本文的目的,我们将使用以下三个查询。

  •  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. Measuring the Performance Impact

5.衡量绩效影响

5.1. Benchmark Code Setup

5.1 基准代码设置

We’ll vary the cache size from one to three – after that, all three of our queries will already be in the cache. Therefore, there’s no point in increasing it further:

我们将缓存的大小从一变到三 – 之后,我们的三个查询都已经在缓存中了。因此,再增加就没有意义了。

@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. Code Under Test

5.2 被测试的代码

Next, let’s have a look at the benchmark code used to measure the average time taken by Hibernate while compiling the queries:

接下来,让我们看一下用于测量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);
}

Note that we’ve used JMH to write our benchmark.

请注意,我们使用了JMH来编写我们的基准测试。

5.3. Benchmark Results

5.3.基准结果

Now, let’s visualize the compilation time vs cache size graph that we prepared by running the above benchmark:

现在,让我们来看看我们通过运行上述基准所准备的编译时间与缓冲区大小的关系图。

plan cache

As we can clearly see in the graph, increasing the number of queries that Hibernate is allowed to cache consequently reduces the compilation time.

从图中我们可以清楚地看到,增加Hibernate允许缓存的查询数量,从而减少编译时间

For a cache size of one, the average compilation time is the highest at 709 microseconds, then it decreases to 409 microseconds for a cache size of two, and all the way to 0.637 microseconds for a cache size of three.

对于缓存大小为1的情况,平均编译时间最高,为709微秒,然后减少到409微秒,对于缓存大小为2的情况,一直减少到0.637微秒。

6. Using Hibernate Statistics

6.使用Hibernate的统计数据

To monitor the effectiveness of the query plan cache, Hibernate exposes the following methods via the Statistics interface:

为了监测查询计划缓存的有效性,Hibernate通过Statistics接口暴露了以下方法。

  • getQueryPlanCacheHitCount
  • getQueryPlanCacheMissCount

So, if the hit count is high and the miss count is low, then most of the queries are served from the cache itself, instead of being compiled over and over again.

所以,如果命中率高,失误率低,那么大部分的查询都是由缓存本身提供的,而不是被反复编译的。

7. Conclusion

7.结语

In this article, we learned what the query plan cache is in Hibernate and how it can contribute to the overall performance of the application. Overall, we should try to keep the query plan cache size in accordance to the number of queries running in the application.

在这篇文章中,我们了解了Hibernate中的查询计划缓存是什么,以及它如何对应用程序的整体性能做出贡献。总的来说,我们应该尽量使查询计划缓存的大小与应用程序中运行的查询数量保持一致。

As always, the source code of this tutorial is available over on GitHub.

一如既往,本教程的源代码可在GitHub上获取