ORM (Object-Relational Mapping)의 "N + 1 선택 문제"란 무엇입니까?


1596

"N + 1 선택 문제"는 일반적으로 ORM (Object-Relational Mapping) 토론에서 문제로 언급되며, 오브젝트에서 단순 해 보이는 것에 대해 많은 데이터베이스 쿼리를 작성해야하는 것과 관련이 있음을 이해합니다. 세계.

누구든지 문제에 대한 자세한 설명이 있습니까?


2
이것은 n + 1 문제 를 이해하는 것에 대한 훌륭한 설명과 훌륭한 링크입니다 . 또한 architects.dzone.com/articles/how-identify-and-resilve-n1
ace와


이 문제에 대한 해결책을 찾는 모든 사람들 에게이 문제를 설명하는 게시물이 있습니다. stackoverflow.com/questions/32453989/…
damndemon

2
답을 고려할 때 이것을 1 + N 문제라고해서는 안됩니까? 이것이 용어 인 것처럼 보이므로 특히 OP를 묻지는 않습니다.
user1418717

답변:


1015

Car개체 컬렉션 (데이터베이스 행)이 있고 각 개체 Car컬렉션 Wheel(행)도 있다고 가정 해 봅시다 . 즉, CarWheel는 일대 다 관계입니다.

이제 모든 자동차를 반복하고 각 자동차에 대해 바퀴 목록을 인쇄해야한다고 가정 해 봅시다. 순진한 O / R 구현은 다음을 수행합니다.

SELECT * FROM Cars;

그리고 각각에 대해 Car:

SELECT * FROM Wheel WHERE CarId = ?

즉, 자동차에 대해 하나의 선택을 한 다음 N 개의 추가 선택을합니다. 여기서 N은 총 자동차 수입니다.

또는 모든 바퀴를 가져 와서 메모리에서 조회를 수행 할 수 있습니다.

SELECT * FROM Wheel

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

참조 : Hibernate를 통한 Java Persistence , 13 장.


139
"이것은 나쁘다"를 명확히하기 위해 SELECT * from Wheel;-N + 1 대신에 1 개의 선택 ( )으로 모든 바퀴를 얻을 수 있습니다. N이 크면 성능 적중이 매우 클 수 있습니다.
tucuxi

211
@ tucuxi 나는 당신이 틀린 것에 대해 너무 많은 공의를 가지고 놀랐습니다. 데이터베이스는 인덱스에 매우 적합하며 특정 CarID에 대한 쿼리를 수행하면 매우 빠르게 반환됩니다. 그러나 모든 휠이 한 번 있으면 응용 프로그램에서 색인이 생성되지 않은 CarID를 검색해야합니다. 이는 느립니다. 데이터베이스에 도달하는 주요 대기 시간 문제가 없다면 n + 1이 실제로 더 빠릅니다. 그렇습니다. 다양한 실제 코드로 벤치마킹했습니다.
Ariel

73
@ariel '올바른'방법은 CarId로 정렬 된 모든 휠 을 가져 오고 (1 개의 선택), CarId보다 자세한 정보가 필요한 경우 모든 자동차에 대해 두 번째 쿼리를 작성 하십시오 (총 2 개의 쿼리). 인쇄 작업이 최적화되었으며 인덱스 나 보조 스토리지가 필요하지 않았습니다 (결과를 반복해서 다운로드 할 필요가 없음). 당신은 잘못된 것을 벤치마킹했습니다. 그래도 벤치 마크를 확신한다면 실험 및 결과를 설명하는 더 긴 의견 (또는 전체 답변)을 게시 하시겠습니까?
tucuxi

92
"최대 절전 모드 (다른 ORM 프레임 워크에 익숙하지 않음)는이를 처리하는 몇 가지 방법을 제공합니다." 그리고이 방법은?
Tima

