programing

ORM(Object-Relational Mapping)에서 "N+1 선택 문제"는?

stoneblock 2023. 10. 11. 20:27

ORM(Object-Relational Mapping)에서 "N+1 선택 문제"는?

일반적으로 ORM(Object-Rational Mapping) 논의에서는 "N+1 select problem"이 문제로 언급되는데, 객체 세계에서 단순해 보이는 것에 대해 많은 데이터베이스 쿼리를 해야 하는 것과 관련이 있다고 생각합니다.

그 문제에 대해 더 자세한 설명이 있는 사람이 있습니까?

이 의 있다고.Car행각체행)행Car다 있습니다.Wheel개체(행도 있음). 말로 하자면,즉,CarWheel 대 1입니다.

이제 모든 자동차를 반복해서 운전대 목록을 출력해야 한다고 가정해 보겠습니다.순진한 O/R 구현은 다음과 같은 역할을 수행합니다.

SELECT * FROM Cars;

그리고 각각의 경우:

SELECT * FROM Wheel WHERE CarId = ?

즉, 차량에 대해 한 번 선택한 다음 N이 추가로 선택됩니다. 여기서 N은 총 차량 수입니다.

또는 모든 휠을 사용하여 메모리에서 검색을 수행할 수도 있습니다.

SELECT * FROM Wheel;

이렇게 하면 데이터베이스에 대한 왕복 횟수가 N+1에서 2로 줄어듭니다.대부분의 ORM 도구는 N+1 선택을 방지하는 몇 가지 방법을 제공합니다.

참조: 동면 상태Java Persistence, 13장

N+1 쿼리 문제란?

N+1 쿼리 문제는 데이터 액세스 프레임워크가 기본 SQL 쿼리를 실행할 때 검색할 수 있었던 동일한 데이터를 가져오기 위해 N개의 추가 SQL 문을 실행했을 때 발생합니다.

N 값이 클수록 더 많은 쿼리가 실행되어 성능에 미치는 영향이 커집니다.또한 느린 실행 쿼리를 찾을 수 있는 느린 쿼리 로그와 달리 각 개별 추가 쿼리가 느린 쿼리 로그를 트리거할 수 없을 만큼 충분히 빠르게 실행되므로 N+1 문제가 발견되지 않습니다.

문제는 전체적으로 응답 시간을 늦추는데 충분한 시간이 걸리는 추가 쿼리를 많이 실행하는 것입니다.

일대일 테이블 관계를 형성하는 다음과 같은 post 및 post_comments 데이터베이스 테이블이 있다고 생각해 보겠습니다.

The post and post_comments tables

과 같은 4가지 입니다 4 을 입니다.post행:

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 1', 1)
 
INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 2', 2)
 
INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 3', 3)
 
INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 4', 4)

그리고 저희도 4개를 만들겠습니다.post_comment자식 레코드:

INSERT INTO post_comment (post_id, review, id)
VALUES (1, 'Excellent book to understand Java Persistence', 1)
 
INSERT INTO post_comment (post_id, review, id)
VALUES (2, 'Must-read for Java developers', 2)
 
INSERT INTO post_comment (post_id, review, id)
VALUES (3, 'Five Stars', 3)
 
INSERT INTO post_comment (post_id, review, id)
VALUES (4, 'A great reference book', 4)

일반 SQL의 N+1 쿼리 문제

post_comments다음 SQL 쿼리 사용:

List<Tuple> comments = entityManager.createNativeQuery("""
    SELECT
        pc.id AS id,
        pc.review AS review,
        pc.post_id AS postId
    FROM post_comment pc
    """, Tuple.class)
.getResultList();

합니다를 post titlepost_comment:

for (Tuple comment : comments) {
    String review = (String) comment.get("review");
    Long postId = ((Number) comment.get("postId")).longValue();
 
    String postTitle = (String) entityManager.createNativeQuery("""
        SELECT
            p.title
        FROM post p
        WHERE p.id = :postId
        """)
    .setParameter("postId", postId)
    .getSingleResult();
 
    LOGGER.info(
        "The Post '{}' got this review '{}'",
        postTitle,
        review
    );
}

하나의 SQL 쿼리 대신 5(1 + 4)를 실행했기 때문에 N+1 쿼리 문제를 트리거합니다.

SELECT
    pc.id AS id,
    pc.review AS review,
    pc.post_id AS postId
FROM post_comment pc
 
SELECT p.title 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.title 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.title FROM post p WHERE p.id = 3
-- The Post 'High-Performance Java Persistence - Part 3' got this review
-- 'Five Stars'
     
SELECT p.title FROM post p WHERE p.id = 4
-- The Post 'High-Performance Java Persistence - Part 4' got this review
-- 'A great reference book'

N+1 쿼리 문제를 해결하는 것은 매우 쉽습니다.다음과 같이 원래 SQL 쿼리에서 필요한 모든 데이터를 추출하기만 하면 됩니다.

List<Tuple> comments = entityManager.createNativeQuery("""
    SELECT
        pc.id AS id,
        pc.review AS review,
        p.title AS postTitle
    FROM post_comment pc
    JOIN post p ON pc.post_id = p.id
    """, Tuple.class)
