문제

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

누구든지 문제에 대해 더 자세한 설명을 갖고 있습니까?

도움이 되었습니까?

해결책

당신이 컬렉션을 가지고 있다고 가정 해 봅시다 Car 개체(데이터베이스 행) 및 각 Car 컬렉션을 가지고 있습니다 Wheel 개체(행).다시 말해서, Car -> Wheel 1대다 관계입니다.

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

SELECT * FROM Cars;

그런 다음 각각 Car:

SELECT * FROM Wheel WHERE CarId = ?

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

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

SELECT * FROM Wheel

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

참조: Hibernate를 사용한 Java 지속성, 13장.

다른 팁

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은 Home 인스턴스를 ID=1, Address="22 Valley St"로 채운 다음 단 한 번의 쿼리로 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의 장점은 쿼리 복잡성이 감소하고 하위 결과 세트가 첫 번째 요청 시에만 로드되는 지연 로딩을 사용할 수 있다는 것입니다.

제품과 일대다 관계를 갖는 공급업체입니다.하나의 공급자는 많은 제품을 보유(공급)합니다.

***** 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은 특히 주목할만한 예입니다).따라서 n+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의 형편없는 성능 때문에 조인을 피하고 있었다면 최신 버전에서 다시 시도하십시오.당신은 아마도 즐겁게 놀랄 것입니다.

이 문제 때문에 우리는 Django의 ORM에서 멀어졌습니다.기본적으로, 노력하고 실천한다면

for p in person:
    print p.car.colour

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

이에 대한 간단하고 매우 효과적인 접근 방식은 제가 ""라고 부르는 것입니다.부채꼴 모양"는 관계형 데이터베이스의 쿼리 결과가 쿼리가 구성된 원래 테이블에 다시 매핑되어야 한다는 무의미한 생각을 피합니다.

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와 EMPLOYEE가 있다고 가정합니다.회사에는 많은 직원이 있습니다(예:EMPLOYEE에는 COMPANY_ID 필드가 있습니다.

일부 O/R 구성에서는 매핑된 Company 개체가 있고 해당 Employee 개체에 액세스할 때 O/R 도구는 모든 직원에 대해 하나의 선택을 수행합니다. 반면, 직접 SQL로 작업을 수행하는 경우에는 다음과 같은 작업을 수행할 수 있습니다. select * from employees where company_id = XX.따라서 N(직원 수) + 1(회사)

이것이 EJB Entity Bean의 초기 버전이 작동하는 방식입니다.나는 Hibernate와 같은 것들이 이 문제를 해결했다고 믿지만, 확신할 수는 없습니다.대부분의 도구에는 일반적으로 매핑 전략에 대한 정보가 포함되어 있습니다.

문제에 대한 좋은 설명은 다음과 같습니다. https://web.archive.org/web/20160310145416/http://www.realsolve.co.uk/site/tech/hib-tip-pitfall.php?name=why-lazy

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

내 생각에는 다음 글에 쓰여진 것 같다. 최대 절전 모드 함정:관계가 게으른 이유 실제 N+1 문제와 정반대입니다.

정확한 설명이 필요하시면 참고해주세요 최대 절전 모드 - 19장:성능 향상 - 전략 가져오기

Select Fetching (기본값)이 N+1에 매우 취약하므로 문제가 발생하므로 결합 페치를 활성화 할 수 있습니다.

주제에 대한 Ayende 게시물을 확인하세요. NHibernate에서 선택 N + 1 문제 해결

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

데이터베이스 쿼리 호출 증가 --> 지연 시간 증가 --> 애플리케이션/데이터베이스 성능 감소.

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

N+1 쿼리 문제는 연결을 가져오는 것을 잊어버린 다음 연결에 액세스해야 할 때 발생합니다.

List<PostComment> comments = entityManager.createQuery(
    "select pc " +
    "from PostComment pc " +
    "where pc.review = :review", PostComment.class)
.setParameter("review", review)
.getResultList();

LOGGER.info("Loaded {} comments", comments.size());

for(PostComment comment : comments) {
    LOGGER.info("The post title is '{}'", comment.getPost().getTitle());
}

다음 SQL 문이 생성됩니다.

SELECT pc.id AS id1_1_, pc.post_id AS post_id3_1_, pc.review AS review2_1_
FROM   post_comment pc
WHERE  pc.review = 'Excellent!'

INFO - Loaded 3 comments

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

INFO - The post title is 'Post nr. 1'

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

INFO - The post title is 'Post nr. 2'

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

INFO - The post title is 'Post nr. 3'

먼저, Hibernate는 JPQL 쿼리를 실행하고, PostComment 엔터티를 가져옵니다.

그러면 각각에 대해 PostComment, 관련 post 속성은 다음을 포함하는 로그 메시지를 생성하는 데 사용됩니다. Post 제목.

왜냐하면 post 연결이 초기화되지 않은 경우 Hibernate는 Post 보조 쿼리가있는 엔티티 및 n PostComment 엔터티가 있으면 N개 이상의 쿼리가 실행됩니다(따라서 N+1 쿼리 문제).

먼저, 당신이 필요합니다 적절한 SQL 로깅 및 모니터링 이 문제를 발견할 수 있도록 말이죠.

둘째, 이런 종류의 문제는 통합 테스트를 통해 파악하는 것이 좋습니다.당신은 사용할 수 있습니다 생성된 SQL 문의 예상 개수를 검증하는 자동 JUnit 어설션.그만큼 db 단위 프로젝트 이미 이 기능을 제공하고 있으며 이는 오픈 소스입니다.

N+1 쿼리 문제를 식별하면 N 대신 하나의 쿼리로 하위 연결을 가져오려면 JOIN FETCH를 사용해야 합니다..여러 하위 연결을 가져와야 하는 경우 초기 쿼리에서 하나의 컬렉션을 가져오고 보조 SQL 쿼리를 사용하여 두 번째 컬렉션을 가져오는 것이 좋습니다.

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

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

한 백만장자는 N대의 자동차를 가지고 있습니다.당신은 모든 (4) 바퀴를 얻고 싶습니다.

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

소송 비용:

인덱스가 램에 맞는다고 가정합니다.

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

인덱스가 램에 맞지 않는다고 가정합니다.

최악의 경우 인덱스 로딩을 위한 1 + N 플레이트 액세스에 대한 추가 비용이 발생합니다.

요약

병목은 플레이트 접근입니다(ca.HDD에서 초당 2 초당 무작위 액세스) 열망 조인 Select는 페이로드의 플레이트 1 + N + (N * 4) 시간에도 액세스 할 수 있습니다.따라서 인덱스가 램에 맞으면 문제 없습니다. 램 작업만 포함되기 때문에 충분히 빠릅니다.

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

N+1 선택 문제는 고통스럽고 단위 테스트에서 이러한 사례를 감지하는 것이 합리적입니다.나는 주어진 테스트 방법이나 임의의 코드 블록에 의해 실행된 쿼리 수를 확인하기 위한 작은 라이브러리를 개발했습니다. JDBC 스니퍼

테스트 클래스에 특별한 JUnit 규칙을 추가하고 테스트 메소드에 예상되는 쿼리 수를 주석으로 추가하기만 하면 됩니다.

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

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

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

이것이 언급되지 않은 것에 놀랐습니다. 그러나 이것이 이 문제를 해결한 방법입니다... 반 임시 ID 테이블을 만듭니다.. 나는 또한 당신이 있을 때 이것을 합니다 IN () 조항 제한.

이는 모든 경우에 작동하지는 않지만(대다수는 아닐 수도 있음) 데카르트 곱이 손에 닿지 않을 정도로 자식 객체가 많은 경우(예: 많은 경우) 특히 잘 작동합니다. OneToMany 열 결과 수는 열의 곱이 됩니다.) 그리고 작업과 같은 배치에 더 가깝습니다.