58
@Ariel 별도의 컴퓨터에서 데이터베이스 및 애플리케이션 서버로 벤치 마크를 실행하십시오. 내 경험에 따르면 데이터베이스로의 왕복은 쿼리 자체보다 오버 헤드가 더 많이 듭니다. 그렇습니다. 쿼리는 정말 빠르지 만, 문제가 발생한 것은 왕복입니다. "WHERE Id = const "를 "WHERE Id IN ( const , const , ...)"로 변환했으며 그로부터 수십 배가 증가했습니다.
Hans

110
SELECT 
table1.*
, table2.*
INNER JOIN table2 ON table2.SomeFkId = table1.SomeId

그러면 table2의 각 자식 행에 대해 table1 결과를 반환하여 table2의 자식 행이 중복되는 결과 집합을 얻을 수 있습니다. 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"로 Home 인스턴스를 채운 다음 한 번의 쿼리로 Dave, John 및 Mike의 People 인스턴스로 Inhabitants 배열을 채워야합니다.

위에서 사용한 것과 동일한 주소에 대한 N + 1 쿼리는 다음과 같습니다.

Id Address
1  22 Valley St

같은 별도의 쿼리로

SELECT * FROM Person WHERE HouseId = 1

다음과 같은 별도의 데이터 세트가 생성됩니다.

Name    HouseId
Dave    1
John    1
Mike    1

최종 결과는 단일 쿼리에서 위와 동일합니다.

단일 선택의 장점은 원하는 모든 데이터를 미리 얻을 수 있다는 것입니다. N + 1의 장점은 쿼리 복잡성이 감소하고 하위 결과 집합이 첫 번째 요청시에만로드되는 지연로드를 사용할 수 있다는 것입니다.


4
n + 1의 다른 장점은 데이터베이스가 인덱스에서 직접 결과를 반환 할 수 있기 때문에 더 빠르다는 것입니다. 조인을 수행 한 다음 정렬하려면 임시 테이블이 필요합니다. n + 1을 피하는 유일한 이유는 데이터베이스와 통신하는 대기 시간이 많은 경우입니다.
Ariel

17
인덱싱 및 정렬 가능한 필드에서 조인하므로 조인 및 정렬이 매우 빠릅니다. 'n + 1'은 얼마나 큽니까? n + 1 문제가 대기 시간이 긴 데이터베이스 연결에만 적용된다고 진지하게 믿고 있습니까?
tucuxi

9
@ariel-벤치 마크가 정확하더라도 N + 1이 "가장 빠름"이라는 조언은 잘못되었습니다. 어떻게 가능합니까? en.wikipedia.org/wiki/Anecdotal_evidence 및이 질문에 대한 다른 답변의 의견도 참조하십시오 .
휘트니 랜드

7
@ 아리엘-나는 그것을 잘 이해했다고 생각합니다 :). 나는 당신의 결과가 하나의 조건 세트에만 적용된다는 것을 지적하려고합니다. 나는 그 반대를 보여준 반례를 쉽게 구성 할 수 있었다. 말이 돼?
휘트니 랜드

13
다시 말하면 SELECT N + 1 문제는 핵심입니다. 검색 할 레코드가 600입니다. 한 번의 쿼리로 600 개 또는 600 개의 쿼리에서 한 번에 하나씩 600 개를 모두 얻는 것이 더 빠릅니다. MyISAM을 사용하지 않거나 정규화되지 않은 / 잘못 색인 된 스키마가없는 경우 (ORM에 문제가없는 경우) 올바르게 조정 된 db는 2ms 동안 600 개의 행을 반환하고 개별 행을 반환합니다. 각각 약 1ms. 우리는 종종 조인 (밀리 초)의 N + 1 개 촬영 수백을 볼 수 있도록 단지 몇 소요

64

제품과 일대 다 관계를 가진 공급 업체. 한 공급 업체는 많은 제품을 보유하고 있습니다.

***** 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 선택 문제입니다!


3
공급 업체는 1을 선택하고 N은 제품을 선택해야합니까?
bencampbell_14