.getResultList();
 
for (Tuple comment : comments) {
    String review = (String) comment.get("review");
    String postTitle = (String) comment.get("postTitle");
 
    LOGGER.info(
        "The Post '{}' got this review '{}'",
        postTitle,
        review
    );
}

이번에는 SQL 쿼리를 하나만 실행하여 사용하고자 하는 모든 데이터를 가져옵니다.

JPA 및 최대 절전 모드에 대한 N+1 쿼리 문제

JPA 및 Hibernate를 사용할 때 N+1 쿼리 문제를 트리거할 수 있는 몇 가지 방법이 있으므로 이러한 상황을 피할 수 있는 방법을 아는 것이 매우 중요합니다.

합니다를 을 생각해 .post그리고.post_comments다음 엔티티에 테이블을 제공합니다.

Post and PostComment entities

JPA 매핑은 다음과 같습니다.

@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
}

FetchType.EAGER

으로.FetchType.EAGER당신의 JPA 연결을 위해 암시적으로 또는 명시적으로 사용하는 것은 당신이 필요로 하는 더 많은 데이터를 가져올 것이기 때문에 좋지 않은 생각입니다. 외더,더,FetchType.EAGER전략 또한 N+1 쿼리 문제가 발생하기 쉽습니다.

도.@ManyToOne그리고.@OneToOne합니다.FetchType.EAGER기본적으로 매핑이 다음과 같은 경우:

@ManyToOne
private Post post;

입니다를 .FetchType.EAGER략을 하는 것을 마다.JOIN FETCHPostCommentJPQL 또는 기준 API 쿼리가 있는 엔티티:

List<PostComment> comments = entityManager
.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

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
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

가 하십시오하기 합니다.post다를 .ListPostComment

할 때 findEntityManager할 수 JPQL Criteria API 는 JOIN FETCH 를하여 Hibernate 합니다.그래서 수동으로 해야 합니다.

이 요.post다를 할 때 . 사용할 때 운이 없습니다.FetchType.EAGER그것을 가져오는 것을 피할 방법이 없기 때문입니다.때문에다를 것이 .FetchType.LAZY

하지만, 만일 당신이 사용하고 싶다면.post, 그러면할 수 .JOIN FETCHN+1 쿼리 문제를 방지하려면:

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
    
-- The Post 'High-Performance Java Persistence - Part 1' got this review 
-- 'Excellent book to understand Java Persistence'

-- The Post 'High-Performance Java Persistence - Part 2' got this review 
-- 'Must-read for Java developers'

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

-- The Post 'High-Performance Java Persistence - Part 4' got this review 
-- 'A great reference book'

FetchType.LAZY

FetchType.LAZY명시적으로 모든 연결에 대해 N+1 문제에 부딪힐 수 있습니다.

에는.post연결은 다음과 같이 매핑됩니다.

@ManyToOne(fetch = FetchType.LAZY)
private Post post;

, .PostComment엔티티:

List<PostComment> comments = entityManager
.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

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연관성:

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'

때문에.post연결을 느리게 가져오면 로그 메시지를 만들기 위해 2차 SQL 문이 laze 연결에 액세스할 때 실행됩니다.

다시 말씀드리지만, 수정은 다음을 추가하는 것으로 구성됩니다.JOIN FETCHJPQL 쿼리에 대한 절:

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()
    );
}

로, 처럼.FetchType.EAGER예를 들어, 이 JPQL 쿼리는 단일 SQL 문을 생성합니다.

FetchType.LAZY입니다의 @OneToOneJPA 관계, 여전히 N+1 쿼리 문제를 트리거할 수 있습니다.

N+1 쿼리 문제를 자동으로 탐지하는 방법

데이터 액세스 계층에서 N+1 쿼리 문제를 자동으로 탐지하려면 오픈 소스 프로젝트를 사용할 수 있습니다.

먼저 다음 메이븐 종속성을 추가해야 합니다.

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>db-util</artifactId>
    <version>${db-util.version}</version>
</dependency>

후에는 요만 하면 됩니다.SQLStatementCountValidator생성되는 기본 SQL 문을 주장하는 유틸리티:

SQLStatementCountValidator.reset();

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

SQLStatementCountValidator.assertSelectCount(1);

FetchType.EAGER위의 테스트 케이스를 실행하면 다음과 같은 테스트 케이스 오류가 발생합니다.

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

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


-- SQLStatementCountMismatchException: Expected 1 statement(s) but recorded 3 instead!
SELECT 
table1.*
, table2.*
INNER JOIN table2 ON table2.SomeFkId = table1.SomeId

그러면 표 2의 각 하위 행에 대한 표 1 결과를 표 2의 각 하위 행에 대해 반환하여 중복을 발생시키는 결과 집합을 얻을 수 있습니다.O/R 매핑기는 고유한 키 필드를 기준으로 table1 인스턴스를 구별한 다음 table2 열을 모두 사용하여 하위 인스턴스를 채워야 합니다.

SELECT table1.*

SELECT table2.* WHERE SomeFkId = #

N+1은 첫 번째 쿼리가 기본 개체를 채우고 두 번째 쿼리가 반환되는 고유한 기본 개체 각각에 대한 모든 하위 개체를 채우는 것입니다.

