중첩 루프에서 느리게 실행되는 쿼리를 최적화하는 방법 (내부 조인)


39

TL; DR

이 질문은 계속 견해를 가지고 있기 때문에 여기에 요약하여 새로 온 사람들이 역사를 겪지 않아도되도록합니다.

JOIN table t ON t.member = @value1 OR t.member = @value2 -- this is slow as hell
JOIN table t ON t.member = COALESCE(@value1, @value2)    -- this is blazing fast
-- Note that here if @value1 has a value, @value2 is NULL, and vice versa

나는 이것이 모든 사람의 문제가 아니라는 것을 알고 있지만 ON 절의 감도를 강조함으로써 올바른 방향을 찾는 데 도움이 될 수 있습니다. 어쨌든 원문은 미래 인류학자를위한 것입니다.

원문

다음과 같은 간단한 쿼리를 고려하십시오 (3 개의 테이블 만 관련됨)

    SELECT

        l.sku_id AS ProductId,
        l.is_primary AS IsPrimary,
        v1.category_name AS Category1,
        v2.category_name AS Category2,
        v3.category_name AS Category3,
        v4.category_name AS Category4,
        v5.category_name AS Category5

    FROM category c4
    JOIN category_voc v4 ON v4.category_id = c4.category_id and v4.language_code = 'en'

    JOIN category c3 ON c3.category_id = c4.parent_category_id
    JOIN category_voc v3 ON v3.category_id = c3.category_id and v3.language_code = 'en'

    JOIN category c2 ON c2.category_id = c3.category_id
    JOIN category_voc v2 ON v2.category_id = c2.category_id and v2.language_code = 'en'

    JOIN category c1 ON c1.category_id = c2.parent_category_id
    JOIN category_voc v1 ON v1.category_id = c1.category_id and v1.language_code = 'en'

    LEFT OUTER JOIN category c5 ON c5.parent_category_id = c4.category_id
    LEFT OUTER JOIN category_voc v5 ON v5.category_id = c5.category_id and v5.language_code = @lang

    JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
    (
        l.category_id = c4.category_id OR
        l.category_id = c5.category_id
    )

    WHERE c4.[level] = 4 AND c4.version_id = 5

이것은 매우 간단한 쿼리이며 혼란스러운 부분은 마지막 범주 조인입니다. 범주 수준 5가 존재하거나 존재하지 않을 수 있기 때문에 이런 식입니다. 쿼리가 끝날 때마다 제품 ID (SKU ID) 당 범주 정보를 찾고 있는데, 이것이 매우 큰 테이블 category_link가 나오는 곳입니다. 마지막으로, 테이블 #Ids는 10'000 ID를 포함하는 임시 테이블입니다.

실행되면 다음과 같은 실제 실행 계획이 나타납니다.

실제 실행 계획

보시다시피, 거의 90 %의 시간이 중첩 루프 (내부 조인)에 사용됩니다. 중첩 루프에 대한 추가 정보는 다음과 같습니다.

중첩 루프 (내부 조인)

가독성을 위해 쿼리 테이블 이름을 편집했기 때문에 테이블 이름이 정확히 일치하지는 않지만 일치하기 쉽습니다 (ads_alt_category = category). 이 쿼리를 최적화 할 수있는 방법이 있습니까? 또한 프로덕션에는 임시 테이블 #Id가 존재하지 않고 저장 프로 시저에 전달 된 동일한 10,000 ID의 테이블 값 매개 변수입니다.

추가 정보:

  • category_id 및 parent_category_id의 범주 인덱스
  • category_id, language_code의 category_voc 색인
  • sku_id, category_id의 category_link 색인

편집 (해결)

허용 된 답변에서 지적한 것처럼 문제는 category_link JOIN의 OR 절이었습니다. 그러나 허용 된 답변에서 제안 된 코드는 원래 코드보다 매우 느리고 느립니다. 훨씬 빠르고 깨끗한 솔루션은 현재 JOIN 조건을 다음과 같이 바꾸는 것입니다.

JOIN category_link l on l.sku_id IN (SELECT value FROM @p1) AND l.category_id = COALESCE(c5.category_id, c4.category_id)

이 미세 조정은 가장 빠른 솔루션이며, 허용 된 답변의 이중 결합에 대해 테스트되었으며 valverij에서 제안한대로 CROSS APPLY에 대해서도 테스트되었습니다.


나머지 쿼리 계획을 확인해야합니다.
RBarryYoung

단지 언급 : 많은 종속 조인 카디널리티 추정 오류가 발생할 가능성이 있습니다. 대부분의 경우 카디널리티 과소 평가로 인해 쿼리 성능이 저하됩니다.
usr

실행 계획이 인덱스를 제안합니까? 또한 임시 테이블에서 기본 키와 인덱스를 설정할 수 있다는 것을 잊지 마십시오 ( 여기의 정보는 여기 참조 )