@bencampbell_ 네, 처음에는 같은 느낌이었습니다. 그러나 그의 예를 들어, 많은 공급 업체에게 하나의 제품입니다.
Mohd Faizan Khan

38

평판이 충분하지 않기 때문에 다른 답변에 직접 의견을 말할 수 없습니다. 그러나 역사적으로 조인을 처리 할 때 많은 dbms가 상당히 열악했기 때문에 문제가 본질적으로 발생한다는 점에 주목할 가치가 있습니다 (MySQL은 특히 주목할만한 예입니다). 따라서 n + 1은 조인보다 훨씬 빠릅니다. 그리고 n + 1을 향상시킬 수있는 방법이 있지만 여전히 조인이 필요하지 않습니다. 이것은 원래 문제와 관련이 있습니다.

그러나 MySQL은 이제 조인 할 때보 다 훨씬 뛰어납니다. 내가 처음 MySQL을 배웠을 때 나는 조인을 많이 사용했다. 그런 다음 속도가 느린 것을 발견하고 대신 코드에서 n + 1로 전환했습니다. 그러나 최근에는 MySQL을 처음 사용했을 때보 다 MySQL을 처리하는 데 훨씬 능숙했기 때문에 조인으로 다시 전환했습니다.

요즘에는 올바르게 인덱스 된 테이블 집합에 대한 간단한 조인이 성능 측면에서 거의 문제가되지 않습니다. 성능이 저하되면 색인 힌트를 사용하여 문제를 해결하는 경우가 많습니다.

이것은 MySQL 개발 팀 중 하나에 의해 여기에서 논의됩니다 :

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

요약은 다음과 같습니다. MySQL과의 대단한 성능으로 인해 과거에 조인을 피한 경우 최신 버전에서 다시 시도하십시오. 당신은 아마 즐겁게 놀랄 것입니다.


7
MySQL의 초기 버전을 관계형 DBMS라고 부르는 것은 상당히 확장 된 일입니다. 만약 그러한 문제에 직면 한 사람들이 실제 데이터베이스를 사용하고 있다면 그러한 종류의 문제는 발생하지 않았을 것입니다. ;-)
Craig

2
흥미롭게도, INNODB 엔진의 도입 및 후속 최적화를 통해 이러한 유형의 문제 중 많은 부분이 MySQL에서 해결되었지만 MYISAM이 더 빠르다고 생각하기 때문에 홍보하려는 사람들에게 여전히 어려움을 겪을 것입니다.
Craig

5
참고로, JOINRDBMS에서 사용되는 3 가지 일반적인 알고리즘 중 하나를 중첩 루프라고합니다. 기본적으로 후드 아래에서 N + 1 선택입니다. 유일한 차이점은 DB가 해당 경로를 범주 적으로 내리는 클라이언트 코드가 아니라 통계 및 인덱스를 기반으로 사용하도록 지능적으로 선택했다는 것입니다.
Brandon

2
@ 브랜든 네! JOIN 힌트 및 INDEX 힌트와 마찬가지로 모든 경우에 특정 실행 경로를 강제 적용해도 데이터베이스를 거의 이길 수는 없습니다. 데이터베이스는 거의 항상 데이터를 얻기위한 최적의 접근 방식을 선택하는 데 매우 능숙합니다. dbs의 초기에는 db를 동축하기 위해 독특한 방식으로 질문을 '구문'해야 할 수도 있지만 수십 년의 세계적 수준의 엔지니어링 후에 데이터베이스에 관계형 질문을하고 그것을 허용함으로써 최고의 성능을 얻을 수 있습니다. 해당 데이터를 가져오고 조립하는 방법을 정리하십시오.

3
데이터베이스는 인덱스와 통계를 사용하는 것뿐만 아니라 모든 작업도 로컬 I / O이며, 대부분 디스크보다 효율적인 캐시에 대해 작동합니다. 데이터베이스 프로그래머는 이러한 종류의 것들을 최적화하는 데 많은주의를 기울입니다.
Craig