고려 사항:

class House
{
    int Id { get; set; }
    string Address { get; set; }
    Person[] Inhabitants { get; set; }
}

class Person
{
    string Name { get; set; }
    int HouseId { get; set; }
}

그리고 비슷한 구조의 테이블."22 Valley St" 주소에 대한 단일 쿼리는 다음을 반환할 수 있습니다.

Id Address      Name HouseId
1  22 Valley St Dave 1
1  22 Valley St John 1
1  22 Valley St Mike 1

O/RM은 홈 인스턴스를 ID=1, Address="22 Valley St"로 채운 다음, 거주자 배열에 Dave, John 및 Mike에 대한 People 인스턴스를 하나의 쿼리로 채워야 합니다.

위에서 사용한 동일한 주소에 대한 N+1 쿼리는 다음과 같은 결과를 가져옵니다.

Id Address
1  22 Valley St

와 같은 별개의 질문으로

SELECT * FROM Person WHERE HouseId = 1

그리고 결과적으로 다음과 같은 별도의 데이터 세트가 생겨납니다.

Name    HouseId
Dave    1
John    1
Mike    1

그리고 최종 결과는 단일 질의로 위와 같습니다.

단일 선택의 장점은 모든 데이터를 미리 파악할 수 있다는 점이며, 이는 궁극적으로 원하는 것이 될 수도 있습니다.N+1의 장점은 쿼리 복잡성이 줄어들고 첫 번째 요청 시에만 하위 결과 세트가 로드되는 레이지 로드를 사용할 수 있습니다.

제품과 일대일 관계에 있는 공급업체.한 공급업체에 많은 제품이 있습니다.

***** Table: Supplier *****
+-----+-------------------+
| ID  |       NAME        |
+-----+-------------------+
|  1  |  Supplier Name 1  |
|  2  |  Supplier Name 2  |
|  3  |  Supplier Name 3  |
|  4  |  Supplier Name 4  |
+-----+-------------------+

***** Table: Product *****
+-----+-----------+--------------------+-------+------------+
| ID  |   NAME    |     DESCRIPTION    | PRICE | SUPPLIERID |
+-----+-----------+--------------------+-------+------------+
|1    | Product 1 | Name for Product 1 |  2.0  |     1      |
|2    | Product 2 | Name for Product 2 | 22.0  |     1      |
|3    | Product 3 | Name for Product 3 | 30.0  |     2      |
|4    | Product 4 | Name for Product 4 |  7.0  |     3      |
+-----+-----------+--------------------+-------+------------+

요인:

  • 공급업체에 대한 레이지 모드가 "true"(기본값)로 설정됨

  • 제품에 대한 쿼리에 사용되는 가져오기 모드는 선택입니다.

  • 가져오기 모드(기본값):공급업체 정보 접근

  • 캐싱이 처음으로 작동하지 않습니다.

  • 공급업체에 접근함

가져오기 모드는 가져오기 선택(기본값)입니다.

// It takes Select fetch mode as a default
Query query = session.createQuery( "from Product p");
List list = query.list();
// Supplier is being accessed
displayProductsListWithSupplierName(results);

select ... various field names ... from PRODUCT
select ... various field names ... from SUPPLIER where SUPPLIER.id=?
select ... various field names ... from SUPPLIER where SUPPLIER.id=?
select ... various field names ... from SUPPLIER where SUPPLIER.id=?

결과:

  • 1 상품명세서선택하기
  • N 공급업체문선택

N+1선택문제입니다!

저는 평판이 좋지 않기 때문에 다른 답변에 대해 직접적으로 언급할 수 없습니다.하지만 이 문제는 역사적으로 조인을 처리하는 데 있어 많은 dbms가 상당히 부실했기 때문에 근본적으로 발생한다는 점에 유의할 필요가 있습니다(MySQL이 특히 주목할 만한 예임).Son+1은 종종 가입보다 눈에 띄게 빠릅니다.그리고 n+1에서 개선할 수 있는 방법이 있지만 여전히 조인은 필요하지 않습니다. 이것이 원래의 문제와 관련된 것입니다.

그러나 MySQL은 가입에 있어서 예전보다 훨씬 더 좋아졌습니다.제가 MySQL을 처음 배울 때는 조인을 많이 사용했습니다.그리고 그들이 얼마나 느리는지 알게 되었고, 대신에 코드에서 n+1로 바꿨습니다.하지만 최근에 가입을 다시 시작하게 되었습니다. MySQL은 처음 사용했을 때보다 훨씬 더 잘 처리할 수 있게 되었기 때문입니다.

요즘에는 성능 측면에서 적절히 색인화된 테이블 집합에 대한 단순한 조인이 문제가 되는 경우가 거의 없습니다.그리고 만약 그것이 성능에 타격을 준다면, 인덱스 힌트를 사용하면 종종 해결됩니다.

여기에서는 MySQL 개발 팀 중 한 명이 이에 대해 설명합니다.

http://jorgenloland.blogspot.co.uk/2013/02/dbt-3-q3-6-x-performance-in-mysql-5610.html

요약하면 다음과 같습니다.이전에 MySQL의 성능이 너무 좋지 않아 가입을 피했다면 최신 버전을 다시 사용해 보십시오.아마 기분 좋게 놀라실 겁니다.

