반응형
주인장은 현재 일본에 거주하고 있습니다.

여러분도 아시다시피 동일본 대지진으로 인해 상황이 좋지 못한 관계로, 한동안 기사를 포스팅하지 못했네요.

그동안 관심을 가지고 기사를 읽어주신 동네 주민들께 너무 죄송합니다.

사태가 어떻게 지속될지 알 수는 없지만, 짬짬이 함께 공유했으면 하는 기사가 있으면 포스팅하도록 하겠습니다.

각설하고 본론으로 들어가도록 하겠습니다.

 

이해를 돕기 위해 먼저, Runners라는 테이블을 작성해 보자.

Runners 테이블은 각 주자(Runner) 별 기록(Time)과 주자의 연령(Age)을 저장하고 있다.

 

CREATE TABLE #Runners
(
  Runner integer NOT NULL
, Time integer NOT NULL
, Age integer NOT NULL
)
INSERT INTO #Runners
SELECT 1 , 10 , 20 UNION ALL
SELECT 2 , 15 , 20 UNION ALL
SELECT 3 , 11 , 20 UNION ALL
SELECT 4 , 12 , 30 UNION ALL
SELECT 5 , 18 , 30 UNION ALL
SELECT 6 , 9 , 40 UNION ALL
SELECT 7 , 16 , 40 UNION ALL
SELECT 8 , 13 , 30
 
자! 이제 이번 토픽의 주제인 각 연령별(그룹별)로 가장 빠른 주자 Top N을 질의해 보자.

여러분이라면 어떻게 T-SQL을 작성할 것인가?

가장 보편적으로 알려진 방법은 Ranking 함수(특히 row_number())와 CTE(Common table Expression) 사용하는 방법이다.

 

 

ROW_NUMBER() 함수와 CTE(Common table Expression) 를 이용한 방법

먼저, 코스를 완주한 주자를 기록 시간 순으로 정렬시켜 보자. 연령별로 그룹핑을 할 필요가 없다면 문제는 아주 간단하다.

 

SELECT * 
  FROM #Runners 
 ORDER BY Time
 
Runner        Time         Age
------ ----------- -----------
     6           9          40
     1          10          20
     3          11          20
     4          12          30
     8          13          30
     2          15          20
     7          16          40
     5          18          30
 
 
그리고, TOP 구문을 이용해서 상위 레코드 2개를 질의해 보자.
   
SELECT TOP(2) * 
  FROM #Runners 
 ORDER BY Time
 
Runner        Time         Age
------ ----------- -----------
     6           9          40
     1          10          20
 
 
하지만, 우리는 연령별로 TOP N 질의를 해야 한다. 이 때, ROW_NUMBER() 함수를 사용하면 유용하다.
 
SELECT *, ROW_NUMBER() OVER(ORDER BY Time) AS RowN 
  FROM #Runners
 
Runner        Time         Age                 RowN
------ ----------- ----------- --------------------
     6           9          40                    1
     1          10          20                    2
     3          11          20                    3
     4          12          30                    4
     8          13          30                    5
     2          15          20                    6
     7          16          40                    7
     5          18          30                    8
 
이 질의는 "해당 테이블(#Runners)의 모든 데이터(*)를 시간별로 레코드를 정렬하여(ORDER BY Time) 정렬 순번(ROW_NUMBER())을 구해서 보여주라"는 것을 의미한다.
만일 동일 순위에 대한 처리가 필요하다면 ROW_NUMBER()함수보다는 DENSE_DANK() 함수를 사용하는 것이 좋다.
 

이제 OVER 구에 “PARTITION_BY” 를 확장해 보자.

“PARTITION_BY” 구는 “PARTITION_BY” 구에 지정한 컬럼 별로 그룹 함수를 적용하라는 것을 의미한다.

따라서, 아래의 질의는 RowN 컬럼의 행 번호를 Age 별로 카운트한다.

 

SELECT *, ROW_NUMBER() OVER(PARTITION BY Age ORDER BY Time) AS RowN 
  FROM #Runners 
 ORDER BY Age,RowN
 
Runner        Time         Age                 RowN
------ ----------- ----------- --------------------
     1          10          20                    1
     3          11          20                    2
     2          15          20                    3
     4          12          30                    1
     8          13          30                    2
     5          18          30                    3
     6           9          40                    1
     7          16          40                    2

 

이 질의로 연령별로 기록이 좋은 주자 순으로 정렬할 수 있게 되었다.

이제,TOP N을 필터링하는 일만 남았다.

여러분은 TOP N을 필터링하기 위해 아래의 SQL을 생각할 수 있을 것이다.

 

SELECT *, ROW_NUMBER() OVER(PARTITION BY Age ORDER BY Time) AS RowN 
  FROM #Runners
 WHERE ROW_NUMBER() OVER(PARTITION BY Age ORDER BY Time) <= 2
 ORDER BY Age,RowN 

 