27

이 문제 때문에 Django의 ORM에서 멀어졌습니다. 기본적으로 시도하고하면

for p in person:
    print p.car.colour

ORM은 모든 사람 (일반적으로 Person 객체의 인스턴스)을 행복하게 반환하지만 각 Person에 대한 자동차 테이블을 쿼리해야합니다.

이것에 대한 간단하고 매우 효과적인 접근 방식은 내가 " fanfolding " 이라고 부르는 것입니다. 관계형 데이터베이스의 쿼리 결과가 쿼리가 구성된 원래 테이블에 다시 매핑되어야한다는 무의미한 아이디어를 피할 수 있습니다.

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

파이썬 용 팬 폴딩 구현에 대해서는 이 웹 페이지 를 참조하십시오 .


10
내가 미쳐 가고 있다고 생각했기 때문에 내가 당신의 게시물에 걸려 너무 기뻐요. N + 1 문제에 대해 알게되었을 때, 나의 즉각적인 생각은 잘 되었습니까? 필요한 모든 정보가 포함 된보기를 작성하고 그보기에서 가져 오십시오. 당신은 내 입장을 확인했습니다. 감사합니다.
개발자 :

14
이 문제 때문에 Django의 ORM에서 멀어졌습니다. 응? Django는 select_related이것을 해결하기위한 것입니다. 사실 문서는 예제와 유사한 예제로 시작합니다 p.car.colour.
Adrian17

8
이것은 우리가 지금 select_related()그리고 prefetch_related()장고에 있는 오래된 답변 입니다.
Mariusz Jamro

1
멋있는. 그러나 select_related()친구는와 같은 조인에 대한 분명히 유용한 외삽 법을 수행하지 않는 것 같습니다 LEFT OUTER JOIN. 문제는 인터페이스 문제가 아니지만 객체와 관계형 데이터를 매핑 할 수 있다는 이상한 생각과 관련이 있습니다.
rorycl

26

이것은 매우 일반적인 질문 이므로이 답변을 기반으로하는이 기사를 작성 했습니다.

N + 1 쿼리 문제는 무엇입니까

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

N 값이 클수록 쿼리가 더 많이 실행 될수록 성능 영향이 커집니다. 또한 느리게 실행되는 쿼리를 찾는 데 도움이 되는 느린 쿼리 로그 와 달리 N + 1 문제는 각각의 추가 쿼리가 느리게 쿼리 로그를 트리거하지 않도록 충분히 빠르게 실행되므로 발견되지 않습니다.

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

일대 다 테이블 관계 를 형성하는 다음 post 및 post_comments 데이터베이스 테이블이 있다고 가정합시다 .

<code> post </ code> 및 <code> post_comments </ code> 테이블

다음과 같은 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 title각각 post_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 및 최대 절전 모드를 사용할 때는 N + 1 쿼리 문제를 트리거 할 수있는 몇 가지 방법이 있으므로 이러한 상황을 피할 수있는 방법을 아는 것이 중요합니다.

다음 예제에서는 postpost_comments테이블을 다음 엔티티에 맵핑 한다고 가정하십시오.

<code> Post </ code> 및 <code> PostComment </ code> 엔티티

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.EAGERJPA 연관에 암시 적 또는 명시 적으로 사용 하는 것은 필요한 더 많은 데이터를 가져올 것이기 때문에 나쁜 생각입니다. FetchType.EAGER또한 이 전략은 N + 1 쿼리 문제도 발생하기 쉽습니다.

불행히도 @ManyToOne@OneToOne연결 FetchType.EAGER은 기본적으로 사용 되므로 매핑이 다음과 같은 경우

@ManyToOne
private Post post;

당신이 사용하는 FetchType.EAGER잊지 때마다 사용, 전략, 그리고 JOIN FETCH일부로드 할 때 PostCommentJPQL 또는 기준 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

때문에 실행되는 추가 SELECT 문 주목 post협회가 이전에 반환을 가져올 수있다 ListPostComment실체를.