문제에 대한 적절한 설명은 Fabricator 설명서에서 찾을 수 있습니다.

TL;DR

각각의 결과를 반환하는 100개의 쿼리를 발행하는 것보다 100개의 결과를 반환하는 1개의 쿼리를 발행하는 것이 훨씬 빠릅니다.

데이터를 반복하기 전에 모든 데이터를 로드합니다.

더상세한내용

N+1 쿼리 문제는 일반적인 성능 반패턴입니다.다음과 같습니다.

$cats = load_cats();
foreach ($cats as $cat) {
   $cats_hats => load_hats_for_cat($cat);
   // ...
}

load_cats()의 구현은 다.

SELECT * FROM cat WHERE ...

..그리고.load_hats_for_cat($cat)다음과 같은 구현이 있습니다.

SELECT * FROM hat WHERE catID = ...

..코드가 실행되면 "N+1" 쿼리를 실행합니다. 여기서 N은 고양이의 수입니다.

SELECT * FROM cat WHERE ...
SELECT * FROM hat WHERE catID = 1
SELECT * FROM hat WHERE catID = 2
SELECT * FROM hat WHERE catID = 3
SELECT * FROM hat WHERE catID = 4
...

우리는 이 문제 때문에 장고에 있는 ORM을 탈퇴했습니다.기본적으로, 당신이 노력하고 한다면

for p in person:
    print p.car.colour

ORM은 모든 사용자(일반적으로 사용자 개체의 인스턴스)를 만족스럽게 반환하지만, 각 사용자에 대한 카테이블을 쿼리해야 합니다.

이에 대한 간단하고 매우 효과적인 접근 방식은 제가 "팬폴딩"이라고 부르는 것인데, 관계형 데이터베이스의 쿼리 결과를 쿼리가 구성된 원래 테이블로 다시 매핑해야 한다는 터무니없는 생각을 피합니다.

1단계 : 와이드 셀렉트

  select * from people_car_colour; # this is a view or sql function

이것은 다음과 같은 것을 돌려줄 것입니다.

  p.id | p.name | p.telno | car.id | car.type | car.colour
  -----+--------+---------+--------+----------+-----------
  2    | jones  | 2145    | 77     | ford     | red
  2    | jones  | 2145    | 1012   | toyota   | blue
  16   | ashby  | 124     | 99     | bmw      | yellow

2단계: 대상화

세 번째 항목 뒤에 분할할 인수를 사용하여 결과를 일반 개체 생성자로 빨아들입니다.이것은 "jones" 개체가 두 번 이상 만들어지지 않는다는 것을 의미합니다.

3단계: 렌더

for p in people:
    print p.car.colour # no more car queries

파이썬용 팬폴딩 구현에 대해서는 이 웹페이지를 참고하세요.

COMPANY와 EMPANY가 있다고 가정합니다. COMPANY에는 EMPANY가 여러 명 있습니다(즉, EMPANY에는 Field Company_ID가 있습니다).

한 선택을 O/R Company Employee O/R 을 할 수 . 단순한 SQL에서 작업을 수행하는 경우에는 다음과 같은 작업을 수행할 수 있습니다.select * from employees where company_id = XXN)을 더한 값입니다 따라서 N(직원 수) + 1(회사)

EJB Entity Beans의 초기 버전은 이렇게 작동 방식은 다음과 같습니다.Hibernate 같은 것들이 이것을 없앴다고 생각하지만, 확실하지는 않습니다.대부분의 도구는 보통 매핑 전략에 대한 정보를 포함합니다.

여기 문제에 대한 좋은 설명이 있습니다.

이제 문제를 이해했으므로 쿼리에서 조인 페치를 수행하면 일반적으로 문제를 방지할 수 있습니다.이것은 기본적으로 n+1개의 쿼리 대신 하나의 쿼리에서 데이터가 검색되도록 게으른 로드된 개체를 강제로 불러옵니다.도움이 되길 바랍니다.

주제에 대한 Ayende 게시물 확인:NH 절전 모드에서 선택 N + 1 문제를 해결합니다.

기본적으로, Nhibernate나 EntityFramework와 같은 ORM을 사용할 때, 일대일(master-detail) 관계가 있고, 각 마스터 레코드마다 모든 세부 정보를 나열하려면, 데이터베이스에 N + 1 쿼리 호출을 해야 합니다. "N"은 마스터 레코드의 개수입니다. 즉, 모든 마스터 레코드를 얻기 위한 1개의 쿼리와, N개의 쿼리는 마스터 레코드마다 하나씩,마스터 레코드 당 모든 세부 정보를 얻을 수 있습니다.

데이터베이스 쿼리 호출 증가 → 대기 시간 증가 → 애플리케이션/ database 성능 저하

그러나 ORM에는 주로 JOIN을 사용하여 이 문제를 방지하는 옵션이 있습니다.

제 생각에는 하이버네이트 피트폴에 쓰여진 기사는 다음과 같습니다. 인간관계가 게을러져야 하는 이유는 진짜 N+1 이슈와는 정반대입니다.

