2016-09-27 1 views
2

비슷한 스레드를 보았지만 모두 거대한 데이터베이스 인 것 같습니다. 나는 오늘 아침에 작은 라이브 데이터베이스에서이 작물을 본 후 문제를 보여주기 위해 더미 데이터베이스를 만들었습니다.내 CTE 조인 업데이트가 내 테이블 변수 조인보다 훨씬 느린 이유는 무엇입니까?

이 데이터의 근거는 다음과 같습니다. 회사는 100 명의 고객에 대한 주식 포트폴리오를 추적합니다. 1000 종목에는 각각 4 종의 투자자와 그 비율을 기록한 일일 기록이 있습니다. 불행히도 소유자가 여러 번 나타날 수있는 결함이 있습니다. 이 절차는 데이터를 구문 분석하고 레코드를 분리하여 매일 각 주식에 대해 4 개의 레코드가있는 경우 각 소유자의 포트폴리오 합계를 합산합니다. 그러나 여러 레코드가 있기 때문에 해당 소유자의 값을 과장 할 수 있습니다. 플래그가 삽입되어 이러한 중복을 식별합니다. 나중에 코드에서 각 행의 값에 해당 플래그가 곱해지며 중복 플래그는 0이고 그렇지 않으면 1입니다.

해당 플래그를 업데이트하는 5 가지 방법이 있습니다. 먼저 0으로 시작합니다. 이는 SELECT 문을 기준으로 CTE를 사용하는 것입니다. 그것은 약 0.07 초 걸립니다. 1은 CTE를 사용하여 JOIN을 사용하여 테이블을 업데이트하고 약 48 초가 걸립니다. 2는 CTE 대신 중첩 된 select 문을 사용하며 약 48 초가 걸립니다. 3은 CTE를 테이블 변수로 덤프하고 그와 결합하여 약 0.13 초 걸립니다. 4 카운터 루프를 사용하고 한 번에 한 행을 업데이트하므로 가장 효율적이지 않다고 생각했지만 0.17 초 밖에 걸리지 않았습니다. 5는 CASE 문을 사용하여 CTE에 조인 된 모든 행을 업데이트하며 약 48 초가 걸립니다.

DECLARE @OwnRec TABLE (
     StockID   INT 
    , TradeDate   DATE 
    , Shares   DECIMAL(4,0) 
    , Price    DECIMAL(4,2) 
    , Owner1   INT 
    , Owner1Pct   DECIMAL(3,2) 
    , Owner2   INT 
    , Owner2Pct   DECIMAL(3,2) 
    , Owner3   INT 
    , Owner3Pct   DECIMAL(3,2) 
    , Owner4   INT 
    , Owner4Pct   DECIMAL(3,2) 
    ) 

DECLARE @OwnRec2 TABLE (
     RecID    INT IDENTITY 
    , StockID   INT 
    , TradeDate   DATE 
    , Shares   DECIMAL(4,0) 
    , Price    DECIMAL(4,2) 
    , Owner0   INT 
    , Owner0Pct   DECIMAL(3,2) 
    , OwnerNum   INT 
    , DupeOwner   TINYINT 
    ) 

DECLARE @CullDupe TABLE (
     ID    INT IDENTITY 
    , RecID    INT 
    ) 

DECLARE @Method  INT 
     , @Counter1  INT = 0 
     , @StartTime DATETIME 

--Populate tables with dummy data 
WHILE @Counter1 < 1000 
    BEGIN 
     SET @Counter1 += 1 
     INSERT INTO @OwnRec (
       StockID 
      , TradeDate 
      , Shares  
      , Price  
      , Owner1  
      , Owner1Pct 
      , Owner2  
      , Owner2Pct 
      , Owner3  
      , Owner3Pct 
      , Owner4  
      , Owner4Pct 
      ) 
     SELECT @Counter1 
      , '2016-09-26' 
      , ROUND((RAND() * 1000 + 500)/25,0)*25 
      , ROUND((RAND() * 30 + 20),2) 
      , ROUND((RAND() * 100 + .5),0) 
      , CAST(ROUND((RAND() * 5 + .5),0)*.05 AS DECIMAL(3,2)) 
      , ROUND((RAND() * 100 + .5),0) 
      , CAST(ROUND((RAND() * 5 + .5),0)*.05 AS DECIMAL(3,2)) 
      , ROUND((RAND() * 100 + .5),0) 
      , CAST(ROUND((RAND() * 5 + .5),0)*.05 AS DECIMAL(3,2)) 
      , ROUND((RAND() * 100 + .5),0) 
      , CAST(ROUND((RAND() * 5 + .5),0)*.05 AS DECIMAL(3,2)) 
    END 

SET @Counter1 = 0 