find메소드를 호출 할 때 사용하는 기본 페치 계획과 달리 EnrityManagerJPQL 또는 Criteria API 조회는 JOIN FETCH를 자동으로 주입하여 Hibernate가 변경할 수없는 명시 적 계획을 정의합니다. 따라서 수동으로 수행해야합니다.

post연결 이 전혀 필요하지 않은 경우 FetchType.EAGER가져 오기를 피할 수있는 방법이 없기 때문에 사용할 때 운이 좋지 않습니다. 따라서 FetchType.LAZY기본적 으로 사용 하는 것이 좋습니다 .

그러나 post연결 을 사용 JOIN FETCH하려면 N + 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.EAGER가져 오기 전략을 피해야하는 이유에 대한 자세한 내용은 이 기사 도 확인하십시오 .

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

최대 절전 모드는 단일 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연관이 느리게 페치 되므로, 로그 메시지를 빌드하기 위해 지연 연관에 액세스 할 때 보조 SQL 문이 실행됩니다.

다시, 수정은 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 조회 문제를 계속 트리거 할 수 있습니다.

@OneToOne연결에 의해 생성 된 N + 1 쿼리 문제를 극복하는 방법에 대한 자세한 내용은 이 문서를 확인 하십시오 .

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

데이터 액세스 계층에서 N + 1 쿼리 문제를 자동으로 감지하려는 경우이 문서에서는db-util 오픈 소스 프로젝트 를 사용하여이를 수행하는 방법에 대해 설명합니다 .

먼저 다음 Maven 종속성을 추가해야합니다.

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

db-util오픈 소스 프로젝트 에 대한 자세한 내용은 이 기사를 확인 하십시오 .


그러나 이제 페이지 매김에 문제가 있습니다. 자동차가 10 대인 경우 각 자동차에 4 개의 바퀴가 있고 페이지 당 5 대의 자동차로 페이지를 매기고 싶습니다. 그래서 당신은 기본적으로 있습니다 SELECT cars, wheels FROM cars JOIN wheels LIMIT 0, 5. 그러나 LIMIT는 루트 절뿐만 아니라 전체 결과 집합을 제한하기 때문에 5 바퀴가 달린 2 대의 자동차 (4 개의 바퀴가있는 첫 번째 차와 1 개의 바퀴가있는 두 번째 차)입니다.
CappY

2
나는 그것에 대한 기사 도 있습니다.
Vlad Mihalcea

기사 감사합니다. 읽을 게요 빠른 스크롤을 통해-솔루션이 Window 함수라는 것을 알았지 만 MariaDB에서는 상당히 새롭기 때문에 문제는 이전 버전에서 지속됩니다. :)
CappY December

@ VladMihalcea, N + 1 문제를 설명하면서 ManyToOne 사례를 언급 할 때마다 기사 또는 게시물에서 지적했습니다. 그러나 실제로 N + 1 문제와 관련된 OneToMany 사례에 주로 관심이있는 사람들. OneToMany 사례를 참조하고 설명해 주시겠습니까?
JJ 빔

18

회사와 직원이 있다고 가정하십시오. 회사에는 많은 직원이 있습니다 (예 : EMPLOYEE에는 필드 COMPANY_ID가 있음).

일부 O / R 구성에서 맵핑 된 Company 오브젝트가 있고 해당 Employee 오브젝트에 액세스하면 O / R 도구는 모든 직원에 대해 하나의 선택을 수행하지만, SQL을 직접 수행하는 경우에는 가능합니다 select * from employees where company_id = XX. 따라서 N (직원 수) + 1 (회사)

EJB Entity Bean의 초기 버전이 작동 한 방식입니다. 나는 최대 절전 모드와 같은 것들이 이것으로 사라 졌다고 생각하지만 확실하지 않습니다. 대부분의 도구에는 일반적으로 매핑 전략에 대한 정보가 포함됩니다.


18