올바른 설명이 필요한 경우 동면 상태 - 19장: 성능 향상 - 가져오기 전략을 참조하십시오.

선택 가져오기(기본값)는 N+1 선택 문제에 매우 취약하므로 조인 가져오기를 사용하도록 설정할 수 있습니다.

제공된 링크에는 n + 1 문제의 매우 간단한 예가 있습니다.Hibernate에 적용하면 기본적으로 같은 얘기를 하는 겁니다.개체를 쿼리하면 엔티티가 로드되지만 별도로 구성되지 않은 경우 연결이 지연 로드됩니다.따라서 루트 개체에 대한 하나의 쿼리와 이들 각각에 대한 연결을 로드하는 다른 쿼리가 있습니다.반환된 개체 100개는 초기 쿼리 1개를 의미하며, 그 다음 100개의 추가 쿼리를 통해 각각의 n + 1에 대한 연결을 얻습니다.

http://pramatr.com/2009/02/05/sql-n-1-selects-explained/

N+1 select 문제는 골치 아픈 문제이며, 단위 테스트에서 이러한 경우를 감지하는 것이 타당합니다.주어진 테스트 방법으로 실행되는 쿼리의 수를 확인하기 위한 작은 라이브러리를 개발하거나 임의의 코드 블록 - JDBC Sniffer

테스트 클래스에 특별한 JUNit 규칙을 추가하고 테스트 방법에 예상되는 쿼리 수를 포함한 주석을 배치하면 됩니다.

@Rule
public final QueryCounter queryCounter = new QueryCounter();

@Expectation(atMost = 3)
@Test
public void testInvokingDatabase() {
    // your JDBC or JPA code
}

동면 및 스프링 데이터 JPA의 N+1 문제

N+1 문제는 응용 프로그램 계층에서 단일 선택 쿼리에 대해 데이터베이스의 여러 선택 쿼리(정확히는 N+1, 여기서 N = 테이블의 레코드 수)를 실행하는 개체 관계 매핑의 성능 문제입니다.Hibernate & Spring Data JPA는 이 성능 문제를 파악하고 해결할 수 있는 다양한 방법을 제공합니다.

N+1 문제란?

N+1 문제를 이해하기 위해 시나리오를 사용해 보겠습니다.데이터베이스에 DB_USER 테이블에 매핑된 User 객체의 컬렉션이 있고, 각 사용자가 DB_USER_ROLE 테이블에 결합 테이블 DB_USER_ROLE을 사용하여 컬렉션 또는 Role을 매핑했다고 가정합니다.ORM 수준에서 사용자역할많은 관계를 맺습니다.

Entity Model
@Entity
@Table(name = "DB_USER")
public class User {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;
    private String name;

    @ManyToMany(fetch = FetchType.LAZY)                   
    private Set<Role> roles;
    //Getter and Setters 
 }

@Entity
@Table(name = "DB_ROLE")
public class Role {

    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private Long id;

    private String name;
    //Getter and Setters
 }

사용자는 다양한 역할을 가질 수 있습니다. 역할들은 게으르게 수행됩니다.이제 이 테이블에서 모든 사용자를 가져오고사용자에 대한 역할을 인쇄하려고 합니다.매우 순진한 Object Relational 구현 - findAllBy 메서드를 사용하는 UserRepository

public interface UserRepository extends CrudRepository<User, Long> {

    List<User> findAllBy();
}

ORM에 의해 실행되는 동등한 SQL 쿼리는 다음과 같습니다.

먼저 모든 사용자 가져오기 (1)

Select * from DB_USER;

그런 다음 N번 실행된 각 사용자에 대한 역할을 가져옵니다(여기서 N은 사용자 수).

Select * from DB_USER_ROLE where userid = <userid>;

따라서 사용자에 대해 하나의 선택이 필요하고, 각 사용자에 대해 역할을 가져오는 N 추가 선택이 필요합니다. 여기서 N은 총 사용자 수입니다.이것은 ORM전형적인 N+1 문제입니다.

식별 방법은?

최대 절전 모드는 콘솔/로그에서 SQL 로깅을 활성화하는 추적 옵션을 제공합니다.로그를 사용하면 hibernate가 특정 통화대해 N+1 쿼리를 발행하고 있는지 쉽게 알 수 있습니다.

지정된 선택 쿼리에 대해 SQL에 대한 항목이 여러 개 표시되면 N+1 문제 때문일 가능성이 높습니다.

N+1 해상도

SQL 수준에서 ORM이 N+1을 피하기 위해 달성해야 할 것은 두 테이블을 결합하는 쿼리를 실행하고 결합된 결과를 단일 쿼리로 얻는 것입니다.

단일 쿼리의 모든 것(사용자 및 역할)을 검색하는 Fetch Join SQL

또는 일반 SQL

select user0_.id, role2_.id, user0_.name, role2_.name, roles1_.user_id, roles1_.roles_id from db_user user0_ left outer join db_user_roles roles1_ on user0_.id=roles1_.user_id left outer join db_role role2_ on roles1_.roles_id=role2_.id

Hibernate & Spring Data JPA는 N+1 ORM 문제를 해결하기 위한 메커니즘을 제공합니다.

1. 스프링 데이터 JPA 접근 방식:

