사용자 정의 함수 관련 최적화 문제


26

한 행만 가져와야하지만 SQL Server가 테이블의 모든 값에 대해 사용자 정의 함수를 호출하기로 결정한 이유를 이해하는 데 문제가 있습니다. 실제 SQL은 훨씬 더 복잡하지만 문제를 다음과 같이 줄일 수있었습니다.

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

이 쿼리의 경우 ORDERLINE에서 반환 된 예상 및 실제 행 수가 1 (기본 키) 인 경우에도 SQL Server는 PRODUCT Table에 존재하는 모든 단일 값에 대해 GetGroupCode 함수를 호출하기로 결정합니다.

쿼리 계획

행 탐색기를 표시하는 계획 탐색기의 동일한 계획 :

탐험가 계획 테이블 :

ORDERLINE: 1.5M rows, primary key: ORDERNUMBER + ORDERLINE + RMPHASE (clustered)
ORDERHDR:  900k rows, primary key: ORDERID (clustered)
PRODUCT:   6655 rows, primary key: PRODUCT (clustered)

스캔에 사용되는 인덱스는 다음과 같습니다.

create unique nonclustered index PRODUCT_FACTORY on PRODUCT (PRODUCT, FACTORY)

이 함수는 실제로 약간 더 복잡하지만 다음과 같은 더미 다중 문 함수에서도 마찬가지입니다.

create function GetGroupCode (@FACTORY varchar(4))
returns @t table(
    TYPE        varchar(8),
    GROUPCODE   varchar(30)
)
as begin
    insert into @t (TYPE, GROUPCODE) values ('XX', 'YY')
    return
end

SQL Server가 상위 1 개 제품을 가져 오도록하여 성능을 "수정"할 수 있었지만 1은 최대입니다.

select  
    S.GROUPCODE,
    H.ORDERCAT
from    
    ORDERLINE L
    join ORDERHDR H
        on H.ORDERID = M.ORDERID
    cross apply (select top 1 P.FACTORY from PRODUCT P where P.PRODUCT = L.PRODUCT) P
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

그런 다음 계획 모양도 원래 예상했던 것으로 변경됩니다.

최고 쿼리 계획

또한 PRODUCT_FACTORY 인덱스가 클러스터 된 인덱스 PRODUCT_PK보다 작은 경우 영향을 미치지 만 쿼리에서 PRODUCT_PK를 사용하도록 강제하더라도 계획은 여전히 ​​원래와 동일하며 6655 번의 함수 호출이 있습니다.

ORDERHDR을 완전히 제외하면 계획은 ORDERLINE과 PRODUCT 사이의 중첩 루프로 시작하고 함수는 한 번만 호출됩니다.

모든 작업이 기본 키를 사용하여 수행되기 때문에 이유가 될 수있는 이유와이를 쉽게 해결할 수없는보다 복잡한 쿼리에서 발생하는 경우 수정하는 방법을 알고 싶습니다.

편집 : 테이블 문을 만듭니다.

CREATE TABLE dbo.ORDERHDR(
    ORDERID varchar(8) NOT NULL,
    ORDERCATEGORY varchar(2) NULL,
    CONSTRAINT ORDERHDR_PK PRIMARY KEY CLUSTERED (ORDERID)
)

CREATE TABLE dbo.ORDERLINE(
    ORDERNUMBER varchar(16) NOT NULL,
    RMPHASE char(1) NOT NULL,
    ORDERLINE char(2) NOT NULL,
    ORDERID varchar(8) NOT NULL,
    PRODUCT varchar(8) NOT NULL,
    CONSTRAINT ORDERLINE_PK PRIMARY KEY CLUSTERED (ORDERNUMBER,ORDERLINE,RMPHASE)
)

CREATE TABLE dbo.PRODUCT(
    PRODUCT varchar(8) NOT NULL,
    FACTORY varchar(4) NULL,
    CONSTRAINT PRODUCT_PK PRIMARY KEY CLUSTERED (PRODUCT)
)

답변:


30