다음은 문제에 대한 좋은 설명입니다

이제 문제를 이해 했으므로 일반적으로 쿼리에서 조인 페치를 수행하면 피할 수 있습니다. 이는 기본적으로 지연로드 된 오브젝트를 강제로 가져 오므로 n + 1 쿼리 대신 하나의 쿼리에서 데이터가 검색됩니다. 도움이 되었기를 바랍니다.


17

NHenbernate에서 N + 1 문제 선택 주제에서 Ayende 게시물을 확인하십시오 .

기본적으로 NHibernate 또는 EntityFramework와 같은 ORM을 사용할 때 일대 다 (마스터-세부) 관계가 있고 각 마스터 레코드 당 모든 세부 사항을 나열하려면 N + 1 쿼리를 호출해야합니다. 데이터베이스, "N"은 마스터 레코드 수 : 모든 마스터 레코드를 가져 오는 쿼리 1 개, 마스터 레코드 당 하나의 쿼리를 통해 마스터 레코드 당 모든 세부 정보를 가져옵니다.

더 많은 데이터베이스 쿼리 호출 → 대기 시간 증가 → 응용 프로그램 / 데이터베이스 성능 저하

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


3
조인은 직교 곱을 초래할 수 있기 때문에 좋은 해결책이 아닙니다 (종종). 결과 행 수에 루트 테이블 결과 수에 각 자식 테이블의 결과 수를 곱한 값이 포함됩니다. 여러 군집 수준에 비해 특히 나쁩니다. 각 게시물에 100 개의 "게시물"과 각 게시물에 10 개의 "댓글"이있는 20 개의 "블로그"를 선택하면 결과 행이 20000 개가됩니다. NHibernate는 "일괄 처리 크기"(부모 ID에 절이있는 하위 선택) 또는 "subselect"와 같은 해결 방법이 있습니다.
Erik Hart

14

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


13

내 견해로는 Hibernate Pitfall : 관계가 게으른 이유 는 실제 N + 1 문제와 정확히 반대입니다.

올바른 설명이 필요하면 최대 절전 모드-19 장 : 성능 향상-페치 전략을 참조하십시오 .

선택 가져 오기 (기본값)는 N + 1 선택 문제에 매우 취약하므로 조인 가져 오기를 활성화 할 수 있습니다.


2
최대 절전 모드 페이지를 읽었습니다. 그것은 무슨 말을하지 않습니다 N + 1 개 선택의 문제는 사실 이다 . 그러나 조인을 사용하여 문제를 해결할 수 있다고 말합니다.
Ian Boyd

3
하나의 select 문에서 여러 부모에 대한 자식 개체를 선택하려면 select 페치에 batch-size가 필요합니다. 부속 선택이 다른 대안이 될 수 있습니다. 여러 계층 구조 수준이 있고 데카르트 제품이 생성되면 조인이 실제로 나빠질 수 있습니다.
Erik Hart

10

제공된 링크에는 n + 1 문제의 간단한 예가 있습니다. Hibernate에 적용하면 기본적으로 같은 것에 대해 이야기하는 것입니다. 개체를 쿼리하면 엔터티가로드되지만 다른 구성이없는 한 모든 연결이 지연로드됩니다. 따라서 루트 개체에 대한 쿼리 하나와 이들 각각에 대한 연결을로드하는 다른 쿼리가 있습니다. 반환 된 100 개의 개체는 하나의 초기 쿼리를 의미하고 각각에 대한 연결을 얻기 위해 100 개의 추가 쿼리를 의미합니다. n + 1

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


9

백만장 자에는 N 대의 자동차가 있습니다. 모든 바퀴를 얻고 싶습니다.

하나의 쿼리로 모든 자동차를로드하지만 각 (N) 자동차에 대해 휠로드에 대해 별도의 쿼리가 제출됩니다.

소송 비용:

인덱스가 램에 적합하다고 가정합니다.