Spring Data JPA를 사용하는 경우 EntityGraph를 사용하거나 fetch join을 사용하여 선택 쿼리를 사용하는 두 가지 옵션이 있습니다.

public interface UserRepository extends CrudRepository<User, Long> {

    List<User> findAllBy();             

    @Query("SELECT p FROM User p LEFT JOIN FETCH p.roles")  
    List<User> findWithoutNPlusOne();

    @EntityGraph(attributePaths = {"roles"})                
    List<User> findAll();
}

왼쪽 조인 페치를 사용하여 데이터베이스 수준에서 N+1 쿼리가 실행됩니다. AttributePaths를 사용하여 N+1 문제를 해결합니다. Spring Data JPA는 N+1 문제를 방지합니다.

2. 동면 접근 방식:

순수한 최대 절전 모드일 경우 다음과 같은 해결책이 적용됩니다.

HQL 사용:

from User u *join fetch* u.roles roles roles

기준 API 사용:

Criteria criteria = session.createCriteria(User.class);
criteria.setFetchMode("roles", FetchMode.EAGER);

이러한 접근 방식은 모두 유사하게 작동하며 왼쪽 조인 페치와 함께 유사한 데이터베이스 쿼리를 발행합니다.

다른 사람들이 더 우아하게 언급한 문제는 OneToMany 열의 데카르트 곱이 있거나 N+1 Selections를 수행하고 있다는 것입니다.가능한 거대한 결과 세트 또는 데이터베이스와의 수다.

이것이 언급되지 않은 것은 놀랍지만, 이것이 제가 이 문제를 해결한 방법입니다.나는 반-일시적 ID 테이블을 만듭니다.조항 제한이 있을저도 이렇게 합니다.

은 모든 경우에 효과가 있는 것은 이 감당할 수 , 많은 이다(), (, ) )에 .OneToMany열 결과 수는 열의 곱이 됩니다)와 배치형 작업에 더 가깝습니다.

먼저 상위 개체 ID를 배치로 ID 테이블에 삽입합니다.이 batch_id는 우리가 앱에서 생성하고 보유하고 있는 것입니다.

INSERT INTO temp_ids 
    (product_id, batch_id)
    (SELECT p.product_id, ? 
    FROM product p ORDER BY p.product_id
    LIMIT ? OFFSET ?);

각 .OneToMany.SELECTINNER JOIN 테이블을 WHERE batch_id=(또는 그 반대).id 열로 순서를 정하면 결과 열 병합이 쉬워지기 때문입니다(그렇지 않으면 전체 결과 집합에 대해 HashMap/Table이 필요하므로 그다지 나쁘지 않을 수 있습니다).

그럼 주기적으로 ID 테이블만 청소하시면 됩니다.

이것은 사용자가 일종의 대량 처리를 위해 100개 정도의 개별 항목을 선택하는 경우에도 특히 잘 작동합니다.100개의 다른 ID를 임시 테이블에 놓습니다.

현재 수행 중인 쿼리 수는 OneToMany 열 개수에 따라 결정됩니다.

기술 스택 구현에 대한 세부 사항을 살펴보면, 구조적으로 N + 1 문제에 대해 다음과 같은 두 가지 솔루션이 있습니다.

  • Have Only 1 - big query - with Joins.이렇게 하면 특히 자식 레코드가 여러 개 있는 경우 많은 정보가 데이터베이스에서 응용 프로그램 계층으로 전송됩니다.데이터베이스의 일반적인 결과는 객체의 그래프가 아닌 행 집합입니다(다른 DB 시스템의 경우 해결책이 있음).
  • 두 개(또는 더 많은 자녀를 위해 가입해야 함) 쿼리 - 부모와 자녀가 있는 후에 하나 - ID별로 쿼리하고 자녀를 매핑합니다.이렇게 하면 DB 계층과 APP 계층 간의 데이터 전송이 최소화됩니다.

N+1의 일반화

N+1 문제는 서버에서 합리적으로 실행될 수 있는 루프를 클라이언트로 이동하는 문제의 ORM 고유 이름입니다.일반적인 문제는 ORM에만 국한된 것이 아니며, 원격 API로도 가능합니다.이 기사에서는 API를 한 번만 호출하는 것이 아니라 N번만 호출하는 경우 JDBC 왕복 비용이 얼마나 많이 드는지 보여드렸습니다.예제의 차이점은 Oracle PL/SQL 프로시저를 호출하는지 여부입니다.

  • dbms_output.get_lines(출, 개령)
  • dbms_output.get_line) (회 기 출)

논리적으로 동일하지만 서버와 클라이언트 간의 지연 시간 때문에 한 번만 기다리는 대신 루프에 N번의 지연 시간 대기 시간을 추가하게 됩니다.

ORM 케이스

실제로 ORM-y N+1 문제는 ORM에 특정한 것도 아니며, PL/SQL에서 이와 같은 작업을 수행할 때 수동으로 자신의 쿼리를 실행함으로써 ORM을 달성할 수 있습니다.

-- This loop is executed once
for parent in (select * from parent) loop

  -- This loop is executed N times
  for child in (select * from child where parent_id = parent.id) loop
    ...
  end loop;
end loop;