WHILE @Counter1 < 1000 
    BEGIN 
     SET @Counter1 += 1 
     INSERT INTO @OwnRec (
       StockID 
      , TradeDate 
      , Shares  
      , Price  
      , Owner1  
      , Owner1Pct 
      , Owner2  
      , Owner2Pct 
      , Owner3  
      , Owner3Pct 
      , Owner4  
      , Owner4Pct 
      ) 
     SELECT @Counter1 + 1000 
      , '2016-09-27' 
      , Shares 
      , ROUND(Price * ROUND(RAND()*10 + .5,0)*.01+.95,2) 
      , Owner1  
      , Owner1Pct 
      , Owner2  
      , Owner2Pct 
      , Owner3  
      , Owner3Pct 
      , Owner4  
      , Owner4Pct 
      FROM @OwnRec WHERE StockID = @Counter1 
    END 

UPDATE orx 
    SET Owner2Pct = Owner1Pct 
     FROM @OwnRec orx 
      WHERE Owner1 = Owner2 

UPDATE orx 
    SET Owner3Pct = Owner1Pct 
     FROM @OwnRec orx 
      WHERE Owner1 = Owner3 

UPDATE orx 
    SET Owner4Pct = Owner1Pct 
     FROM @OwnRec orx 
      WHERE Owner1 = Owner4 

UPDATE orx 
    SET Owner3Pct = Owner2Pct 
     FROM @OwnRec orx 
      WHERE Owner2 = Owner3 

UPDATE orx 
    SET Owner4Pct = Owner2Pct 
     FROM @OwnRec orx 
      WHERE Owner2 = Owner4 

UPDATE orx 
    SET Owner4Pct = Owner3Pct 
     FROM @OwnRec orx 
      WHERE Owner3 = Owner4 

INSERT INTO @OwnRec2 
    SELECT StockID, TradeDate, Shares, Price, Owner1 AS Owner0, Owner1Pct, 1, 1 AS Owner0Pct 
     FROM @OwnRec 
    UNION 
    SELECT StockID, TradeDate, Shares, Price, Owner2 AS Owner0, Owner2Pct, 2, 1 AS Owner0Pct 
     FROM @OwnRec 
    UNION 
    SELECT StockID, TradeDate, Shares, Price, Owner3 AS Owner0, Owner3Pct, 3, 1 AS Owner0Pct 
     FROM @OwnRec 
    UNION 
    SELECT StockID, TradeDate, Shares, Price, Owner4 AS Owner0, Owner4Pct, 4, 1 AS Owner0Pct 
     FROM @OwnRec 
--END Populate tables with dummy data 

SET @StartTime = GETDATE() 

SET @Method = 5 -- Choose which method to test 


--CASE 0: Just identify duplicates 

IF @Method = 0 
    BEGIN 
     ; WITH CullDupe 
      AS (
       SELECT RecID, ROW_NUMBER() OVER (PARTITION BY StockID, TradeDate, Owner0 ORDER BY OwnerNum) AS rn 
        FROM @OwnRec2 
       ) 
     SELECT * FROM CullDupe WHERE rn > 1 
    END 


--CASE 1: Update on JOIN to CTE 

IF @Method = 1 
    BEGIN 
     ; WITH CullDupe 
      AS (
       SELECT RecID, ROW_NUMBER() OVER (PARTITION BY StockID, TradeDate, Owner0 ORDER BY OwnerNum) AS rn 
        FROM @OwnRec2 
       ) 
     UPDATE OR2 
      SET DupeOwner = 0 
       FROM @OwnRec2 OR2 
        JOIN CullDupe cd 
         ON OR2.RecID = cd.RecID 
        WHERE rn > 1 
    END 


--CASE 2: Update on JOIN to nested SELECT 

IF @Method = 2 
    BEGIN 
     UPDATE OR2 
      SET DupeOwner = 0 
       FROM @OwnRec2 OR2 
        JOIN (SELECT RecID, ROW_NUMBER() OVER 
         (PARTITION BY StockID, TradeDate, Owner0 ORDER BY OwnerNum) AS rn 
         FROM @OwnRec2) cd 
         ON OR2.RecID = cd.RecID 
        WHERE rn > 1 
    END 


--CASE 3: Update on JOIN to temp table 

IF @Method = 3 
    BEGIN 
     ; WITH CullDupe 
      AS (
       SELECT RecID, ROW_NUMBER() OVER (PARTITION BY StockID, TradeDate, Owner0 ORDER BY OwnerNum) AS rn 
        FROM @OwnRec2 
       ) 

     INSERT INTO @CullDupe SELECT RecID FROM CullDupe WHERE rn > 1 

     UPDATE OR2 
      SET DupeOwner = 0 
       FROM @OwnRec2 OR2 
        JOIN @CullDupe cd 
         ON OR2.RecID = cd.RecID 
    END 


