37. Расскажите про проблему N+1 Select и путях ее решения.

37. Расскажите про проблему N+1 Select и путях ее решения.

Unknown

Проблема N+1 запросов возникает, когда получение данных из БД выполняется за N дополнительных SQL-запросов для извлечения тех же данных, которые могли быть получены при выполнении основного SQL-запроса.

Допустим у нас есть две сущности Post и PostComment. В БД имеется 4 Post и у каждого из них по одному PostComment:

@Entity(name = "Post")

@Table(name = "post")

public class Post {

    @Id

    private Long id;

    private String title;

    //Getters and setters omitted for brevity

}

@Entity(name = "PostComment")

@Table(name = "post_comment")

public class PostComment {

    @Id

    private Long id;

    @ManyToOne

    private Post post;

    private String review;

    //Getters and setters omitted for brevity

}

N+1 при FetchType.EAGER

Так  как  у  @ManyToOne  план  извлечения  по  умолчанию  -  EAGER,  то  при получении из БД сущности PostComment немедленно будет загружена связанная с ней сущность Post:

List<PostComment> comments = entityManager.createQuery(

      """select pc from PostComment pc""", PostComment.class)

      .getResultList();

Этот SQL-запрос приведет к проблеме N+1 и выполнит больше запросов, чем нужно:

SELECT

    pc.id AS id1_1_,

    pc.post_id AS post_id3_1_,

    pc.review AS review2_1_

FROM

    post_comment pc

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id=1  82

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id=2

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id=3

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id=4

Обратите  внимание  на  дополнительные  4  оператора  SELECT,  которые выполняются, потому что 4 сущности Post должны быть извлечены из БД до возврата списка  из  4  сущностей  PostComment  с  инициализированными  полями  Post. 

Автоматически Hibernate делает это не очень хорошо, порождая количество запросов в БД, равное N+1, а именно один запрос для получения PostComment, и четыре запроса для получения Post для каждого PostComment. В нашем случае это проблема 4+1.

N+1 при FetchType.LAZY

Даже если мы явно переключимся на использование FetchType.LAZY для всех ассоциаций, мы всё равно можем столкнуться с проблемой N+1. Явно укажем над полем Post план извлечения - LAZY:

@ManyToOne(fetch = FetchType.LAZY)

private Post post;

Теперь,  когда  мы  выбираем  все  сущности  PostComment,  Hibernate  выполнит одну инструкцию SQL:

SELECT

    pc.id AS id1_1_,

    pc.post_id AS post_id3_1_,

    pc.review AS review2_1_

FROM

    post_comment pc

Но, если в этом же контексте персистентности, мы обратимся к сущностям Post в PostComment:

for(PostComment comment : comments) {

    LOGGER.info(

        "The Post '{}' got this review '{}'",

        comment.getPost().getTitle(),

        comment.getReview()

    );

}

То мы опять получим проблему N+1:

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id=1

-- The Post 'High-Performance Java Persistence - Part 1' got this review

-- 'Excellent book to understand Java Persistence'

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id=2

-- The Post 'High-Performance Java Persistence - Part 2' got this review

-- 'Must-read for Java developers'

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id=3

-- The Post 'High-Performance Java Persistence - Part 3' got this review

-- 'Five Stars'

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id=4

-- The Post 'High-Performance Java Persistence - Part 4' got this review

-- 'A great reference book'  83

Поскольку Post извлекается лениво, вторая группа SQL-запросов, состоящая в нашем случае из 4 штук, будет выполняться при обращении к ленивым полям Post у каждого PostComment.

Решения проблемы N+1:

  • JOIN FETCH

И при FetchType.EAGER и при FetchType.LAZY нам поможет JPQL-запрос с JOIN FETCH. Опцию «FETCH» можно использовать в JOIN (INNER JOIN или LEFT JOIN) для выборки связанных объектов в одном запросе вместо дополнительных запросов для каждого доступа к ленивым полям объекта.

List<PostComment> comments = entityManager.createQuery("""

    select pc

    from PostComment pc

    join fetch pc.post p

    """, PostComment.class)

.getResultList();

for(PostComment comment : comments) {

    LOGGER.info(

        "The Post '{}' got this review '{}'",

        comment.getPost().getTitle(),

        comment.getReview()

    );

}