(이 경우) 조인을 사용하여 이를 구현하는 것이 훨씬 더 나을 것입니다.

for rec in (
  select *
  from parent p
  join child c on c.parent_id = p.id
)
loop
  ...
end loop;

는 한 로해시 조인(되며를 들어, (PL/SQL) 로되었으며(SQL) , (해시 조인) 에서 으로써 루프를 도 있습니다.O(N)()이 O(N log N) 포함함)

N+1 문제 자동 탐지

JDBC를 사용하는 경우 jOOQ를 JDBC 프록시로 사용하여 N+1 문제를 자동으로 탐지할 수 있습니다. jOOQ의 구문 분석기는 SQL 쿼리를 정규화하고 부모 및 자식 쿼리의 연속 실행에 대한 데이터를 캐시합니다.쿼리가 완전히 동일하지는 않지만 의미적으로 동일한 경우에도 작동합니다.

Matt Solnit의 예를 들어, 자동차와 바퀴 사이의 연관성을 LAZY로 정의하고 휠 필드가 필요하다고 생각해 보십시오.즉, 첫 번째 선택 후 동면 상태가 각 차량에 대해 "Select * from Wheels where car_id = :id"를 수행합니다.

이것은 N차 한 대당 첫 번째 선택과 더 많은 한 대의 선택을 하게 하기 때문에 n+1 문제라고 불립니다.

이를 방지하려면 연결을 빨리 가져와 최대 절전 모드가 조인과 함께 데이터를 로드하도록 합니다.

그러나 여러 번 연결된 휠에 액세스하지 못하는 경우에는 게으르게 유지하거나 기준을 사용하여 페치 유형을 변경하는 것이 좋습니다.

N+1 SELECT 문제는 특히 도메인이 큰 프로젝트에서 성능을 저하시키기 시작하는 순간까지 발견하기가 매우 어렵습니다.eager loading을 추가하여 문제를 해결하더라도 추가 개발을 통해 해결책이 깨지거나 다른 곳에서 다시 N+1 SELECT 문제가 발생할 수 있습니다.

JPA 기반 Spring Boot Java 애플리케이션의 이러한 문제를 해결하기 위해 오픈 소스 라이브러리 jplusone을 만들었습니다.이 라이브러리는 두 가지 주요 기능을 제공합니다.

  1. SQL 문을 트리거한 JPA 작업의 실행과 관련된 보고서를 생성하고 관련된 응용프로그램의 소스 코드에 배치합니다.