계획을 세 가지 주요 기술적 이유는 다음과 같습니다.

  1. 옵티마이 저의 원가 계산 프레임 워크는 인라인이 아닌 함수를 실제로 지원하지 않습니다 . 함수 정의 내부를 조사하여 비용이 얼마나 드는지 확인하려고 시도하지 않고 고정 비용이 매우 적으며 호출 될 때마다 함수가 1 행의 출력을 생성한다고 추정합니다. 이러한 모델링 가정은 종종 완전히 안전하지 않습니다. 고정 1 행 추측이 고정 100 행 추측으로 대체되므로 2014 년에 새로운 카디널리티 추정기가 활성화되어 상황이 약간 개선되었습니다. 그러나 인라인이 아닌 함수의 컨텐츠 비용은 여전히 ​​지원되지 않습니다.
  2. SQL Server는 처음에 조인을 축소하고 단일 내부 n-ary 논리 조인에 적용합니다. 이를 통해 옵티마이 저가 나중에 조인 주문에 대한 이유를 알 수 있습니다. 단일 n-ary 조인을 후보 조인 순서로 확장하면 나중에 나오고 대체로 휴리스틱을 기반으로합니다. 예를 들어, 내부 조인은 외부 조인 앞에오고 작은 테이블과 선택적 조인은 큰 테이블 앞에 있고 덜 선택적 조인 앞에옵니다.
  3. SQL Server는 비용 기반 최적화를 수행 할 때 노력을 선택적 단계로 분할하여 비용이 많이 드는 저비용 쿼리 최적화 시간을 최소화 할 수 있습니다. 검색 0, 검색 1 및 검색 2의 세 가지 주요 단계가 있습니다. 각 단계에는 진입 조건이 있으며 이후 단계에서는 이전 단계보다 더 최적화 된 탐색이 가능합니다. 쿼리는 가능한 최소 검색 단계 인 단계 0에 적합합니다. 충분한 비용 계획이있어 이후 단계가 입력되지 않습니다.

UDF에 지정된 작은 카디널리티 추정이 적용되면 n-ary 조인 확장 휴리스틱은 불행히도 트리에서 원하는 위치보다 빨리 위치를 변경합니다.

또한 쿼리는 3 개 이상의 조인 (적용 포함)이있어 검색 최적화에 적합합니다. 이상하게 보이는 스캔을 통해 얻을 수있는 최종 물리적 계획은 경험적으로 습득 한 조인 순서를 기반으로합니다. 옵티마이 저가 계획을 "충분히 양호"하다고 간주 할만큼 비용이 적게 듭니다. UDF에 대한 저비용 추정 및 카디널리티는이 초기 마무리에 기여합니다.

검색 0 (트랜잭션 처리 단계라고도 함)은 일반적으로 중첩 루프 조인을 특징으로하는 최종 계획으로 카디널리티가 낮은 OLTP 유형 쿼리를 대상으로합니다. 더 중요한 것은 검색 0은 최적화 프로그램의 탐색 능력 중 비교적 작은 부분 집합 만 실행한다는 것입니다. 이 서브 세트에는 조인 (rule PullApplyOverJoin)을 통해 조회 트리를 적용하는 것이 포함되지 않습니다 . 이것은 테스트 순서에서 조인 위에 UDF 적용을 재배치하여 작업 순서에서 마지막으로 표시되도록 요구되는 것입니다.

옵티마이 저가 순진 중첩 루프 조인 (조인 자체의 조인 술어)과 상관 된 술어가 인덱스 탐색을 사용하여 조인의 내부에 적용되는 상관 인덱스 조인 (적용) 사이에서 결정할 수있는 문제도 있습니다. 후자는 일반적으로 원하는 평면 모양이지만 옵티마이 저는 둘 다를 탐색 할 수 있습니다. 잘못된 원가 계산 및 카디널리티 추정값을 사용하면 제출 된 계획 (스캔 설명)에서와 같이 비적용 NL 조인을 선택할 수 있습니다.

따라서 과도한 리소스를 사용하지 않고 단기간에 좋은 계획을 찾는 데 일반적으로 잘 작동하는 몇 가지 일반적인 최적화 기능과 관련된 여러 가지 상호 작용 이유가 있습니다. 이유 중 하나를 피하면 빈 테이블이 있어도 샘플 쿼리에 대한 '예상'계획 형태를 생성하기에 충분합니다.

검색 0이 비활성화 된 빈 테이블 계획

검색 계획 선택을 피하거나 조기 옵티 마이저 종료를 피하거나 UDF의 비용을 개선 할 수있는 방법은 없습니다 (SQL Server 2014 CE 모델의 향상된 기능 제외). 이로 인해 계획 지침, 수동 쿼리 다시 작성 ( TOP (1)아이디어 포함 또는 중간 임시 테이블 사용) 및 인라인이 아닌 함수와 같이 비용이 많이 드는 '블랙 박스'(QO 관점에서)를 피할 수 있습니다.

