Рубрики
Без рубрики

Как оптимизировать планы запросов JPQL и API критериев с помощью статистики гибернации

Узнайте, как оптимизировать планы запросов JPQL и API критериев с помощью статистики гибернации для ускорения запросов, в которых можно использовать параметр заполнения параметров.

Автор оригинала: Vlad Mihalcea.

Вступление

Каждый запрос сущности, будь то JPQL или API критериев, должен быть проанализирован и скомпилирован в AST (Абстрактное синтаксическое дерево) для создания соответствующего SQL-запроса. Компиляция запроса сущности требует времени, как описано в этой статье поэтому Hibernate предоставляет QueryPlanCache для хранения уже скомпилированных планов.

Начиная с версии Hibernate 5.4, механизм Hibernate Статистика позволяет отслеживать кэш плана запроса, и в этой статье будет показано, как воспользоваться этой функцией для ускорения выполнения запросов.

Для ознакомления с механизмом статистики гибернации ознакомьтесь с этой статьей .

Как оптимизировать планы запросов JPQL и API критериев с помощью #Hibernate Статистики @vlad_mihalcea https://t.co/bHfkKKCGOn pic.twitter.com/1k2PUeVpb7

Модель предметной области

Предполагая, что у нас есть следующая Должность сущность:

Который отображается следующим образом:

@Entity(name = "Post")
@Table(name = "post")
public class Post {

    @Id
    private Integer id;

    private String title;

    //Getters and setters omitted for brevity

}

JPQL В запросе

Теперь давайте посмотрим, что происходит, когда Hibernate выполняет запрос JPQL, содержащий предложение IN:

SessionFactory sessionFactory = entityManagerFactory()
    .unwrap(SessionFactory.class);
    
Statistics statistics = sessionFactory.getStatistics();
statistics.clear();

doInJPA(entityManager -> {
    List posts = entityManager.createQuery(
        "select p " +
        "from Post p " +
        "where p.id in :ids", Post.class)
    .setParameter("ids", Arrays.asList(1, 2, 3))
    .getResultList();
});

for (String query : statistics.getQueries()) {
    LOGGER.info("Executed query: {}", query);
}

Для проверки выполненных запросов сущностей мы можем использовать метод Statistics#getQueries . Однако по умолчанию Hibernate не собирает никакой статистики, поэтому нам нужно включить эту функцию, установив для свойства hibernate.generate_statistics configuration значение true .


Теперь при выполнении приведенного выше запроса JPQL будут регистрироваться следующие запросы:

-- Executed query: 
select p 
from Post p 
where p.id in :ids

-- Executed query: 
select p 
from Post p 
where p.id in (:ids_0, :ids_1, :ids_2)

Первый запрос-это тот, который мы предоставили, в то время как второй-расширенный запрос, поскольку JDBC не поддерживает привязку массива значений как отдельных В значениях параметров запроса.

API критериев В запросе

Теперь давайте переведем предыдущий запрос JPQL в его эквивалент API критериев:

SessionFactory sessionFactory = entityManagerFactory()
    .unwrap(SessionFactory.class);

Statistics statistics = sessionFactory.getStatistics();
statistics.clear();

doInJPA(entityManager -> {
    CriteriaBuilder builder = entityManager.getCriteriaBuilder();

    CriteriaQuery criteria = builder.createQuery(Post.class);
    Root fromPost = criteria.from(Post.class);

    criteria.where(
        builder.in(
            fromPost.get("id")).value(Arrays.asList(1, 2, 3)
        )
    );

    List posts = entityManager
    .createQuery(criteria)
    .getResultList();
});

for (String query : statistics.getQueries()) {
    LOGGER.info("Executed query: {}", query);
}

Написание запросов API критериев JPA не очень просто. Плагин Codota IDE может помочь вам в написании таких запросов, что повысит вашу производительность.

Для получения более подробной информации о том, как вы можете использовать Codota для ускорения процесса написания запросов API критериев, ознакомьтесь с этой статьей .

При выполнении запроса API критериев Hibernate регистрирует следующие запросы исполняемых сущностей:

-- Executed query: 
select generatedAlias0 from Post as generatedAlias0 where generatedAlias0.id in (:param0)

-- Executed query: 
select generatedAlias0 from Post as generatedAlias0 where generatedAlias0.id in (:param0_0, :param0_1, :param0_2)

Таким образом, каждый запрос сущности, будь то JPQL или API критериев, должен быть расширен, если он содержит предложение IN.

Кэш плана запроса и расширение предложения IN

Теперь, если количество параметров предложения IN изменяется, Hibernate придется скомпилировать значительное количество запросов сущностей, как показано в следующем примере:

SessionFactory sessionFactory = entityManagerFactory()
    .unwrap(SessionFactory.class);
    
Statistics statistics = sessionFactory.getStatistics();
statistics.clear();

doInJPA(entityManager -> {
    for (int i = 1; i < 16; i++) {
        getPostByIds(
            entityManager,
            IntStream
                .range(1, i + 1)
                .boxed()
                .toArray(Integer[]::new)
        );
    }
});

assertEquals(
    16L, 
    statistics.getQueryPlanCacheMissCount()
);

for (String query : statistics.getQueries()) {
    LOGGER.info("Executed query: {}", query);
}

При выполнении приведенного выше тестового случая Hibernate генерирует следующие выходные данные:

--Executed query: 
select p 
from Post p 
where p.id in :ids

--Executed query: 
select p 
from Post p 
where p.id in (:ids_0)