페이로드 로딩을위한 1 + N 쿼리 구문 분석 및 계획 + 인덱스 검색 및 1 + N + (N * 4) 플레이트 액세스.

인덱스가 램에 맞지 않는다고 가정하십시오.

최악의 경우 추가 비용 1 + N 플레이트 액세스 인덱스로드.

요약

병목은 플레이트 액세스 (HDD에서 초당 70 회 임의 액세스) 열성적인 조인 선택은 페이로드에 대해 플레이트에 1 + N + (N * 4) 번 액세스합니다. 따라서 인덱스가 램에 적합하면 문제가 없으며 램 작업 만 관련되므로 충분히 빠릅니다.


9

N + 1 선택 문제는 고통이며 단위 테스트에서 이러한 경우를 감지하는 것이 좋습니다. 주어진 테스트 방법이나 임의의 코드 블록으로 실행되는 쿼리 수를 확인하는 작은 라이브러리를 개발했습니다.- JDBC 스니퍼

테스트 클래스에 특수 JUnit 규칙을 추가하고 테스트 메소드에 예상되는 쿼리 수와 함께 주석을 추가하십시오.

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

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

5

다른 사람들 이보다 우아하게 언급 한 문제는 OneToMany 열의 카티 전 곱을 가지고 있거나 N + 1 선택을하고 있다는 것입니다. 가능한 거대한 결과 집합이나 데이터베이스와의 대화가 가능합니다.

나는 이것이 언급되지 않았지만 이것이이 문제를 해결 한 방법에 놀랐습니다 ... 나는 반 임시 id 테이블을 만듭니다. 조항 제한 이있는 경우 IN ()에도이 작업을 수행합니다. .

이것은 모든 경우에 적용되지는 않지만 (아마도 대다수는 아니지만) 직교 제품이 손에 닿지 않도록 많은 하위 객체가있는 경우 특히 효과적입니다. OneToMany 열이 결과가 열의 곱셈)과 일과 같은 배치에 더 가깝습니다.

먼저 부모 객체 ID를 배치로 ids 테이블에 삽입합니다. 이 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열에 SELECT대해 ids 테이블에 대한 작업을 수행 INNER JOIN하고 자식 테이블에 aWHERE batch_id= (또는 그 반대로) 수행하십시오. 결과 열을 쉽게 병합 할 수 있으므로 id 열을 기준으로 정렬하고 싶습니다 (그렇지 않으면 전체 결과 세트에 대해 HashMap / Table이 필요합니다).

그런 다음 주기적으로 ids 테이블을 정리하십시오.

이것은 사용자가 일종의 벌크 처리를 위해 100 개 이상의 별개의 항목을 선택하는 경우 특히 효과적입니다. 임시 테이블에 100 개의 고유 ID를 넣습니다.

이제 수행중인 쿼리의 수는 OneToMany 열의 수입니다.


1

Matt Solnit의 예를 들어 Car와 Wheels 간의 연관성을 LAZY로 정의하고 일부 Wheels 필드가 필요하다고 가정하십시오. 즉, 첫 번째 선택 후 최대 절전 모드에서 각 자동차에 대해 "Select * from Wheels where car_id = : id"를 수행합니다.

이것은 각 N 차량마다 첫 번째 선택과 더 많은 1 선택을하므로 n + 1 문제라고합니다.

이를 피하려면 최대 절전 모드로 연결을 가져 오도록 연결을 가져 오십시오.

그러나 관련 휠에 여러 번 액세스하지 않으면 LAZY로 유지하거나 Criteria로 페치 유형을 변경하는 것이 좋습니다.


1
다시 말하지만 조인은 특히 두 개 이상의 계층 구조 수준이로드 될 때 좋은 솔루션이 아닙니다. 대신 "subselect"또는 "batch-size"를 확인하십시오. 마지막은 "in"절에서 부모 ID로 하위를로드합니다 (예 : "(1,3,4,6,7,8,11,13)에서 car_id가있는 휠에서 select ...)).
Erik Hart
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.