--CASE 4: Update using counted loop 

IF @Method = 4 
    BEGIN 
     ; WITH CullDupe 
      AS (
       SELECT RecID, ROW_NUMBER() OVER (PARTITION BY StockID, TradeDate, Owner0 ORDER BY OwnerNum) AS rn 
        FROM @OwnRec2 
       ) 

     INSERT INTO @CullDupe SELECT RecID FROM CullDupe WHERE rn > 1 
     SET @Counter1 = 0 
     WHILE @Counter1 < (SELECT MAX(ID) FROM @CullDupe) 
      BEGIN 
       SET @Counter1 += 1 
       UPDATE OR2 
        SET DupeOwner = 0 
         FROM @OwnRec2 OR2 
          WHERE RecID = (SELECT RecID FROM @CullDupe WHERE ID = @Counter1) 
      END 
    END 


--CASE 5: Update using JOIN to CTE, but updating all rows (CASE to identify) 

IF @Method = 5 
    BEGIN 
     ; WITH CullDupe 
      AS (
       SELECT RecID, ROW_NUMBER() OVER (PARTITION BY StockID, TradeDate, Owner0 ORDER BY OwnerNum) AS rn 
        FROM @OwnRec2 
       ) 

     UPDATE OR2 
      SET DupeOwner = CASE WHEN rn > 1 THEN 0 ELSE 1 END 
       FROM @OwnRec2 OR2 
        JOIN CullDupe cd 
         ON OR2.RecID = cd.RecID 
    END 

SELECT 'Method ' + CAST(@Method AS NVARCHAR(1)) + ': ' + CAST(DATEDIFF(ms,@StartTime,GETDATE()) AS NVARCHAR(10)) + ' milliseconds' 

답변

2

이것은 테이블 변수의 일반적인 문제입니다.

이들을 참조하는 명령문의 실행 계획은 일괄 처리가 실행되기 전에 컴파일되고 삽입 문이 실행되기 전에 컴파일됩니다.

당신이 당신의 문제의 실행 계획 중 하나에서 하나를 선택하고 테이블의 카디널리티는 것을 볼 수 속성 창을 보면 0

enter image description here

그것은 여전히 ​​그럼에도 불구하고 1 개 행이 방출되는 것으로 가정 이것은 빈 계획표에서 대부분의 상황에서 실행 계획의 잎 연산자로부터의 최소 행 견적이기 때문에. 중첩 루프 내부의 하위 트리는 운전 테이블에서 각 행에 대해 한 번 실행됩니다. 이것은 1 행으로 추정되므로 아래의 강조 표시된 하위 트리는 한 번 실행되는 것으로 추정됩니다. 실제로 전체 하위 트리는 8,000 번 실행됩니다 (비싼 테이블 스캔 및 정렬 연산자 포함). 만약 그 서브 트리의 결과를 저장할 수있어 계획이 아직를 사용하지만 이는 단지 (회만 계산되도록 테이블 변수 번호 행의 결과를 구체화하면

enter image description here

중첩 서브 최적 갖는다 루프가 새로운 테이블 변수에 조인). 하나의 행 추정치

일반적인 솔루션은 문 실행시 해당 테이블의 카디널리티가 고려 될 수 있도록 문제 문에 OPTION (RECOMPILE)을 추가하거나 (기수 변경 후 자동 재 컴파일을 트리거 할 수있는) 또는 사용을 추적 플래그 2453을 사용하다 대신 #temp 테이블 (자동 재 컴파일을 트리거하고 열 통계로부터 이점을 얻을 수 있음)

이 중 일부에 대한 자세한 내용은 in my answer here을 참조하십시오.

+0

그것은 절대적으로 놀라운 것입니다. OPTION (RECOMPILE)은 48,136ms에서 13ms로 날 데려갔습니다.이것을 설명해 주셔서 감사합니다. – DaveX

+0

후속 조치. 나는 오늘 이것을 사용할 또 다른 기회가 있었다. 모든 종류의 조인과 모든 종류의 물건으로 18,000 개의 행, 문제 없습니다. 그러나 나는 한 조각 더 추가해야했고, 그 조인은 2 분 넘게 걸렸다. 나를 미치게 만들어. 나는 CTE를 시도한 다음 테이블 변수와 임시 테이블을 시도했다. 마지막으로이 게시물로 돌아가 내가 만든 코드를 살펴 보았습니다. 내 원래 테이블 변수를 만든 다음 OPTION (RECOMPILE)을 사용하여 다른 테이블 변수를 사용하여 업데이트를 수행했습니다. 6 초. 6 초! 저는 55 점으로 내려갔습니다. 그리고 그것은 굉장했습니다, 비교적. 하지만 지금 6! 와우. – DaveX