ㅜㅜ… 실행이 안된다...

당연히 구문 에러가 발생할 것이다.

어떻게 해야지?

이 문제를 해결하기 위해 일반적으로 CTE(Common Table Expresssion) 를 사용한다.

 

WITH cteRunners
AS ( SELECT *, ROW_NUMBER() OVER(PARTITION BY Age ORDER BY Time) AS RowN 
        FROM #Runners )
SELECT *
  FROM cteRunners
 WHERE RowN <=2
 ORDER BY Age, Rown 
 
Runner        Time         Age                 RowN
------ ----------- ----------- --------------------
     1          10          20                    1
     3          11          20                    2
     4          12          30                    1
     8          13          30                    2
     6           9          40                    1
     7          16          40                    2

 

이제 우리가 원하는 소기의 목적을 달성하게 되었다.

하지만 이대로 만족하는가?!

그룹 함수는 그 특성상 그룹 함수의 대상이 되는 집합이 커지면 커질수록 CPU, Memory 등의 리소스 사용량이 늘어나고 응답 속도가 늦어진다.

그러면 어떻게 하면 좀 더 최적화된 질의를 할 수 있을까?

 

 

CROSS APPLY 연산자를 이용한 집합 축소

문제는 이러하다.

우리가 최종적으로 가지고 와야 할 집합은 그룹별 상위 N개 이지만, 상위 N개를 도출하기 위해서는 전체 집합을 읽어야 한다는 것이다.

그렇다! 최적화의 키는 읽어야 할 집합을 축소 하는 것이다. 모든 질의가 그러하듯이...

 

집합을 축소해 보자!!!

우리는 집합을 축소하기 위해 CROSS APPLY 연산자를 사용할 것이다.

 

우선 아래의 스크립트를 실행해서 예제 테이블 #RunnersBig을 만들자.

이 테이블에 100만 건의 데이터을 입력하였고, age + time 순서로 된 인덱스를 작성하였다.

 

IF OBJECT_ID('tempdb..#RunnersBig') IS NOT NULL
    DROP TABLE #RunnersBig
GO 
 
CREATE TABLE #RunnersBig
(
    RunnerId INTEGER IDENTITY
  , Time INTEGER NOT NULL
  , Age INTEGER NOT NULL
)
GO
 
INSERT INTO #runnersbig ( Time , Age ) 
SELECT TOP 1000000 
       ABS (CHECKSUM(NEWID()))% 1000 
     , ABS (CHECKSUM(NEWID()))% 99 
  FROM sys.columns a 
 CROSS JOIN sys . columns b 
 CROSS JOIN sys . columns c
GO
 
CREATE INDEX idxrunnersbig
    ON #runnersbig ( age , time ) INCLUDE ( runnerid ) 

 

자! 이제 획기적인 성능 향상을 경험할 차례이다. 아래의 두 질의를 비교해서 실행해 보자.

위의 SQL은 ROW_NUMBER() 함수와 CTE를 이용하여 그룹별 상위 2개의 레코드를 필터링하고 있고, 아래의 SQL은 CROSS APPLY 연산자를 이용하여 읽어야 할 집합을 축소하였다.

 

1. ROW_NUMBER() 함수와 CTE를 이용하여 그룹별 상위 2개의 레코드를 필터링한 SQL

WITH cteRunners 
AS ( SELECT * , ROW_NUMBER() OVER(PARTITION BY Age ORDER BY Time ) AS RowN 
       FROM #RunnersBig ) 
SELECT * 
  FROM cteRunners 
 WHERE RowN <= 2 
 ORDER BY Age , Rown
GO

 

2. CROSS APPLY를 이용한 SQL

WITH cteN
AS ( SELECT number 
       FROM master .. spt_values 
      WHERE type = 'p' 
        AND number between 0 and 100 ) 
SELECT * 
  FROM cteN 
 CROSS APPLY ( SELECT TOP(2) * 
                 FROM #RunnersBig 
                WHERE #RunnersBig . Age = cteN . number 
                ORDER BY Time ) AS runners 
 ORDER by cteN.number , runners.Time

 

주인장의 머신에서 테스트한 결과  ROW_NUMBER() 함수와 CTE를 이용하여 그룹별 상위 2개의 레코드를 필터링한 SQL은 평균 463ms, CROSS APPLY를 이용한 SQL은 1ms 안팎을 기록하였다.

어떻게 이렇게 놀라운 성능 향상이 있을 수 있었는지는 실행 계획을 비교해 확인해 보기 바란다.

물론, 이 SQL을 실전에서 적절하게 사용하기 위해서는 CTE내의 master..spt_value에 해당하는 부모 자식 관계를 정확하게 데이터 모델링해야 하며, 적절한 인덱스 설계가 필요하다.

반응형

+ Recent posts