--Executed query: 
select p 
from Post p 
where p.id in (:ids_0, :ids_1)

--Executed query: 
select p 
from Post p 
where p.id in (:ids_0, :ids_1, :ids_2)

--Executed query: 
select p 
from Post p 
where p.id in (:ids_0, :ids_1, :ids_2, :ids_3)

--Executed query: 
select p 
from Post p 
where p.id in (:ids_0, :ids_1, :ids_2, :ids_3, :ids_4)

--Executed query: 
select p 
from Post p 
where p.id in (:ids_0, :ids_1, :ids_2, :ids_3, :ids_4, :ids_5)

--Executed query: 
select p 
from Post p 
where p.id in (:ids_0, :ids_1, :ids_2, :ids_3, :ids_4, :ids_5, :ids_6)

--Executed query: 
select p 
from Post p 
where p.id in (:ids_0, :ids_1, :ids_2, :ids_3, :ids_4, :ids_5, :ids_6, :ids_7)

--Executed query: 
select p 
from Post p 
where p.id in (:ids_0, :ids_1, :ids_2, :ids_3, :ids_4, :ids_5, :ids_6, :ids_7, :ids_8)

--Executed query: 
select p from Post p 
where p.id in (:ids_0, :ids_1, :ids_2, :ids_3, :ids_4, :ids_5, :ids_6, :ids_7, :ids_8, :ids_9)

--Executed query: 
select p 
from Post p 
where p.id in (:ids_0, :ids_1, :ids_2, :ids_3, :ids_4, :ids_5, :ids_6, :ids_7, :ids_8, :ids_9, :ids_10)

--Executed query: 
select p 
from Post p 
where p.id in (:ids_0, :ids_1, :ids_2, :ids_3, :ids_4, :ids_5, :ids_6, :ids_7, :ids_8, :ids_9, :ids_10, :ids_11)

--Executed query: 
select p 
from Post p 
where p.id in (:ids_0, :ids_1, :ids_2, :ids_3, :ids_4, :ids_5, :ids_6, :ids_7, :ids_8, :ids_9, :ids_10, :ids_11, :ids_12)

--Executed query: 
select p 
from Post p 
where p.id in (:ids_0, :ids_1, :ids_2, :ids_3, :ids_4, :ids_5, :ids_6, :ids_7, :ids_8, :ids_9, :ids_10, :ids_11, :ids_12, :ids_13)

--Executed query: 
select p 
from Post p 
where p.id in (:ids_0, :ids_1, :ids_2, :ids_3, :ids_4, :ids_5, :ids_6, :ids_7, :ids_8, :ids_9, :ids_10, :ids_11, :ids_12, :ids_13, :ids_14)

Таким образом, хотя один и тот же JPQL выполняется несколько раз из-за расширения предложения IN, Hibernate генерирует 15 запросов JPQL, которые необходимо проанализировать и скомпилировать отдельно.

statistics.getQueryPlanCacheMissCount() возвращает значение 16, которое включает исходный запрос JPQL и 15 запросов JPQL, полученных после расширения предложения IN. По этой причине Кэш плана запроса не помогает в этой ситуации.

Заполнение параметров в предложении

Как уже объяснялось в этой статье , Hibernate предлагает функцию заполнения параметров в предложении, которая может помочь нам уменьшить количество пропусков в кэше плана запроса, которое мы видели ранее.

Чтобы включить заполнение параметров в предложении, необходимо включить следующее свойство конфигурации:


После включения спящий режим.query.in_clause_parameter_padding свойство, мы можем ожидать всего 6 расширений запросов В предложении:

Statistics statistics = sessionFactory.getStatistics();
statistics.clear();

doInJPA(entityManager -> {
    for (int i = 1; i < 16; i++) {
        getPostByIds(
            entityManager,
            IntStream
                .range(1, i + 1)
                .boxed()
                .toArray(Integer[]::new)
        );
    }
});

assertEquals(
    6L, 
    statistics.getQueryPlanCacheMissCount()
);

for (String query : statistics.getQueries()) {
    LOGGER.info("Executed query: {}", query);
}

При запуске приведенного выше тестового примера мы действительно видим, что было выполнено только 6 расширений запросов:

-- Executed query: 
select p 
from Post p 
where p.id in :ids

-- Executed query: 
select p 
from Post p 
where p.id in (:ids_0)

-- Executed query: 
select p 
from Post p 
where p.id in (:ids_0, :ids_1)

-- Executed query: 
select p 
from Post p 
where p.id in (:ids_0, :ids_1, :ids_2, :ids_3)

-- Executed query: 
select p 
from Post p 
where p.id in (:ids_0, :ids_1, :ids_2, :ids_3, :ids_4, :ids_5, :ids_6, :ids_7)

-- Executed query: 
select p 
from Post p 
where p.id in (:ids_0, :ids_1, :ids_2, :ids_3, :ids_4, :ids_5, :ids_6, :ids_7, :ids_8, :ids_9, :ids_10, :ids_11, :ids_12, :ids_13, :ids_14, :ids_15)

Круто, правда?

Вывод

Таким образом, когда запросы сущностей JPQL или API критериев выполняются с переменным числом значений параметров привязки В предложении, вы можете повысить производительность приложения, включив режим гибернации.запрос.in_clause_parameter_padding свойство конфигурации.

Кроме того, благодаря статистике кэша плана запросов , включенной в Hibernate 5.4 Статистика , вы можете лучше понять, что делает Hibernate за кулисами.