2020-10-22 18:41:43.236 DEBUG 14913 --- [main] c.a.j.core.report보고서 생성기:뿌리com.adgadev.jplusone.test.domain.서점BookshopControllerTest.should GetBookDetailsLazy (BookshopControllerTest.java:65)com.adgadev.jplusone.test.domain.서점BookshopController.Get 샘플북 게으른 로딩 사용(BookshopController.java:31)com.adgadev.jplusone.test.domain.서점BookshopService.Get 샘플북 상세보기 게으른 로딩 사용 [PROXY]세션 경계작업 [암시]com.adgadev.jplusone.test.domain.서점BookshopService.샘플북 상세보기 게으른 로딩 사용(BookshopService)java:35)com.adgadev.jplusone.test.domain.서점작성자.getName [PROXY]com.adgadev.jplusone.test.domain.서점작성자 [엔터티 가져오기]문 [읽기][...] 중에서 고르다저자0_왼쪽 아우터 조인 장르1_ on author0_.genre_id= genre1_.id어디에작성자 0_.id=1작업 [암시]com.adgadev.jplusone.test.domain.서점BookshopService.샘플북 상세보기 게으른 로딩 사용(BookshopService)java:36)com.adgadev.jplusone.test.domain.서점작성자.countWrittenBooks(작성자.javacom.adgadev.jplusone.test.domain.서점작가.도서 [Fetching Collection]문 [읽기][...] 중에서 고르다도서 0_어디에books0_.작성자_id=1
  1. 애플리케이션이 얼마나 효과적으로 JPA를 사용하고 있는지 확인하는 테스트를 작성할 수 있는 API 제공(즉, 지연 로딩 작업의 양을 주장)
@SpringBootTest
class LazyLoadingTest {

    @Autowired
    private JPlusOneAssertionContext assertionContext;

    @Autowired
    private SampleService sampleService;

    @Test
    public void shouldBusinessCheckOperationAgainstJPlusOneAssertionRule() {
        JPlusOneAssertionRule rule = JPlusOneAssertionRule
                .within().lastSession()
                .shouldBe().noImplicitOperations().exceptAnyOf(exclusions -> exclusions
                        .loadingEntity(Author.class).times(atMost(2))
                        .loadingCollection(Author.class, "books")
                );

        // trigger business operation which you wish to be asserted against the rule,
        // i.e. calling a service or sending request to your API controller
        sampleService.executeBusinessOperation();

        rule.check(assertionContext);
    }
}

ORM "N 더하기 하나" 문제

"N 더하기 하나" 문제는 ORM(Object-Relational Mapping) 프레임워크를 사용할 때 발생할 수 있는 일반적인 성능 문제입니다.ORM 프레임워크는 객체 지향 프로그래밍 언어로 데이터베이스 테이블을 객체에 매핑하는 데 사용되는 도구입니다.특정 방식으로 ORM을 사용하여 관계형 데이터베이스에서 데이터를 가져올 때 이 문제가 발생합니다.

두 "N 1" 입니다.Customer그리고.Order 수 . .Customer그리고.Order탁자들클래스 및같은 을 사용하여 합니다.ORM에서는 클래스 및 참조와 같은 개체 지향 개념을 사용하여 이러한 관계를 정의합니다.

이제 고객의 주문과 함께 모든 고객을 회수하고 싶다고 가정해 보겠습니다.ORM에서는 다음과 같은 쿼리를 사용할 수 있습니다.

customers = Customer.objects.all()

for customer in customers:
    orders = customer.orders.all()
    # Do something with the orders

합니다를 Customer.objects.all()각 . 합니다를 customer.orders.all().

이 방법의 문제는 데이터베이스에 여러 개의 쿼리가 실행된다는 것입니다.예를 들어, 100명의 고객이 있는 경우 이 코드는 101개의 쿼리를 실행합니다. 하나는 모든 고객을 검색하는 것이고 다른 하나는 각 고객에 대한 주문을 검색하는 것입니다(따라서 "N 더하기 하나" 문제라는 이름).이는 특히 대규모 데이터셋을 다룰 때 성능에 상당한 영향을 미칠 수 있습니다.

ORM 프레임워크가 필요한 모든 데이터를 단일 쿼리로 가져오는 대신 각 고객의 주문에 대해 별도의 쿼리를 수행하기 때문에 "N plus one" 문제가 발생합니다.이러한 동작은 ORM 프레임워크에서는 다른 시나리오에서 성능 문제가 될 수 있는 모든 관련 데이터를 불필요하게 로드하지 않도록 하기 위한 기본값인 경우가 많습니다.

"N 더하기 하나" 문제를 완화하기 위해 ORM 프레임워크는 일반적으로 긴급 로딩 또는 명시적 조인과 같은 데이터 검색을 최적화하는 방법을 제공합니다.긴급 로딩을 사용하면 필요한 데이터를 단일 쿼리로 가져올 수 있으므로 데이터베이스 왕복 횟수를 줄일 수 있습니다.포함할 관계를 지정하면 ORM 프레임워크는 필요한 모든 데이터를 한 번에 검색하는 보다 효율적인 쿼리를 생성할 수 있습니다.

"N plus one" 문제와 그 해결책의 시연으로서, 다음은 SQLAlchemy를 사용하여 ORM에서 방출되는 실제 SQL을 보여줍니다.

N 더하기 하나의 문제를 포함한 원본 ORM 쿼리(고객의 경우 1개의 쿼리, 각 고객의 주문에 대해 N개의 쿼리):

with Session(engine) as session:
    customers = session.scalars(select(Customer))
    for customer in customers:
        print(f"> Customer: #{customer.customer_id}")
        for order in customer.orders:
            print(f">   order #{order.order_id} at {order.order_datetime}")
-- This query gets all customers:
SELECT customer.customer_id, ...
FROM customer

-- The following SQL is executed once for each customer:
SELECT "order".order_id AS order_order_id, ...
FROM "order"
WHERE "order".customer_id = %(param_1)s

eager loading을 지정한 후(포함)selectinload() 두 합니다.), 합니다.

with Session(engine) as session:
    customers = session.scalars(
        select(Customer).options(selectinload(Customer.orders)))
    for customer in customers:
        print(f"> Customer: #{customer.customer_id}")
        for order in customer.orders:
            print(f">   order #{order.order_id} at {order.order_datetime}")
SELECT customer.customer_id, ...
FROM customer

-- This loads all the orders you need in one query:
SELECT "order".order_id AS order_order_id, ...
FROM "order"
WHERE "order".customer_id IN (%(primary_keys_1)s, %(primary_keys_2)s, ...)

또는 명시적으로 필수 필드에 가입 및 쿼리합니다(1개의 쿼리만 필요).

with Session(engine) as session:
    stmt = (
        select(
            Customer.customer_id,
            Order.order_id,
            Order.order_datetime,
        )
        .select_from(Customer)
        .join(Customer.orders)
        .order_by(Customer.customer_id)
    )
    results = session.execute(stmt)

    current_customer_id = None
    for row in results:
        customer_id = row.customer_id
        if current_customer_id != customer_id:
            current_customer_id = customer_id
            print(f"> Customer: #{current_customer_id}")
        print(f">   order #{row.order_id} at {row.order_datetime}")
SELECT customer.customer_id, "order".order_id, ...
FROM customer
JOIN "order" ON customer.customer_id = "order".customer_id
ORDER BY customer.customer_id

요약하면, ORM의 "N 더하기 하나" 문제는 프레임워크가 컬렉션의 각 항목에 대한 관련 데이터를 검색하기 위해 여러 쿼리를 실행할 때 발생하며, 이로 인해 상당한 성능 오버헤드가 발생합니다.데이터 검색 전략을 최적화하여 이 문제를 이해하고 해결하면 ORM 기반 응용 프로그램의 효율성을 향상시킬 수 있습니다.

언급URL : https://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem-in-orm-object-relational-mapping