현재 조인 축소 작업 중 일부를 막을 수 CROSS APPLY있으므로 OUTER APPLY가능한 대로 다시 작성 하는 것도 가능하지만 원래 쿼리 의미를 보존해야합니다 (예 : NULL옵티마이 저가 교차 적용). 이 동작이 안정적으로 유지되는 것은 아닙니다. 따라서 SQL Server를 패치하거나 업그레이드 할 때마다 관찰 된 동작을 다시 테스트해야합니다.

전반적으로 귀하에게 적합한 솔루션은 당사가 판단 할 수없는 다양한 요인에 따라 다릅니다. 그러나 앞으로도 항상 작동하고 가능한 경우 옵티 마이저와 함께 작동하는 솔루션을 고려하는 것이 좋습니다.


24

이것이 옵티마이 저의 비용 기반 결정이지만 다소 나쁜 결정 인 것 같습니다.

PRODUCT에 50000 개의 행을 추가하면 옵티마이 저는 스캔이 너무 많은 것으로 생각하고 세 번의 탐색과 한 번의 UDF 호출로 계획을 제공합니다.

PRODUCT에서 6655 행에 대한 계획

여기에 이미지 설명을 입력하십시오

PRODUCT에 50000 개의 행이 있으면이 계획을 대신받습니다.

여기에 이미지 설명을 입력하십시오

UDF 호출 비용이 크게 과소 평가 된 것 같습니다.

이 경우 제대로 작동하는 한 가지 해결 방법은 UDF에 대해 외부 적용을 사용하도록 조회를 변경하는 것입니다. PRODUCT 테이블에 몇 개의 행이 있는지에 관계없이 좋은 계획을 얻습니다.

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    outer apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01' and
    S.GROUPCODE is not null

여기에 이미지 설명을 입력하십시오

귀하의 경우 가장 좋은 해결 방법은 필요한 값을 임시 테이블로 가져온 다음 임시 테이블을 UDF에 교차 적용하여 쿼리하는 것입니다. 그렇게하면 UDF가 필요 이상으로 실행되지 않을 것입니다.

select  
    P.FACTORY,
    H.ORDERCATEGORY
into #T
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

select  
    S.GROUPCODE,
    T.ORDERCATEGORY
from #T as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

drop table #T

임시 테이블을 유지하는 대신 top()파생 테이블에서 사용 하여 UDF가 호출되기 전에 SQL Server가 조인 결과를 평가하도록 할 수 있습니다 . SQL Server가 쿼리의 해당 부분에 대한 행을 계산하여 UDF를 사용하기 전에 최상위 숫자를 사용하십시오.

select S.GROUPCODE,
       T.ORDERCATEGORY
from (
     select top(2147483647)
         P.FACTORY,
         H.ORDERCATEGORY
     from    
         ORDERLINE L
         join ORDERHDR H on H.ORDERID = L.ORDERID
         join PRODUCT P  on P.PRODUCT = L.PRODUCT    
     where   
         L.ORDERNUMBER = 'XXX/YYY-123456' and
         L.RMPHASE = '0' and
         L.ORDERLINE = '01'
     ) as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

여기에 이미지 설명을 입력하십시오

모든 작업이 기본 키를 사용하여 수행되기 때문에 이유가 될 수있는 이유와이를 쉽게 해결할 수없는보다 복잡한 쿼리에서 발생하는 경우 수정하는 방법을 알고 싶습니다.

나는 정말로 대답 할 수는 없지만 어쨌든 내가 알고있는 것을 공유해야한다고 생각했습니다. PRODUCT 테이블 스캔이 전혀 고려되지 않는 이유를 모르겠습니다. 그것이 최선의 경우이고 최적화 프로그램이 내가 모르는 UDF를 처리하는 방법에 관한 것들이있을 수 있습니다.

새로운 카디널리티 견적 도구를 사용하면 SQL Server 2014에서 쿼리가 좋은 계획을 얻는다는 추가 관찰이있었습니다. 이는 UDF에 대한 각 호출의 예상 행 수가 SQL Server 2012와 그 이전의 1이 아니라 1이 아니라 100이기 때문입니다. 그러나 여전히 스캔 버전과 검색 버전의 계획간에 동일한 비용 기반 결정을 내립니다. PRODUCT에 500 개 미만 (필자의 경우 497 개)의 행으로 SQL Server 2014에서도 계획의 스캔 버전을 얻을 수 있습니다.


2
어떻게 든 SQL Bits에서 Adam Machanic의 세션을 상기시킵니다 : sqlbits.com/Sessions/Event14/…
James Z
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.