먼저 상위 개체 ID를 ids 테이블에 배치로 삽입합니다.이 배치 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 ID 테이블에 INNER JOIN어린이 테이블에 WHERE batch_id= (혹은 그 반대로도).결과 열을 더 쉽게 병합할 수 있도록 id 열을 기준으로 정렬하는 것이 좋습니다(그렇지 않으면 전체 결과 집합에 대해 HashMap/Table이 필요하므로 그다지 나쁘지는 않습니다).

그런 다음 주기적으로 ids 테이블을 정리하면 됩니다.

이는 사용자가 일종의 대량 처리를 위해 100개 정도의 개별 항목을 선택하는 경우에도 특히 효과적입니다.임시 테이블에 100개의 개별 ID를 넣습니다.

이제 수행 중인 쿼리 수는 OneToMany 열 수만큼 달라집니다.

Matt Solnit의 예를 들어 Car와 Wheels 간의 연결을 LAZY로 정의하고 일부 Wheels 필드가 필요하다고 가정해 보겠습니다.이는 첫 번째 선택 후 최대 절전 모드가 각 자동차에 대해 "car_id = :id인 Wheels에서 * 선택"을 수행한다는 것을 의미합니다.

이렇게 하면 각 N개의 차량이 첫 번째 선택을 하고 그 이상 1개의 선택이 이루어지므로 이를 n+1 문제라고 합니다.

이를 방지하려면 연결을 열성적으로 가져오도록 만들어 최대 절전 모드가 조인을 통해 데이터를 로드하도록 합니다.

그러나 관련 Wheels에 여러 번 액세스하지 않는 경우에는 LAZY를 유지하거나 Criteria를 사용하여 가져오기 유형을 변경하는 것이 좋습니다.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top