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? Как и для чего его использовать?
Все вопросы по теме: список
Все темы: список
Вопросы/замечания/предложения/нашли ошибку: напишите мне