@rbarry 현재 솔루션을 시도한 후에도 아무것도 얻지

1
UNION으로 쿼리를 복제하고 OR을 제거하는 것은

답변:


17

이 코드 부분에 문제가있는 것으로 보입니다.

JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
(
    l.category_id = c4.category_id OR
    l.category_id = c5.category_id
)

or조인 조건에서 항상 의심됩니다. 한 가지 제안은 이것을 두 개의 조인으로 나누는 것입니다.

JOIN category_link l1 on l1.sku_id in (SELECT value FROM #Ids) and l1.category_id = cr.category_id
left outer join
category_link l1 on l2.sku_id in (SELECT value FROM #Ids) and l2.category_id = cr.category_id

그런 다음이 쿼리를 처리하기 위해 나머지 쿼리를 수정해야합니다. . . coalesce(l1.sku_id, l2.sku_id)예를 들어 select조항에서.


특정에서 수행되는 필터링의 양이 결합을 통해, 또한 변화하는 테스트 것 JOINA와를 CROSS APPLYIN로 전환하기 EXISTS에서 APPLYWHERE절.

고든 고마워, 나는 아침 에이 첫 번째 것을 테스트 할 것입니다. @Valverij, 나는 교차 적용에 익숙하지 않습니다. 솔루션에 대한 답변을 더 자세히 설명 할 수 있습니까? 그래서 가장 빠른 시나리오로 판명되면 투표 할 수 있습니까?

3
문제를 지적한 첫 번째 답변 이었으므로이 답변을 수락하고 있습니다. 그러나 제안 된 솔루션은 원래 코드보다 훨씬 느리고 느립니다. 그러나 OR 절이 문제라는 것을 알면 단순히 ON l.category_id = ISNULL(c5.category_id, c4.category_id트릭으로 대체했습니다 .
Luis Ferrao

1
@LuisFerrao. . . 추가 정보를 주셔서 감사합니다. coalesce()푸시가 옵티 마이저를 올바른 방향으로 푸시 한다는 것을 아는 것이 유용합니다 .
Gordon Linoff

9

다른 사용자가 언급했듯이이 조인이 원인 일 수 있습니다.

JOIN category_link l on l.sku_id IN (SELECT value FROM #Ids) AND
(
    l.category_id = c4.category_id OR
    l.category_id = c5.category_id
)

이것들을 여러 개의 조인으로 나누는 것 외에도 시도해 볼 수 있습니다 CROSS APPLY

CROSS APPLY (
    SELECT [some column(s)]
    FROM category_link x
    WHERE EXISTS(SELECT value FROM #Ids WHERE value = x.sku_id)
    AND (x.category_id = c4.category_id OR x.category_id = c5.category_id)        
) l

위의 MSDN 링크에서 :

테이블 반환 함수는 오른쪽 입력으로 작동하고 외부 테이블 식은 왼쪽 입력으로 작동합니다. 오른쪽 입력은 왼쪽 입력에서 각 행에 대해 평가되며 생성 된 행은 최종 출력을 위해 결합됩니다 .

기본적으로, APPLY오른쪽에서 레코드를 먼저 필터링 한 다음 나머지 쿼리에 적용 하는 하위 쿼리와 같습니다 .

이 기사는 그것이 무엇이며 언제 사용 해야하는지 잘 설명합니다 : http://explainextended.com/2009/07/16/inner-join-vs-cross-apply/

그러나 CROSS APPLY가 항상보다 더 빠른 성능을 나타내는 것은 아니라는 점에 유의해야 합니다 INNER JOIN. 많은 상황에서 아마 거의 동일 할 것입니다. 드문 경우이지만 실제로는 느리게 보았습니다 (다시 말해서 이것은 모두 테이블 구조와 쿼리 자체에 달려 있습니다).

일반적으로 조건문이 너무 많은 테이블에 참여하는 경우, APPLY

또한 재미있는 노트 : OUTER APPLY유사한 역할을합니다LEFT JOIN

또한, 내가 EXISTS아니라 내가 선택한 것을 기록해 두십시오 IN. IN하위 쿼리를 수행 할 때는 값을 찾은 후에도 전체 결과 집합을 반환합니다. 그러나을 사용하면 EXISTS일치하는 것을 발견하면 하위 쿼리가 중지됩니다.


이 솔루션을 철저히 테스트했습니다. 글을 쓸 때 속도가 느리지 만 메시지를 시작한 조언을 적용하는 것을 잊었습니다. 교체 AND x.cat = c4.cat OR x.cat = c5.catx.cat = ISNULL(c5.cat, c4.cat)꽤 유익한 때문에 IN 절 치우는 것은 두 번째 빠른 솔루션 및 upvote에 합당이했다.
Luis Ferrao

감사. IN 라인은 실제로 존재하지 않아야합니다 (IN을 사용하거나 OR을 고수로 결정할 수 없었습니다).
valverij
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.