На этот раз Hibernate выполнит одну инструкцию SQL:

SELECT

    pc.id as id1_1_0_,

    pc.post_id as post_id3_1_0_,

    pc.review as review2_1_0_,

    p.id as id1_0_1_,

    p.title as title2_0_1_

FROM

    post_comment pc

INNER JOIN

    post p ON pc.post_id = p.id

Использование  LEFT  JOIN  FETCH  аналогично  JOIN  FETCH,  только  будут загружены все сущности из таблицы PostComment, даже те, у которых нет связанной сущности Post (в нашем случае пример не логичный, но понятный).

  • EntityGraph

В случаях, когда нам нужно получить по-настоящему много данных, и у нас jpql запрос - лучше всего использовать EntityGraph.

  • @Fetch(FetchMode.SUBSELECT)

Это  Аннотация  Hibernate,  в  JPA  её  нет.  Можно  использовать  только  с коллекциями. Будет сделан один sql-запрос для получения корневых сущностей и, если в  контексте  персистентности  будет  обращение  к  ленивым  полям-коллекциям,  то выполнится еще один запрос для получения связанных коллекций:

@Entity

public class Customer {

    @Id

    @GeneratedValue

    private Long id;

    @OneToMany(mappedBy ="customer")

    @Fetch(value = FetchMode.SUBSELECT)

    private Set<Order> orders = new HashSet<>();

    // getters and setters

}

Первый запрос:

select ...

from customer customer0_

Второй запрос:

select ...

    from

        order order0_

    where

        order0_.customer_id in (

            select

                customer0_.id

            from

                customer customer0_

        )

  • Batch fetching

Это Аннотация Hibernate, в JPA её нет. Указывается над классом сущности илинад полем коллекции с ленивой загрузкой. Будет сделан один sql-запрос для получения корневых сущностей и, если в контексте персистентности будет обращение к ленивым полям-коллекциям,  то  выполнится  еще  один  запрос  для  получения  связанных коллекций. Изменим пример:

@OneToMany(mappedBy = "customer")

@Fetch(value = FetchMode.SELECT)

@BatchSize(size=5)

private Set<Order> orders = new HashSet<>();

Например,  мы  знаем,  что  в  персистентный  контекст  загружено  12  сущностей Customer, у которых по одному полю-коллекции orders, но так как это @OneToMany, то у них ленивая загрузка по умолчанию и они не загружены в контекст персистентности из БД.

При первом обращении к какому-нибудь полю orders, нам бы хотелось, чтобы для всех  12  сущностей  Customer  были  загружены  их  12  коллекций  Order,  по  одной  для каждой. Но так как у нас @BatchSize(size=5), то Hibernate сделает 3 запроса: в первом и втором получит по пять коллекций, а в третьем получит две коллекции. 

Если мы знаем примерное количество коллекций, которые будут использоваться в  любом  месте  приложения,  то  можно  использовать  @BatchSize  и  указать  нужное количество.

Также аннотация @BatchSize может быть указана у класса. Рассмотрим пример, где  у нас  есть  сущность Order, у  которой  есть поле  типа  Product(не  коллекция). Мы выгрузили  в  контекст  персистентности  27  объектов  Order.  При  обращении  к  полям  85

Product у объектов Order будет инициализировано до 10 ленивых прокси сущностей Product одновременно:

@Entity

class Order {

    @OneToOne(fetch = FetchType.LAZY)

    private Product product;

    ...

}

@Entity

@BatchSize(size=10)

class Product {

    ...

}

Хотя  использовать  @BatchSize  лучше,  чем  столкнуться  с  проблемой  запроса N+1, в большинстве случаев гораздо лучшей альтернативой является использование DTO или JOIN FETCH, поскольку они позволяют получать все необходимые данные одним запросом.

  • HibernateSpecificMapping, SqlResultSetMapping

Для нативных запросов рекомендуется использовать именно их.


Предыдущий вопрос: 36.Что такое Criteria API и для чего он используется?

Следующий вопрос: 38. Что такое Entity Graph? Как и для чего его использовать?

Все вопросы по теме: список

Все темы: список

Вопросы/замечания/предложения/нашли ошибку: напишите мне


Report Page