분석함수의 비효율을 찾는 방법
분석함수의 성능개선 원리 

분석함수의 실행계획 3부 - 심화과정의 모범답안

이번 글이 분석함수의 실행계획 시리즈의 마지막이다. 1
2에서 분석함수의 실행계획에 대해서 알아보았다
이 글(3부)은 새로 작성되었으며
, 이전 글( 문제 )은 삭제하지 않고 그대로 두었음을 밝혀둔다. 나중에라도 문제를 풀어볼 사람은 이 글(답안)을 보지 말고, 이전 글을 보기 바란다. 각각의 답안은 오렌지색으로 표시해 두었으므로 쉽게 채점할 수 있을 것이다.


분석함수의 다섯 가지 원리
많은 수의 개발자, DBA, 튜너들은 분석함수를 만나면 식은 땀을 흘린다. 왜냐하면, 분석함수의 비효율을 어떻게 찾을 수 있는지, 또 그 비효율은 어떻게 제거 할 수 있는지 알 수 없기 때문이다. 기껏해야 분석함수의 over절을 만족하는 적절한 인덱스를 생성하는 정도이다. 어쩌면 이런 결과들은 당연하다고 볼 수 있다. 매뉴얼이나 튜닝 책에 분석함수의 비효율을 발견하는 방법과 개선방법에 대한 언급이 별로 없기 때문이다. 이런 어려움을 겪고 있는 여러 사람들의 요구에 의해서 이 글이 만들어졌다. 분석함수의 비효율을 찾고, 성능을 향상시킬 수 있는 다섯 가지 방법을 공개하니, 많은 사람들이 적용하여 더 이상 식은 땀은 흘리지 않기 바란다. 

SQL 두 개에 5가지 비효율이 있다. 이것들을 제거하라
이 글에서 설명되는 두 개의 SQL은 길이가 매우 짧다그 중 하나는 비효율을 찾아내기 쉽고나머지 하나는 어렵다. 두 개의 SQL에는 총 5가지의 튜닝 포인트가 있다. 각각의 포인트는 20점이며 5가지를 모두 맞추는 경우는 100점이 된다. 채점 시 중요한 점은, 독자들이 직접 튜닝한 SQL이 모범답안과 같은지 비교하는 것이 아니라는 것이다. 그것 보다는 비효율을 몇 개 잡아내었는지, 또한 각각의 비효율을 개선시킬 수 있는 방법은 몇 개나 생각했는지를 채점하는 것이다. 이렇게 채점하면, 내가 아는 것은 무엇이고, 모르는 것은 무엇인지 명확히 드러난다, 무엇을 더 공부해야 하는지 알 수 있다.


다른 사람들은 몇 점 정도 받았나?
몇몇 지인들에게 퀴즈를 내본 결과 100점은 아무도 없었다. 80점이 평균적인 튜너의 수준이라는 이야기 이다. 개발자의 경우는 60점이면 상위 수준이다. 점수에 대해 오해는 하지 말기 바란다. 이 퀴즈로 받은 점수는 분석함수의 성능과 관련된 것일 뿐, 전반적인 SQL 튜닝능력에 대한 점수가 아니다.

이제 실습을 위한 테이블과 인덱스를 생성하자. 실습환경은 Oracle 10g R2, Oracle 11g R1, Oracle 11g R2로 한정한다.


CREATE TABLE SALES_T NOLOGGING AS SELECT * FROM SALES;

 

ALTER TABLE SALES_T ADD CONSTRAINT PK_SALES_T

PRIMARY KEY (PROD_ID, CUST_ID, CHANNEL_ID, TIME_ID) USING INDEX;

 

개발자가 작성한 문제의 SQL 1

 

SELECT *

  FROM ( SELECT /*+  INDEX(T PK_SALES_T) */ PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID, AMOUNT_SOLD,

                MAX(TIME_ID) OVER( PARTITION BY CUST_ID ,CHANNEL_ID ) AS MAX_TIME

           FROM SALES_T T

          WHERE PROD_ID = 30 )

 WHERE TIME_ID = MAX_TIME;

 

업무설명 : 상품번호 30인 데이터에 대하여 고객별, 채널 별로 가장 최근의 판매량을 나타내시오.

             단 고객별, 채널별로 가장 최근의 데이터는 2건 이상일 수 있음.

 

-----------------------------------------------------------------------------------------------

| Id  | Operation                     | Name       | A-Rows |   A-Time   | Buffers | Used-Mem |

-----------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT              |            |  12649 |00:00:00.31 |   28337 |          |

|*  1 |  VIEW                         |            |  12649 |00:00:00.31 |   28337 |          |

|   2 |   WINDOW BUFFER               |            |  29282 |00:00:00.27 |   28337 | 1495K (0)|

|   3 |    TABLE ACCESS BY INDEX ROWID| SALES_T    |  29282 |00:00:00.17 |   28337 |          |

|*  4 |     INDEX RANGE SCAN          | PK_SALES_T |  29282 |00:00:00.03 |     118 |          |

-----------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   1 - filter("TIME_ID"="MAX_TIME")

   4 - access("PROD_ID"=30)

 

WHERE 절에 TIME_ID = MAX_TIME 이 존재하는 이유는 고객별, 채널별로 MAX(TIME_ID)에 해당하는 데이터가 두건 이상일 수 있기 때문이다. 이런 경우는 자주 발생한다. 이해를 돕기 위해 비슷한 예를 들어보자. 부서별로 최대급여를 받는 사람들을 출력하고자 할 때, 1번 부서의 최대급여는 1억 원이라고 하자. 그런데 그 부서에서 1억 원(최대급여)을 받는 사람은 강호동, 유재석 둘이라는 이야기 이다.

 

위의 SQL은 답이 정확하다. 하지만 성능은 비효율이 있어서 별로 신통치 못하다. 이제 문제를 보자. SQL에 여전히 남아있는 비효율을 개선하는 것이 문제이다.

 

문제1) 위의 SQL 에서 성능상 문제점을 발견하고 개선하시오. 문제점은 두 가지(분석함수의 관점, 일반적인 SQL튜닝의 관점) 이다. 단 인덱스를 추가로 생성하거나 변경해서는 안 된다.

 

문제 1의 답안

비효율 1 Sample 답안이다. 점수에 반영되지 않는다.

 

문제1의 비효율 1 : SALES_T 테이블에 Random Access가 불필요하게 많이 발생한다. 12649 번만 테이블로 액세스 하면 되지만 실제로는 29282 번 액세스함으로써 비효율이 발생되었다.

 

문제1의 비효율 1의 해결방법 : PK인덱스에 AMOUNT_SOLD가 포함되어 있지 않으므로 Select 절에서 AMOUNT_SOLD를 빼면 인덱스만 액세스 하게 됨. 이때 Rowid를 추가로 Select 해야 한다. 이후에 이미 인덱스에서 Filter Rowid로 테이블을 액세스하면 Random Access는 정확히 12649번 만 시도한다. 아래에 SQL이 있으므로 참조하시오.

 

문제1의 비효율1이 해결된 SQL과 실행계획 제시:

 

SELECT /*+ LEADING(S) USE_NL(S1) */

        S.PROD_ID, S.CUST_ID, S.TIME_ID, S.CHANNEL_ID, S1.AMOUNT_SOLD

  FROM ( SELECT /*+  INDEX(T PK_SALES_T) */ PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID,

                MAX(TIME_ID) OVER( PARTITION BY CUST_ID ,CHANNEL_ID ) AS MAX_TIME,

                ROWID AS RID

           FROM SALES_T T

          WHERE PROD_ID = 30 ) S,

       SALES_T S1

 WHERE S.TIME_ID = S.MAX_TIME

   AND S1.ROWID = S.RID;

  

------------------------------------------------------------------------------------------------------

| Id  | Operation                   | Name       | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT            |            |      1 |  12649 |00:00:02.72 |   11237 |          |

|   1 |  NESTED LOOPS               |            |      1 |  12649 |00:00:02.72 |   11237 |          |

|*  2 |   VIEW                      |            |      1 |  12649 |00:00:00.18 |     118 |          |

|   3 |    WINDOW BUFFER            |            |      1 |  29282 |00:00:00.13 |     118 | 1432K (0)|

|*  4 |     INDEX RANGE SCAN        | PK_SALES_T |      1 |  29282 |00:00:00.03 |     118 |          |

|   5 |   TABLE ACCESS BY USER ROWID| SALES_T    |  12649 |  12649 |00:00:02.48 |   11119 |          |

------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   2 - filter("S"."TIME_ID"="S"."MAX_TIME")

   4 - access("PROD_ID"=30)

 

이렇게 해서 Sample 답안을 작성해 보았다. 이 방법은 분석함수의 튜닝방법이 아니라 예외적으로 SQL 튜닝방법론에서 언급되었던 방법을 사용하였다. 물론 여기서 페이징처리를 한다면 추가적인 성능향상이 가능하지만 Sample이므로 여기서 멈추기로 한다위의 답안은 Sample 이므로 점수에서 빠진다. 또한 이 부분은 분석함수의 튜닝 방법이 아니다. 하지만 앞으로 풀게 될 문제들은 모두 분석함수와 관련된 문제들이다.

 

 

문제1의 비효율 2

비효율 1을 제거하니 Random Access는 최소화 되었다. 하지만 실행계획을 보면 쓸데없이 PGA 1432K만큼 사용하고 있다. 즉 불필요한 WINDOW BUFFER Operation 때문에 sort가 발생하였다.

 

문제1의 비효율 2 해결방법 

고객별 채널별로 최종일자의 데이터를 구하는 것이므로, Rank를 사용하면 적절한 인덱스가 존재하므로 sort를 하지 않는다. Rank Row_number와는 다르게 1등이 두 명 이상일 수 있으므로 업무조건을 만족한다.

 

문제1의 비효율 2이 해결된 SQL과 실행계획 제시.

 

SELECT /*+ LEADING(A) USE_NL(B) */ A.*, B.AMOUNT_SOLD

  FROM ( SELECT /*+  INDEX_DESC(T PK_SALES_T) */ PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID,

                RANK() OVER(PARTITION BY CUST_ID, CHANNEL_ID ORDER BY TIME_ID DESC) RNK,

                ROWID AS RID

           FROM SALES_T T

          WHERE PROD_ID = 30 ) A,

       SALES_T B

 WHERE A.RNK = 1

   AND A.RID = B.ROWID;

 

---------------------------------------------------------------------------------------------------------

| Id  | Operation                      | Name       | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

---------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT               |            |      1 |  12649 |00:00:00.32 |   11240 |          |

|   1 |  NESTED LOOPS                  |            |      1 |  12649 |00:00:00.32 |   11240 |          |

|*  2 |   VIEW                         |            |      1 |  12649 |00:00:00.20 |     120 |          |

|*  3 |    WINDOW NOSORT               |            |      1 |  29282 |00:00:00.16 |     120 |          |

|*  4 |     INDEX RANGE SCAN DESCENDING| PK_SALES_T |      1 |  29282 |00:00:00.06 |     120 |          |

|   5 |   TABLE ACCESS BY USER ROWID   | SALES_T    |  12649 |  12649 |00:00:00.05 |   11120 |          |

---------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   2 - filter("A"."RNK"=1)

   3 - filter(RANK() OVER ( PARTITION BY "CUST_ID","CHANNEL_ID" ORDER BY INTERNAL_FUNCTION("TIME_ID") DESC )<=1)

   4 - access("PROD_ID"=30)

 

이제 문제2를 풀어보자.

 

개발자가 작성한 문제의 SQL 2

 

SELECT *

  FROM ( SELECT /*+ INDEX(T PK_SALES_T) */ PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID, AMOUNT_SOLD,

                ROW_NUMBER()     OVER ( PARTITION BY PROD_ID ORDER BY CUST_ID,CHANNEL_ID,TIME_ID NULLS FIRST) AS RN,

                SUM(AMOUNT_SOLD) OVER ( ORDER BY CUST_ID,CHANNEL_ID,TIME_ID,PROMO_ID   ) AS SUM_AMT

           FROM SALES_T T

          WHERE PROD_ID = 30 )

 WHERE RN <= 10 ;

 

---------------------------------------------------------------------------------------------------------

| Id  | Operation                      | Name       | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

---------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT               |            |      1 |     10 |00:00:00.43 |   28337 |          |

|*  1 |  VIEW                          |            |      1 |     10 |00:00:00.43 |   28337 |          |

|*  2 |   WINDOW SORT PUSHED RANK      |            |      1 |  29282 |00:00:00.40 |   28337 | 1999K (0)|

|   3 |    WINDOW SORT                 |            |      1 |  29282 |00:00:00.30 |   28337 | 1684K (0)|

|   4 |     TABLE ACCESS BY INDEX ROWID| SALES_T    |      1 |  29282 |00:00:00.18 |   28337 |          |

|*  5 |      INDEX RANGE SCAN          | PK_SALES_T |      1 |  29282 |00:00:00.04 |     118 |          |

---------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   1 - filter("RN"<=10)

   2 - filter(ROW_NUMBER() OVER ( PARTITION BY "PROD_ID" ORDER BY "CUST_ID","CHANNEL_ID",INTERNAL_FUNCTION("TIME_ID")

              NULLS FIRST)<=10)

   5 - access("PROD_ID"=30)

 

문제2) 위의 SQL 에서 분석함수의 성능상 문제점을 발견하고 개선하시오. 분석함수와 관련된 성능상 문제점은 4가지 이다. 비효율들을 발견하고 이를 모두 개선해야 한다. 단 인덱스를 추가로 생성하거나 변경하면 안 된다.

튜닝의 결과부터 이야기 하자면, Sort량은 3683 K 에서 0 K 로 줄어들었고 Block I/O량은 28337 에서 15 로 줄어들었다. 성능이 몇 배나 개선되었는지 계산하기도 힘들 정도이다. 결과건수가 많아 질수록 이 차이는 점점 더 벌어질 것이다. 성능이 어떻게 개선된 것일까? 지금부터 비효율을 하나씩 벗겨내고 개선방법을 도출해보자.
 
 

문제 2의 답안

문제2의 비효율 1

NULLS FIRST를 사용하면 인덱스를 Ascending으로 사용하면 null 값은 가장 마지막에 출력된다. null 값이 마치 가장 큰 값인 것처럼 저장 되는 것이다. 하지만 NULLS FIRST를 명시하는 순간 sort가 뒤바뀌어 가장 먼저 출력해야 한다. 즉 인덱스를 사용했지만 NULLS FIRST 때문에 추가적인 sort가 필요하다는 이야기 이다. 이러한 문제는 인덱스를 Descending 하게 사용할 때, NULLS LAST를 명시해도 똑같이 발생한다.

 

문제2의 비효율 1 해결방법

문제의 SQL을 보면 PARTITION BY ORDER BY의 컬럼들은 PK 컬럼들이므로 null 일수 없다. 따라서 NULLS FIRST삭제해도 무방하다. 다시 작성한 SQL은 다음과 같다.

 

문제2의 비효율 1이 해결된 SQL과 실행계획 제시

 

SELECT *

  FROM ( SELECT /*+ INDEX(T PK_SALES_T) */ PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID, AMOUNT_SOLD,

                ROW_NUMBER()     OVER (PARTITION BY PROD_ID ORDER BY CUST_ID,CHANNEL_ID,TIME_ID ) AS RN,

                SUM(AMOUNT_SOLD) OVER ( ORDER BY CUST_ID,CHANNEL_ID,TIME_ID,PROMO_ID  ) AS SUM_AMT

           FROM SALES_T T

          WHERE PROD_ID = 30 )

 WHERE RN <= 10 ;

 

---------------------------------------------------------------------------------------------------------

| Id  | Operation                      | Name       | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

---------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT               |            |      1 |     10 |00:00:00.45 |   28337 |          |

|*  1 |  VIEW                          |            |      1 |     10 |00:00:00.45 |   28337 |          |

|*  2 |   WINDOW NOSORT                |            |      1 |  29282 |00:00:00.42 |   28337 |          |

|   3 |    WINDOW SORT                 |            |      1 |  29282 |00:00:00.32 |   28337 | 1684K (0)|

|   4 |     TABLE ACCESS BY INDEX ROWID| SALES_T    |      1 |  29282 |00:00:00.17 |   28337 |          |

|*  5 |      INDEX RANGE SCAN          | PK_SALES_T |      1 |  29282 |00:00:00.03 |     118 |          |

---------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   1 - filter("RN"<=10)

   2 - filter(ROW_NUMBER() OVER ( PARTITION BY "PROD_ID" ORDER BY "CUST_ID","CHANNEL_ID","TIME_ID")<=10)

   5 - access("PROD_ID"=30)

 

ROW_NUMBER에 해당하는 Operation WINDOW SORT PUSHED RANK에서 WINDOW NOSORT로 바뀌었다. 그 영향으로 ROW_NUMBER Sort가 전혀 발생하지 않는다.

 

 

문제2의 비효율 2

비효율 1의 튜닝의 결과를 놓고 보면, ROW_NUMBER SUM 분석함수의 Operation이 실행계획에 각각 존재한다. 즉 분석함수가 따로 따로 두 번 실행되는 것이다. 분석함수의 종류는 두 개(row_number sum)라 하더라도, 분석함수와 관련된 Operation이 한번만 나와야 한다는 말이다. 또한 분석함수 SUM order by절에 PROMO_ID가 존재하여 불필요한 sort가 발생하였다.

 

문제2의 비효율 2 해결방법

이 비효율을 없애면 마치 분석함수가 하나뿐인 것처럼 만들 수 있다. Partition 절과 Order 절을 일치시키면 된다. 아래의 SQL Partition 절과 Order 절을 일치시키기 위해 SUM 분석함수에 PARTITION BY 절을 추가해야 한다. 물론 ROW_NUMBER함수에서 Partition By절을 삭제할 수 있지만, 그와 관련된 이야기는 이후 단계에서 논의 되므로, 여기서는 그냥 Partition By절을 추가하기로 한다. 그리고 WINDOW SORT를 유발하는 PROMO_ID도 삭제해야 한다.

 

문제2의 비효율 2가 해결된 SQL과 실행계획 제시

 

SELECT *

  FROM ( SELECT /*+ INDEX(T PK_SALES_T) */ PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID, AMOUNT_SOLD,

                ROW_NUMBER()     OVER (PARTITION BY PROD_ID ORDER BY CUST_ID,CHANNEL_ID,TIME_ID ) AS RN,

                SUM(AMOUNT_SOLD) OVER (PARTITION BY PROD_ID ORDER BY CUST_ID,CHANNEL_ID,TIME_ID ) AS SUM_AMT

           FROM SALES_T T

          WHERE PROD_ID = 30 )

 WHERE RN <= 10 ;  

 

--------------------------------------------------------------------------------------------------------

| Id  | Operation                     | Name       | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

--------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT              |            |      1 |     10 |00:00:00.26 |   28337 |          |

|*  1 |  VIEW                         |            |      1 |     10 |00:00:00.26 |   28337 |          |

|   2 |   WINDOW BUFFER               |            |      1 |  29282 |00:00:00.23 |   28337 | 1495K (0)|

|   3 |    TABLE ACCESS BY INDEX ROWID| SALES_T    |      1 |  29282 |00:00:00.11 |   28337 |          |

|*  4 |     INDEX RANGE SCAN          | PK_SALES_T |      1 |  29282 |00:00:00.03 |     118 |          |

--------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   1 - filter("RN"<=10)

   4 - access("PROD_ID"=30)

 

만약 해결방법 두 가지(Partition By 추가, PROMO_ID 삭제)중 한가지만 적용했다면 10점으로 처리하면 되므로 채점은 가능하다. 아래의 설명부분과 SQL은 답안이 아니며, 원리를 설명하는 부분이다.

 

Operation 통합의 원리
분석함수 SUM
Partition By절을 추가하고, order by절에 PROMO_ID를 삭제하니, 예상대로 분석함수 두 개의 Plan이 하나로 합쳐졌다. 여기서 알 수 있는 점은 두 가지 이다. 첫 번째, Partition By Order By를 통일 시키면 Operation이 통합되어 분석함수 하나만 실행시키는 결과를 얻는다. 두 번째, WINDOW NOSORT(ROW_NUMBER) + WINDOW BUFFER(SUM) 가 합쳐져서 WINDOW BUFFER가 되었다는 점이다. 좋지 않은 Operation으로 통합된다는 것을 알 수 있다. 기준은 다음과 같다.

 

 WINDOW NOSORT + WINDOW SORT    => WINDOW SORT

 WINDOW NOSORT + WINDOW BUFFER  => WINDOW BUFFER

 WINDOW BUFFER + WINDOW SORT    => WINDOW SORT


PROMO_ID
를 삭제해도 되는 이유는 ORDER BY에서 모든 PK 컬럼을 사용하였기 때문이다. PK 컬럼들로 ORDER BY절에서 모두 SORT 되었으므로, 나머지 컬럼으로 아무리 SORT 해보아야 결과는 동일하다는 것이다. Order By절에서 PROMO_ID를 삭제하자 WINDOW SORT WINDOW BUFFER 로 바뀌었다. 이에 따라 PGA사용량이 1747K에서 1495K 약간 줄어들었다
 


만약 Partition By만 추가하고 PROMO_ID를 삭제하지 않으면 아래와 같이 sort의 비효율이 발생한다.

 

SELECT *

  FROM ( SELECT /*+ INDEX(T PK_SALES_T) */ PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID, AMOUNT_SOLD,

                ROW_NUMBER()     OVER (PARTITION BY PROD_ID ORDER BY CUST_ID,CHANNEL_ID,TIME_ID ) AS RN,

                SUM(AMOUNT_SOLD) OVER (PARTITION BY PROD_ID ORDER BY CUST_ID,CHANNEL_ID,TIME_ID,PROMO_ID  ) AS SUM_AMT

           FROM SALES_T T

          WHERE PROD_ID = 30 )

 WHERE RN <= 10 ; 

 

--------------------------------------------------------------------------------------------------------

| Id  | Operation                     | Name       | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

--------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT              |            |      1 |     10 |00:00:00.33 |   28337 |          |

|*  1 |  VIEW                         |            |      1 |     10 |00:00:00.33 |   28337 |          |

|   2 |   WINDOW SORT                 |            |      1 |  29282 |00:00:00.30 |   28337 | 1747K (0)|

|   3 |    TABLE ACCESS BY INDEX ROWID| SALES_T    |      1 |  29282 |00:00:00.17 |   28337 |          |

|*  4 |     INDEX RANGE SCAN          | PK_SALES_T |      1 |  29282 |00:00:00.03 |     118 |          |

--------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

 

   1 - filter("RN"<=10)

   4 - access("PROD_ID"=30)

  

Order By절이 다르지만, 통합이 가능한 이유
PROMO_ID
를 추가하자, WINDOW SORT가 발생하여 PGA 사용량이 늘어났다. 분석함수 SUM ORDER BY절을 보면 PROMO_ID가 있는데 ROW_NUMBER 분석함수에는 PROMO_ID가 없다. ORDER BY는 완벽히 같지 않아도 Operation이 통합 될 수 있다는 점을 알 수 있다. ORDER BY의 마지막 항목이 달라도 Operation이 통합 될 수 있는 원리는 간단하다. Sort 작업을 A + B + C로 이미 했다면 Sort A + B 로 다시 해야 할 필요가 없기 때문이다. A + B + C Sort 하는 분석함수 기준으로 A + B Sort 하는 분석함수가 통합된다는 의미이다.  

 

오해하지 말 것

서로 다른 종류의 분석함수를 실행했을 때, Partition By의 컬럼과 Order By의 컬럼을 각각 똑같이 맞추어야 하는 것은 아니다. Partition ByOrder By의 컬럼을 각각 맞추는 것이 아니라, OVER절 전체의 컬럼을 일치시키면 된다. 아래의 SQL이 좋은 예제이다.

 

SELECT *

  FROM ( SELECT /*+ INDEX(T PK_SALES_T) */ PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID, AMOUNT_SOLD,

                ROW_NUMBER()     OVER (PARTITION BY PROD_ID  ORDER BY CUST_ID, CHANNEL_ID,TIME_ID ) AS RN,

                SUM(AMOUNT_SOLD) OVER (PARTITION BY PROD_ID, CUST_ID  ORDER BY CHANNEL_ID,TIME_ID ) AS SUM_AMT

           FROM SALES_T T

          WHERE PROD_ID = 30 )

 WHERE RN <= 10 ;

 

--------------------------------------------------------------------------------------------------------

| Id  | Operation                     | Name       | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

--------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT              |            |      1 |     10 |00:00:00.32 |   28337 |          |

|*  1 |  VIEW                         |            |      1 |     10 |00:00:00.32 |   28337 |          |

|   2 |   WINDOW BUFFER               |            |      1 |  29282 |00:00:00.28 |   28337 | 1495K (0)|

|   3 |    TABLE ACCESS BY INDEX ROWID| SALES_T    |      1 |  29282 |00:00:00.14 |   28337 |          |

|*  4 |     INDEX RANGE SCAN          | PK_SALES_T |      1 |  29282 |00:00:00.04 |     118 |          |

--------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   1 - filter("RN"<=10)

   4 - access("PROD_ID"=30)

 

위의 SQL을 보면 분석함수 두 개의 Partition By의 컬럼과 Order By 컬럼이 모두 다르다. 하지만 실행계획상의 operation은 하나로 통합되었다. OVER절을 바라볼 때 Partition ByOrder By를 삭제하고 컬럼들만 남긴다면, operation이 통합이 될지, 아닐지 판단할 수 있다. 예컨대, over절의 컬럼은 두 개의 분석함수 모두 PROD_ID + CUST_ID + CHANNEL_ID + TIME_ID 로 똑같으므로 operation이 통합된다.

 

중요한 것은 컬럼의 순서이다. Partition By Order By를 삭제하고 순수하게 over절의 컬럼들만 남겼을 때, 컬럼의 순서가 같아야 operation이 통합된다. 컬럼의 순서가 다르면 통합되지 않는다. 위의 분석함수 둘은 over절의 컬럼순서가 PROD_ID + CUST_ID + CHANNEL_ID + TIME_ID로 같음을 알 수 있다. OVER절 전체의 컬럼순서가 같다면, 컬럼의 개수와는 상관없이 통합이 가능하다. 예를 들어, 분석함수를 두 개 사용하고 컬럼순서가 하나는 A+B+C 이고 다른 하나는 A+B 라면, 컬럼순서가 동일하므로 통합이 가능하다하지만 A+B+C A+C의 조합은 컬럼순서가 다르므로 통합이 불가능하다.

 

문제2의 비효율 3

비효율 2의 튜닝결과를 보면 Ranking family를 사용하고, 인라인뷰 외부에서 분석함수를 Filter로 처리했음에도 불구하고 STOPKEY가 작동하지 않는다. 이에 따라 부분범위처리가 되지 못하고 불필요한 데이터를 모두 Scan해야 한다.

 

문제2의 비효율 3의 해결방법

STOPKEY 작동되도록 하려면 ROW_NUMBER OVER절에서 불필요한 PARTITION BY PROD_ID를 제거하면 된다. PROD_ID는 이미 WHERE 절의 Equal 조건에 의하여 상수화 되었기 때문에 PARTITION BY는 필요 없다.

 

문제2의 비효율 3이 해결된 SQL과 실행계획 제시

 

SELECT *

  FROM ( SELECT /*+ INDEX(T PK_SALES_T) */ PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID, AMOUNT_SOLD,

                ROW_NUMBER()     OVER (                     ORDER BY CUST_ID,CHANNEL_ID,TIME_ID ) AS RN,

                SUM(AMOUNT_SOLD) OVER (PARTITION BY PROD_ID ORDER BY CUST_ID,CHANNEL_ID,TIME_ID ) AS SUM_AMT

           FROM SALES_T T

          WHERE PROD_ID = 30 )

 WHERE RN <= 10 ; 

 

---------------------------------------------------------------------------------------------------------

| Id  | Operation                      | Name       | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

---------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT               |            |      1 |     10 |00:00:00.16 |   28337 |          |

|*  1 |  VIEW                          |            |      1 |     10 |00:00:00.16 |   28337 |          |

|*  2 |   WINDOW NOSORT STOPKEY        |            |      1 |     10 |00:00:00.16 |   28337 |          |

|   3 |    WINDOW BUFFER               |            |      1 |     11 |00:00:00.16 |   28337 | 1495K (0)|

|   4 |     TABLE ACCESS BY INDEX ROWID| SALES_T    |      1 |  29282 |00:00:00.11 |   28337 |          |

|*  5 |      INDEX RANGE SCAN          | PK_SALES_T |      1 |  29282 |00:00:00.03 |     118 |          |

---------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   1 - filter("RN"<=10)

   2 - filter(ROW_NUMBER() OVER ( ORDER BY "CUST_ID","CHANNEL_ID","TIME_ID")<=10)

   5 - access("PROD_ID"=30)

 

아래의 설명부분과 SQL은 답안이 아니며 원리를 설명하는 부분이다.

 

Ranking family에서 불필요한 Partition By를 삭제하자 WINDOW NOSORT STOPKEY가 발생되었다. 하지만 실행계획을 자세히 보면 비효율이 존재한다. WINDOW BUFFER 때문에 29282건을 모두 읽은 후에 STOPKEY가 발생되었다. 처음부터 11건만 읽는 방법은 없는 것일까? 그렇다고 위의 SQL에서 분석함수 SUM PARTITION BY도 삭제한다면 비효율 2의 실행계획으로 돌아가 버린다. 비효율 2에서 이미 설명했던 원칙(좋지 않은 Operation으로 통합되는 원칙) 때문에 WINDOW NOSORT STOPKEY WINDOW BUFFER에 묻혀버리는 것이다.

 

만약 WINDOW BUFFER(분석함수 SUM)가 없다면 WINDOW NOSORT STOPKEY가 완벽히 동작한다. 아래의 SQL로 증명할 수 있다.

 

SELECT *

  FROM ( SELECT /*+ INDEX(T PK_SALES_T) */ PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID, AMOUNT_SOLD,

                ROW_NUMBER() OVER ( ORDER BY CUST_ID,CHANNEL_ID,TIME_ID ) AS RN

           FROM SALES_T T

          WHERE PROD_ID = 30 )

 WHERE RN <= 10 ;  

 

--------------------------------------------------------------------------------------------------------

| Id  | Operation                     | Name       | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

--------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT              |            |      1 |     10 |00:00:00.01 |      15 |          |

|*  1 |  VIEW                         |            |      1 |     10 |00:00:00.01 |      15 |          |

|*  2 |   WINDOW NOSORT STOPKEY       |            |      1 |     10 |00:00:00.01 |      15 |          |

|   3 |    TABLE ACCESS BY INDEX ROWID| SALES_T    |      1 |     11 |00:00:00.01 |      15 |          |

|*  4 |     INDEX RANGE SCAN          | PK_SALES_T |      1 |     11 |00:00:00.01 |       4 |          |

--------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   1 - filter("RN"<=10)

   2 - filter(ROW_NUMBER() OVER ( ORDER BY "CUST_ID","CHANNEL_ID","TIME_ID")<=10)

   4 - access("PROD_ID"=30)

 

분석함수 SUM만 제거한다면 STOPKEY가 비효율 없이 작동하여 정확히 11건만 읽었다. 위의 실행통계와 비효율 3이 튜닝된 SQL의 실행통계를 비교해보라. Buffers Used-Mem 항목을 본다면 비교할 수 없을 정도이다.  또한 WINDOW STOPKEY의 활용범위는 ROWNUM 보다 넓다. 물론 ROW_NUMBER는 ROWNUM 처럼 사용 할 수도 있다. 하지만, RANK나 DENSE_RANK의 경우로 본다면 추가적인 활용법이 있다. 예를 들어, 학교에서 수학성적으로 전교석차 2등 까지 출력한다고 치자. 그러면 1등과 2등이 각각 두 명 이상일 수 있다. 이런 경우는  ROWNUM으로는 구현할 수 없다. RANK나 DENSE_RANK가 WINDOW NOSORT STOPKEY로 성능을 충족시켜면서, 업무요구사항을 만족시키는 유일한 방법이다.

Sort량 2K vs 1999K의 비밀

비효율 3에서는 Partition By를 삭제하여 Sort가 전혀 발생하지 않는 경우를 보았다. 만약 Sort를 동반하는 경우라면, Partition By를 삭제함에 따라 Sort의 일량에 어떤 영향을 끼칠까? 아래의 SQL을 보자.
 

SELECT *

  FROM (SELECT /*+ INDEX(T PK_SALES_T) */

               PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID, AMOUNT_SOLD,

               ROW_NUMBER ()     OVER (ORDER BY CUST_ID, CHANNEL_ID, TIME_ID NULLS FIRST) AS RN,

               SUM (AMOUNT_SOLD) OVER (ORDER BY CUST_ID, CHANNEL_ID, TIME_ID, PROMO_ID) AS SUM_AMT

          FROM SALES_T T

         WHERE PROD_ID = 30)

 WHERE RN <= 10 ;

 

---------------------------------------------------------------------------------------------------------

| Id  | Operation                      | Name       | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

---------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT               |            |      1 |     10 |00:00:00.34 |   28337 |          |

|*  1 |  VIEW                          |            |      1 |     10 |00:00:00.34 |   28337 |          |

|*  2 |   WINDOW SORT PUSHED RANK      |            |      1 |     11 |00:00:00.34 |   28337 | 2048  (0)|

|   3 |    WINDOW SORT                 |            |      1 |  29282 |00:00:00.30 |   28337 | 1684K (0)|

|   4 |     TABLE ACCESS BY INDEX ROWID| SALES_T    |      1 |  29282 |00:00:00.18 |   28337 |          |

|*  5 |      INDEX RANGE SCAN          | PK_SALES_T |      1 |  29282 |00:00:00.04 |     118 |          |

---------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   1 - filter("RN"<=10)

   2 - filter(ROW_NUMBER() OVER ( ORDER BY "CUST_ID","CHANNEL_ID",INTERNAL_FUNCTION("TIME_ID") NULLS FIRST)<=10)

   5 - access("PROD_ID"=30)

위의 SQL은 개발자가 작성한 원본 SQL에서 ROW_NUMBER 함수의 Partition By절을 삭제하고 실행한 것이다. 이 경우는 over절의 NULLS FIRST 때문에 Sort가 발생되는 경우이다. 이 때에도 Sort가 최소화 되어 PGA 사용량 2K에 불과하다. 개발자가 작성한 원본 실행통계의 Sort량이 1999K임을 감안하면 Partition By의 존재유무는 성능에 지대한 영향을 끼친다고 할 수 있다. Sort량이 이렇게 큰 차이가 나는 이유는 파티션을 삭제한 효과 + RN <= 10 조건 때문이다. 그 filter 조건에 의해서 10개의 배열만 관리하면 되므로 PGA 사용량은 최소화 된다. WINDOW SORT PUSHED RANK의 이러한 성격은 인덱스를 사용하지 않는 경우에도 동일하게 나타난다위의 SQL에서 힌트를 FULL(T)로 바꾸고 실행하여도 PGA 사용량은 동일하다. 

위의 SQL 처럼 ROW_NUMBER 함수의 NULLS FIRST 보다 Partition By절을 먼저 삭제한 사람은 이 원리를 답안으로 사용해도 무방하다하지만 비효율 3에서 설명되었던, Partition By 삭제에 의한 NOSORT STOPKEY 유도 원리는 WINDOW SORT PUSHED RANK와 성격이 다르므로 반드시 알아두어야 한다.    

 

문제2의 비효율 4

진퇴양란, 포기할 것인가?

비효율 3의 튜닝결과를 보면 STOPKEY를 발생시키려고 ROW_NUMBER Partition By를 제거하니 Operation이 두 개로 분리되어 다시 비효율이 발생하였다. 그렇다고 분석함수 SUM을 삭제하자니 업무요구사항이 만족되지 않는다. 어떻게든 STOPKEY를 발생시켜서 29282건을 모두 읽는 비효율을 제거해야 한다.

 

분석함수 SUM을 삭제하는 것은 답이 아니다

보통 이런 경우(진퇴양란)에서는 둘 중에 한가지를 선택한다. 튜닝을 포기하거나 장애물을 제거한다. 열정이 있는 개발자들은 장애물을 제거할 것이다. 분석함수 SUM을 삭제하고, 조회화면에서 프로그램 스크립트를 작성하여 분석함수 SUM의 로직을 대신한다. 물론 이렇게 하는 것은 목적을 달성하는 것이므로, 포기하는 것보다는 만족스럽다. 하지만 나는 포기하지도 말고, 프로그래밍 언어의 도움도 받지 말라고 주장한다. SQL이 아닌 다른 언어의 도움으로 비효율을 해결한 것은 이 문제의 정답이 아니며, SQL만으로 해결할 수 있다.

 

문제2의 비효율 4 해결방법

일부 개발자는 분석함수 SUM을 제거하고 프로그래밍 언어의 도움을 받아 해결하였다. 하지만 우리는 분석함수 SUM을 물리적으로 삭제하는 것이 아니라 실행계획상에서만 삭제하고자 한다. 방법은 SUM/MIN/MAX/AVG 등의 분석함수에 WINDOW의 범위를 명시적으로 작성하면 된다. 물론 범위를 명시적으로 작성해도 답은 동일하다.

 

문제2의 비효율 4가 해결된 SQL과 실행계획 제시

 

SELECT *

  FROM ( SELECT /*+ INDEX(T PK_SALES_T) */ PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID, AMOUNT_SOLD,

                ROW_NUMBER()     OVER (ORDER BY CUST_ID,CHANNEL_ID,TIME_ID ) AS RN,

                SUM(AMOUNT_SOLD) OVER (ORDER BY CUST_ID,CHANNEL_ID,TIME_ID

                                       ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS SUM_AMT

           FROM SALES_T T

          WHERE PROD_ID = 30 )

 WHERE RN <= 10 ; 

 

--------------------------------------------------------------------------------------------------------

| Id  | Operation                     | Name       | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

--------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT              |            |      1 |     10 |00:00:00.01 |      15 |          |

|*  1 |  VIEW                         |            |      1 |     10 |00:00:00.01 |      15 |          |

|*  2 |   WINDOW NOSORT STOPKEY       |            |      1 |     10 |00:00:00.01 |      15 |          |

|   3 |    TABLE ACCESS BY INDEX ROWID| SALES_T    |      1 |     11 |00:00:00.01 |      15 |          |

|*  4 |     INDEX RANGE SCAN          | PK_SALES_T |      1 |     11 |00:00:00.01 |       4 |          |

--------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   1 - filter("RN"<=10)

   2 - filter(ROW_NUMBER() OVER ( ORDER BY "CUST_ID","CHANNEL_ID","TIME_ID")<=10)

   4 - access("PROD_ID"=30)

 

분석함수 SUM을 제거한 것은 아니지만, WINDOW의 범위를 주어 동일한 효과를 얻었다. 정확히 11건만 읽었으므로, 깔끔하게 모든 문제가 정리되었다. Window의 범위를 명시적으로 작성하는 것이 얼마나 중요한지 알 수 있다.

 

여기까지가 답안이다. 이제 어떻게 된 것인지 알아보기 위해 분석함수 ROW_NUMBER를 삭제해 보자.

 

SELECT *

  FROM ( SELECT /*+ INDEX(T PK_SALES_T) */ PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID, AMOUNT_SOLD,

                SUM(AMOUNT_SOLD) OVER (ORDER BY CUST_ID,CHANNEL_ID,TIME_ID

                                       ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS SUM_AMT

           FROM SALES_T T

          WHERE PROD_ID = 30 ) ;

         

 

--------------------------------------------------------------------------------------------------------

| Id  | Operation                     | Name       | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

--------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT              |            |      1 |  29282 |00:00:00.36 |   28340 |          |

|   1 |  VIEW                         |            |      1 |  29282 |00:00:00.36 |   28340 |          |

|   2 |   WINDOW NOSORT               |            |      1 |  29282 |00:00:00.30 |   28340 |          |

|   3 |    TABLE ACCESS BY INDEX ROWID| SALES_T    |      1 |  29282 |00:00:00.20 |   28340 |          |

|*  4 |     INDEX RANGE SCAN          | PK_SALES_T |      1 |  29282 |00:00:00.03 |     121 |          |

--------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   4 - access("PROD_ID"=30)

 

그룹분석함수와 Ranking Family를 같이 사용해도 STOPKEY가 발생한다

그룹분석함수임에도 SORT가 전혀 발생하지 않았다. SUM/MIN/MAX/AVG 등의 분석함수에 WINDOW의 범위를 지정해주면 WINDOW BUFFERWINDOW NOSORT로 바뀐다는 이야기 이다. 이것은 의미 있는 발견이다. 왜냐하면 이때 까지는 SUM/MIN/MAX/AVG 등의 분석함수 때문에 적절한 인덱스를 사용하였지만 WINDOW BUFFER가 발생하여 STOPKEY를 발생시키지 못했다. 그런데 이제 WINDOW NOSORT가 가능해짐으로 Ranking Family STOPKEY가 정상적으로 작동하기 때문이다.

이제 적절한 인덱스가 있고, SUM등의 그룹분석함수를 사용한다면, 다음과 같은 결론을 내릴 수 있다

 

SUM등의 그룹분석함수를 사용한다면, WINDOW의 범위를 지정해주어야 한다. 그렇게 한다면 Sort가 전혀 발생하지 않을 뿐만 아니라, Ranking Family 분석함수를 rownum 처럼 사용할 때, STOPKEY를 발생시켜 성능이 개선된다.”


분석함수를 만날 때의 자세
2
번 문제의 원본 SQL 매우 짧은 코드이지만, 비효율은 4개나 된다튜닝을 완료했다고 자신하는 순간이라도마지막 남은 분석함수의 비효율 하나 때문에 SQL의 성능향상은 도루묵이 될 수 있다. 이런 사실은 SQL을 튜닝 할 때, 분석함수를 만나면 안테나를 세워야 하는 이유가 된다

미래지향

다섯 가지 비효율에 대해 80점 혹은 그 이상을 받은 사람은 분석함수에 관한 튜닝은 상당한 수준에 있다고 생각한다. 반대로, 80점이 안 된다고 실망하지 말자. 왜냐하면 퀴즈에서 실패했더라도, 실무튜닝에서 100점을 받으면 그만이다. 물론 이렇게 되려면, SQL과 실행계획을 보고 비효율을 읽어내는 능력이 필요하다

마지막으로 다섯 가지의 원리로 이 글 전부를 정리 해보자. 다섯 가지의 원리는 대부분 아래의 구조로 되어있다.

1) 소제목
2) ~ 를 하라.
3) 그렇게 하면 ~ 효과가 있다.
 

분석함수의 성능향상 원리


1. min/max 분석함수를 Ranking family로 대체하는 원리

적절한 인덱스를 사용하면서, 최종일자에 해당하는 데이터를 구할 때, MAX(최종일자) 분석함수를 사용하지 말고 Ranking family를 사용하라. 그렇게 하면 WINDOW BUFFER WINDOW NOSORT로 바꾸어 sort를 방지한다.

 

2. sort의 기준을 유지하는 원리
적절한 인덱스가 존재하는 경우, 분석함수의 ORDER BY절에 NULL FIRST LAST를 삭제할 수 있는지 검토하라그렇게 하면 인덱스 사용에 의한 Sort 기준이 유지되므로, 추가적인 sort를 방지한다주의사항은 NULL FIRST LAST를 사용한다고 해서 항상 Sort의 기준이 바뀌는 것은 아니며, 아래의 경우만 해당한다.

            ex1) Index ASC로 사용 + 분석함수의 ORDER BY절에 NULL FIRST 는 추가적인 sort발생

               ex2) Index DESC로 사용 + 분석함수의 ORDER BY절에 NULL LAST 는 추가적인 sort발생

 

3. 여러 개의 분석함수를 하나의 Operation으로 통합하는 원리

적절한 인덱스가 존재하고, 분석함수를 여러 개 사용할 때, 가능하면 OVER절의 Partition By Order By절을 일치시켜라. 그렇게 하면 여러 개의 OPERATION이 통합된다. 다시 말해, 분석함수를 하나만 실행하는 효과를 얻어서 실행시간을 단축시킨다이때 ORDER BY는 완전히 같지 않아도 Operation은 통합될 수 있다단 아래와 같이 좋지 않은 Operation 을 기준으로 통합된다.

         ex1) WINDOW NOSORT + WINDOW SORT   => WINDOW SORT

         ex2) WINDOW NOSORT + WINDOW BUFFER => WINDOW BUFFER

         ex3) WINDOW BUFFER + WINDOW SORT   => WINDOW SORT
 
ORDER BY가 다를 때, Operation이 통합될 수 있는 조건이 있다. 그 세부내용은 본문을 참조하라.

 

4. Ranking Family를 이용한 TOP SQL에서 Sort 최소화의 원리

Ranking Family 분석함수를 인라인뷰 외부에서 Rownum 처럼 Filter로 사용했을 때, 불필요한 Partition By절을 삭제하라. 그렇게 하면 두 가지 경우에 성능이 향상된다. 주의사항은 Partition By절 전체를 제거해야 한다는 것이다. 만약 Partition By절에 컬럼이 하나라도 있으면 Sort가 대량으로 발생된다. 각각의 파티션 값마다 sort를 해야 하므로 어쩔 수 없는 일이다.

첫 번째, 적절한 인덱스가 없어서 FULL SCAN을 하거나 혹은 OVER절의 NULL FIRST LAST등의 원인으로 sort가 발생될 때 이다. 이럴 경우 Partition By을 제거하면 필요한 개수 + 1 만큼만 sort가 발생하므로 성능이 향상된다. 하지만 Rownum과는 달리 STOPKEY가 발생되지 않으므로 비효율은 존재한다. 이 때 발생되는 OperationWINDOW SORT PUSHED RANK 이다.

 

두 번째, 적절한 인덱스가 있어서 sort가 발생되지 않는 경우이다. 이 경우는 첫 번째의 경우와는 달리 sort가 전혀 발생하지 않으며, Rownum 처럼 STOPKEY를 발생시켜 부분범위 처리의 효과를 얻는다. 다시 말해, 분석함수를 Rownum 처럼 사용할 수 있다. 이 때 발생되는 OperationWINDOW NOSORT STOPKEY 이다. 이 기능은 10g R2에서 추가 되었다

WINDOW NOSORT STOPKEY는 주의사항이 있다그룹분석함수의 OVER절과 Ranking Family OVER절의 컬럼순서가 다르면 Operation이 하나로 통합되지 못한다이 경우는 성능이 저하된다왜냐하면 WINDOW SORT 혹은 WINDOW BUFFER 가 먼저 실행되고 그 후에 WINDOW NOSORT STOPKEY가 발생하기 때문에 Block I/O sort량을 감소시키지 못한다. 이런 비효율 때문에 분석함수 여러 개를 하나의 Operation으로 통합하는 원리(원리 3) 역할이 중요한 것이다. 이와 관련된 예제는 비효율3이 해결된 SQL과 실행계획을 참조하기 바란다. 

 

5. 그룹분석함수의 NO-SORT 원리

이 원리는 장점이 두 가지 이다. Sort를 방지하고, STOPKEY를 작동시킨다.

 

적절한 인덱스가 존재하고, sum/min/max/avg등의 Group 분석함수를 사용할 경우 명시적으로 WINDOW의 범위를 지정하라. 그렇게 하면 WINDOW BUFFER Operation WINDOW NOSORT로 바꾸어 불필요한 sort를 방지한다. 물론 여기서 이야기 하는 그룹분석함수는 현재 row 까지의 누적집계(Cumulative total) 이다.

 

더욱 좋은 것은, Ranking family Rownum 처럼 사용하고 있다면, STOPKEY를 사용할 수 있게 한다. Group 분석함수를 사용하면, WINDOW BUFFER가 발생하고, sort가 발생되는데, 이 때문에 STOPKEY를 작동 시킬 수 없다. 그런데 WINDOW의 범위를 지정하여 WINDOW NOSORT로 바꾸면 sort가 발생하지 않기 때문에 STOPKEY가 작동한다. WINDOW의 범위를 지정하는 방법은 아래와 같다. 범위를 생략해도 결과는 같지만, 성능이 저하된다.

 ex) SUM(AMOUNT_SOLD) OVER(ORDER BY CUST_ID,CHANNEL_ID,TIME_ID ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)

참고: Ranking family를 사용하여 WINDOW STOPKEY가 발생하는 경우는 SORT가 발생하지 않는 경우(NOSORT)뿐이다그룹분석함수에 의해서 추가적인 SORT를 해야 한다면, 전체범위로 처리됨으로 STOPKEY가 발생하지 않는다.


위의 다섯 가지 원리가 적용되지 않은 것이 각각의 비효율이다. 즉 위의 다섯 가지 원리를 비효율의 해결방법으로 작성한 사람은 100점이다.


자동튜닝기능이 아쉬워

이 글의 결론을 보고 많은 이들이 두 가지를 아쉬워할 것이다. 예컨대, 분석함수 SUM WINDOW의 범위를 생략했더라도 자동으로 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW를 추가해 준다면 얼마나 좋을까? 불필요한 Partition By절을 수동으로 삭제해야 하는 것 또한 마찬가지이다. 아직까지 옵티마이저는 사람을 따라올 수 없다. 사람의 손길을 여전히 필요로 한다따라서 전문 개발자라면 Partition By Order By 혹은 Window의 범위지정 등의 코딩을 할 때 내부적으로 어떤 일이 발생하는지 알아야 한다. 그냥 코딩을 하는 사람과 그것이 성능상 어떤 의미를 지니는지 알고 개발하는 사람과는 차이가 크다

 

이상으로 분석함수의 내부에 대해 3회에 걸쳐 알아보았다. 1/2부의 내용과 위의 결론 다섯 가지를 완벽히 이해한다면, 분석함수를 사용한 SQL과 실행계획, 그리고 인덱스 구조만 보고도 순간적으로 비효율을 잡아낼 수 있다분석함수의 비효율을 만나면 번개가 치듯이 생각이 번쩍 들것이다실행계획을 보고 비효율의 냄새를 맡을 줄 아는 능력은 중요하다. 

마지막으로, 분석함수의 실행계획에 관한 글을 의뢰한 개발자에게 감사를 표한다. 아무래도 내가 그 개발자에게 맥주를 사는 것이 더 좋을 것 같다. 왜냐하면 그 사람 덕분에 좋은 것(Window의 범위 지정의 효과)을 발견할 수 있었기 때문이다. 두서 없는 긴 글을 읽어주신 독자 여러분들께 감사 드린다 

------------------------------------------------------------------------------------------------------------------------------------------------

이번 글은 여기까지 이다. 아래의 글은 이전에 올렸던 글의 정답보다 더 좋은 것이 발견되었음을 알리는 것이다. 

 

분석함수의 실행계획을 정리하면서 Group(SUM/MIN/MAX/AVG) 분석함수에 WINDOW의 범위를 명시적으로 작성하는 것이 얼마나 중요한지 필자도 깨닫게 되었다. 예컨대, Pagination 분석함수의 위험한 조합 글에서 도출된 정답보다 더 좋은 것은 WINDOW의 범위를 명시하는 것이다. 따라서 분석함수를 밖으로 빼는 것보다, 아래와 같이 window의 범위를 지정해 주는 것이 더 유리하다.

 

SELECT *

  FROM (SELECT s.*, ROWNUM rnum

          FROM (SELECT   /*+ INDEX(S PK_SALES_T) */

                         s.prod_id, s.cust_id,  s.channel_id, s.time_id, amount_sold,

                         sum (amount_sold) OVER (PARTITION BY s.cust_id ORDER BY s.channel_id, s.time_id

ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS sum_amt

                    FROM sales_t s

                   WHERE s.prod_id = :v_prod_id  --> 30 대입

                ORDER BY s.cust_id, s.channel_id, s.time_id

               ) s

         WHERE ROWNUM <= :v_max_row  --> 20 대입

       )

 WHERE rnum >= :v_min_row ;          --> 1 대입

 

----------------------------------------------------------------------------------------------------------

| Id  | Operation                       | Name       | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

----------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT                |            |      1 |     20 |00:00:00.01 |      24 |          |

|*  1 |  VIEW                           |            |      1 |     20 |00:00:00.01 |      24 |          |

|*  2 |   COUNT STOPKEY                 |            |      1 |     20 |00:00:00.01 |      24 |          |

|   3 |    VIEW                         |            |      1 |     20 |00:00:00.01 |      24 |          |

|   4 |     WINDOW NOSORT               |            |      1 |     20 |00:00:00.01 |      24 |          |

|   5 |      TABLE ACCESS BY INDEX ROWID| SALES_T    |      1 |     20 |00:00:00.01 |      24 |          |

|*  6 |       INDEX RANGE SCAN          | PK_SALES_T |      1 |     20 |00:00:00.01 |       4 |          |

----------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   1 - filter("RNUM">=:V_MIN_ROW)

   2 - filter(ROWNUM<=:V_MAX_ROW)

   6 - access("S"."PROD_ID"=:V_PROD_ID)

 

20건에 대해서 조차 SORT가 전혀 발생하지 않는다. 따라서 Pagination 분석함수의 위험한 조합 이라는 글은 수정되어야만 한다. 내가 작성한 글을 내가 뒤집었다. 내 것이라도 비판적으로 바라보아야 한다. 최종이론이란 없는 것이므로
 

------------------------------------------------------------------------------------------------------------------------------------------------

PS: 몇분이 구두로 혹은 메일로 괴상한 질문을 해서 답글을 남긴다.

Question: 어떻게 이런 책과 글을 쓰며, 주제는 어떻게 잡으며, 연구는 어떻게 하는가?  

--> 이상한 질문이다. 내가 용빼는 재주라도 있나?

Answer:

필자의 머리가 좋아서 글을 쓸 수 있는 게 아니다오히려 그 반대이다. 똑똑한 다른 사람이라면, 내가 연구랍시고 소비한 시간의 절반만 투자해도 이 글을 썼을 것이다나는 다른 사람과 달리 영민하지 못하여 하찮은 문제에도 많은 시간을 소모하며, 실패하면 다시 조금씩 앞으로 전진 할 수 밖에 없다그렇다고 다른 재주가 있느냐 하면 그렇지 않다. 방향감각이 둔하여 연구가 막다른 길로 빠진 적이 여러 번일 뿐만 아니라 남들이 가지고 있는 창의성, 순발력, 영어실력문장력, 준수한 외모를 나는 가지지 못했고, 심지어 노래방에서는 박자를 못 맞추는 절대음감까지 그 어느 것도 잘하는 게 없다아무리 생각해도 세상은 불공평 한 듯 하다

내가 할 수 있는 일은 나의 유일한 단점이자 장점을 살리는 수뿐이다실수를 하더라도, 거북이 근성으로 시간이 나는 대로 조금씩 연구를 하여 이런 글을 쓰는 수 밖에 없다. 글의 주제와 관련된 것도 특별한 것은 없다. 소주한잔 마시면서 어떤 글을 쓸지 동료들과 소통하거나, 독자들이 필요한 글을 나에게 요청한다내가 직접 글감을 고른 적은 5번중의 한번 정도이다글의 주제가 정해지면 무작정 글을 쓴다. 글을 쓰다가 모르는 것이 있으면(모르는 게 많다) 그에 맞춰 test(연구)를 한다. 궁금증이 해결되면 다시 글을 쓴다. 이것이 끝이다.

 

이 글도 위에서 설명한 절차와 똑같이 진행되었다. 단언하건대, 분석함수의 성능에 대해 이 문서만큼 깊이 다룬 책이나 매뉴얼은 지금까지 없다이런 일이 가능한 이유는 내가 아둔하고, 모르는 게 많은 거북이라서 그런 것이 아닐까?  

Posted by extremedb
,

필자가 제안한 SQL 튜닝방법론을 읽어 보았다면 다음을 기억할 것이다.


“SQL
튜닝방법론은 온라인 SQL 90% 만 해결할 수 있으며,
나머지 10%는 그때 그때 마다 다르게(On the fly 모드) 처리된다. 또한 그것들은 책이나 매뉴얼에 나와있지 않기 때문에 경험치 이거나 실험과 연구의 결과로 알아내는 것들이다.”

이번 글은 방법론에서 다룰 수 없는, 나머지 10%에 해당하는 경험치와 연구의 결과이다.


분석함수의 나머지 10%

오늘은 SQL 튜닝방법론에서 언급한 나머지 10%를 만났을 때 어떻게 해야 하는지 알아보자. 이번 글이 분석함수의 실행계획 시리즈의 마지막이다. 12에서 분석함수의 실행계획에 대해서 알아보았다. 하지만 두 개의 글로도 분석함수와 관련된 성능이슈를 모두 알 수는 없다. 왜냐하면 분석함수와 관련된 세밀한 튜닝포인트가 더 있기 때문이다. 이번에 다루게 될 실전 튜닝은 단순히 적절한 인덱스를 생성하여 비효율을 없애는 것이 아니다. 적절한 인덱스를 생성하는 것은 1부와 2부만 이해해도 충분하며, 이 글에서는 1부와 2부에서 언급하지 않은 것들을 설명하게 된다. 따라서 이 글에서 제시된 문제를 해결하지 못하면 분석함수를 튜닝 할 수 없는 경우를 많이 만날 것이다.

 

퀴즈는 SQL 두 개이며, 5가지 비효율을 개선해야 한다

이번에는 실전에서 발생할 수 있는 비효율에 대해서 두 가지 관점으로 논의한다. 분석함수를 사용할 때, 실행계획을 보고 비효율을 잡아내는 능력을 향상시키고, 실제로 SQL튜닝을 해보는 것이다. 반드시 문제를 직접 풀어보기 바란다. 당신이 튜너의 입장이 되어, 성능이 좋지 않은 SQL을 두 개 받았다고 가정하고 튜닝문제를 풀어보기 바란다. 두 가지 SQL 중 하나는 쉽고 나머지 하나는 어렵다. 두 개의 SQL에는 5가지의 튜닝 포인트가 있다. 각각의 포인트는 20점이며 5가지를 모두 맞추는 경우는 100점이 된다. 채점 시 중요한 점은 SQL을 맞추는 것이 아니라 비효율을 몇 개 잡아내었는지, 또한 각각의 비효율을 개선시킬 수 있는 방법은 몇 개나 생각했는지를 채점하는 것이다. 

  

이제 실습을 위한 테이블과 인덱스를 생성하자.

CREATE TABLE SALES_T NOLOGGING AS SELECT * FROM SALES;

 

ALTER TABLE SALES_T ADD CONSTRAINT PK_SALES_T

PRIMARY KEY (PROD_ID, CUST_ID, CHANNEL_ID, TIME_ID) USING INDEX;

 

개발자가 작성한 문제의 SQL 1

 

SELECT *

  FROM ( SELECT /*+  INDEX(T PK_SALES_T) */ PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID, AMOUNT_SOLD,

                MAX(TIME_ID) OVER( PARTITION BY CUST_ID ,CHANNEL_ID ) AS MAX_TIME

           FROM SALES_T T

          WHERE PROD_ID = 30 )

 WHERE TIME_ID = MAX_TIME;

 

업무설명 : 상품번호 30인 데이터에 대하여 고객별, 채널 별로 가장 최근의 판매량을 나타내시오.

             단 고객별, 채널별로 가장 최근의 데이터는 2건 이상일 수 있음.

-----------------------------------------------------------------------------------------------

| Id  | Operation                     | Name       | A-Rows |   A-Time   | Buffers | Used-Mem |

-----------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT              |            |  12649 |00:00:00.31 |   28337 |          |

|*  1 |  VIEW                         |            |  12649 |00:00:00.31 |   28337 |          |

|   2 |   WINDOW BUFFER               |            |  29282 |00:00:00.27 |   28337 | 1495K (0)|

|   3 |    TABLE ACCESS BY INDEX ROWID| SALES_T    |  29282 |00:00:00.17 |   28337 |          |

|*  4 |     INDEX RANGE SCAN          | PK_SALES_T |  29282 |00:00:00.03 |     118 |          |

-----------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   1 - filter("TIME_ID"="MAX_TIME")

   4 - access("PROD_ID"=30)

 

WHERE 절에 TIME_ID = MAX_TIME 이 존재하는 이유는 고객별, 채널별로 MAX(TIME_ID)에 해당하는 데이터가 두건 이상일 수 있기 때문이다. 이런 경우는 자주 발생한다. 이해를 돕기 위해 비슷한 예를 들어보자. 부서별로 최대급여를 받는 사람들을 출력하고자 할 때, 1번 부서의 최대급여는 1억 원이라고 하자. 그런데 그 부서에서 1억 원(최대급여)을 받는 사람은 강호동, 유재석 둘이라는 이야기 이다.

 

위의 SQL은 답이 정확하다. 하지만 성능은 비효율이 있어서 별로 신통치 못하다. 이제 문제를 보자. SQL의 비효율을 개선하는 것이 문제이다.

 

문제1) 위의 SQL 에서 성능상 문제점을 발견하고 개선하시오. 문제점은 두 가지(분석함수의 관점, 일반적인 SQL튜닝의 관점) 이다. 단 인덱스를 추가로 생성하거나 변경해서는 안 된다.

아래처럼 정답을 제시하면 된다.

 

문제1의 비효율 1 : 구체적인 설명

문제1의 비효율 1의 해결방법 : 구체적인 설명 

문제1의 비효율 1이 해결된 SQL과 실행계획 제시

 

문제1의 비효율 2 : 구체적인 설명

문제1의 비효율 2의 해결방법 : 구체적인 설명 

문제1의 비효율 2이 해결된 SQL과 실행계획 제시

 

이제 실제로 답안을 작성해보자.(답안 Sample)

 

문제1의 비효율 1 : SALES_T 테이블에 Random Access가 불필요하게 많이 발생한다. 12649 번만 테이블로 액세스 하면 되지만 실제로는 29282 번 액세스함으로써 비효율이 발생되었다.

 

문제1의 비효율 1의 해결방법 : PK인덱스에 AMOUNT_SOLD가 포함되어 있지 않으므로 Select 절에서 AMOUNT_SOLD를 빼면 인덱스만 액세스 하게 됨. 이때 Rowid를 추가로 Select 해야 한다. 이후에 이미 인덱스에서 Filter Rowid로 테이블을 액세스하면 Random Access정확히 12649번 만 시도한다. 아래에 SQL이 있으므로 참조하시오.

 

문제1의 비효율1이 해결된 SQL과 실행계획 제시:

 

SELECT /*+ LEADING(S) USE_NL(S1) */

        S.PROD_ID, S.CUST_ID, S.TIME_ID, S.CHANNEL_ID, S1.AMOUNT_SOLD

  FROM ( SELECT /*+  INDEX(T PK_SALES_T) */ PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID,

                MAX(TIME_ID) OVER( PARTITION BY CUST_ID ,CHANNEL_ID ) AS MAX_TIME,

                ROWID AS RID

           FROM SALES_T T

          WHERE PROD_ID = 30 ) S,

       SALES_T S1

 WHERE S.TIME_ID = S.MAX_TIME

   AND S1.ROWID = S.RID;

  

------------------------------------------------------------------------------------------------------

| Id  | Operation                   | Name       | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT            |            |      1 |  12649 |00:00:02.72 |   11237 |          |

|   1 |  NESTED LOOPS               |            |      1 |  12649 |00:00:02.72 |   11237 |          |

|*  2 |   VIEW                      |            |      1 |  12649 |00:00:00.18 |     118 |          |

|   3 |    WINDOW BUFFER            |            |      1 |  29282 |00:00:00.13 |     118 | 1432K (0)|

|*  4 |     INDEX RANGE SCAN        | PK_SALES_T |      1 |  29282 |00:00:00.03 |     118 |          |

|   5 |   TABLE ACCESS BY USER ROWID| SALES_T    |  12649 |  12649 |00:00:02.48 |   11119 |          |

------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   2 - filter("S"."TIME_ID"="S"."MAX_TIME")

   4 - access("PROD_ID"=30)

 

이렇게 해서 Sample 답안을 작성해 보았다. 이 방법은 분석함수의 튜닝방법이 아니라 예외적으로 SQL 튜닝방법론에서 언급되었던 방법을 사용하였다. 물론 여기서 페이징처리를 한다면 추가적인 성능향상이 가능하지만 Sample이므로 여기서 멈추기로 한다. 위의 답안은 Sample 이므로 점수에서 빠진다. 또한 이 부분은 분석함수의 튜닝 방법이 아니다. 하지만 앞으로 풀게 될 문제들은 모두 분석함수와 관련된 문제들이다.

 

이제 시작해보자.

 

문제1에서 분석함수와 관련된 비효율이 하나 있다. 이와 관련된 비효율, 해결방법, 그리고 해결된 SQL과 실행계획을 제시해야 한다.

 

문제1의 비효율 2 : 구체적인 설명

문제1의 비효율 2 해결방법 : 구체적인 설명 

문제1의 비효율 2가 해결된 SQL과 실행계획 제시

 

이제 좀더 어려운 문제를 풀어보자. 아래의 문제를 100% 맞춘 사람은 아무도 없었다.

 

문제2) 아래의 SQL 에서 성능상 문제점을 발견하고 개선하시오. 성능상 문제점은 4가지 이다. 4가지의 비효율을 발견하고 이를 모두 개선해야 한다. 단 인덱스를 추가로 생성하거나 변경하면 안 된다.

SELECT *

  FROM

  ( SELECT /*+ INDEX(T PK_SALES_T) */ PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID, AMOUNT_SOLD,

                ROW_NUMBER()     OVER ( PARTITION BY PROD_ID ORDER BY CUST_ID,CHANNEL_ID,TIME_ID NULLS FIRST) AS RN,

                SUM(AMOUNT_SOLD) OVER ( ORDER BY CUST_ID,CHANNEL_ID,TIME_ID,PROMO_ID   ) AS SUM_AMT

           FROM SALES_T T

          WHERE PROD_ID = 30 )

 WHERE RN <= 10 ;

 

---------------------------------------------------------------------------------------------------------

| Id  | Operation                      | Name       | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

---------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT               |            |      1 |     10 |00:00:00.43 |   28337 |          |

|*  1 |  VIEW                          |            |      1 |     10 |00:00:00.43 |   28337 |          |

|*  2 |   WINDOW SORT PUSHED RANK      |            |      1 |  29282 |00:00:00.40 |   28337 | 1999K (0)|

|   3 |    WINDOW SORT                 |            |      1 |  29282 |00:00:00.30 |   28337 | 1684K (0)|

|   4 |     TABLE ACCESS BY INDEX ROWID| SALES_T    |      1 |  29282 |00:00:00.18 |   28337 |          |

|*  5 |      INDEX RANGE SCAN          | PK_SALES_T |      1 |  29282 |00:00:00.04 |     118 |          |

---------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   1 - filter("RN"<=10)

   2 - filter(ROW_NUMBER() OVER ( PARTITION BY "PROD_ID" ORDER BY "CUST_ID","CHANNEL_ID",INTERNAL_FUNCTION("TIME_ID")

              NULLS FIRST)<=10)

   5 - access("PROD_ID"=30)

 

아래처럼 정답을 제시하면 된다.

 

문제2의 비효율 1 : 구체적인 설명

문제2의 비효율 1의 해결방법 : 구체적인 설명 

문제2의 비효율 1이 해결된 SQL과 실행계획 제시

 

문제2의 비효율 2 : 구체적인 설명

문제2의 비효율 2의 해결방법 : 구체적인 설명 

문제2의 비효율 2가 해결된 SQL과 실행계획 제시

 

문제2의 비효율 3 : 구체적인 설명

문제2의 비효율 3의 해결방법 : 구체적인 설명 

문제2의 비효율 3이 해결된 SQL과 실행계획 제시

 

문제2의 비효율 4 : 구체적인 설명

문제2의 비효율 4의 해결방법 : 구체적인 설명 

문제2의 비효율 4가 해결된 SQL과 실행계획 제시

 

문제의 구성
이렇게 하여 총 다섯 문제가 출제되었다. 문제 1에는 비효율이 하나이고 문제 2에는 비효율이 넷이다. 다섯 가지 비효율 모두 분석함수와 관련된 것들이다. 실습환경은 Oracle 10g R2, Oracle 11g R1, Oracle 11g R2로 한정한다. 답안은 2011년 2 18일 공개될 예정이다. 답안을 바로 공개하지 않는 이유는 튜닝실습을 유도하기 위함이다. SQL 두개를 분석하는데 이틀이면 넉넉한 시간이므로 충분히 생각하고 답을 작성하기 바란다. 자신이 튜너가 되었다고 가정하고 비효율을 모두 찾아보길 바란다.


다른 사람들은 몇 점 정도 받았나?

몇몇 지인들에게 퀴즈를 내본 결과 100점은 아무도 없었다. 80점이 평균적인 튜너의 수준이라는 이야기 이다. 개발자의 경우는 60점을 받더라도 실망해서는 안 된다. 개발자가 60점이면 괜찮은 수준이다. 점수에 대해 오해는 하지 말기 바란다. 이 퀴즈로 받은 점수는 분석함수와 관련된 것일 뿐, 전반적인 SQL 튜닝능력에 대한 점수가 아니다. 또한 100 점 이상을 받을 수도 있다. 필자가 알고 있는 튜닝포인트는 5가지뿐이다. 여러분들이 6~7가지 이상의 비효율과 튜닝 포인트를 잡아내어 필자를 능가하는 점수를 받는 것도 가능하다.


행운을 빈다.

 

Posted by extremedb
,

빠른 이해를 위해서 이전 을 먼저 보고 이 글을 읽기 바란다. 이 글의 1부에서 WINDOW SORT, WINDOW SORT PUSHED RANK, WINDOW NOSORT, WINDOW NOSORT STOPKEY OPERATION들을 정리해 보았다. 이번에는 나머지 네 가지 Operation에 관해 알아볼 것이다. 이번 시간을 마치면 분석함수의 실행계획은 모두 정리된다.

 

WINDOW (SORT)  

WINDOW (SORT PUSHED RANK)

WINDOW (NOSORT)

WINDOW (NOSORT STOPKEY)   <-- 저번 시간에는 여기까지

WINDOW (BUFFER)

WINDOW (BUFFER PUSHED RANK)

WINDOW (CHILD PUSHED RANK)

WINDOW (IN SQL MODEL) SORT <-- 이번 시간에는 여기까지

 

5. WINDOW BUFFER

WINDOW BUFFER는 기본적으로 Ranking Family를 사용하지 않는 경우에 발생한다. 또한 인덱스가 적절하거나, 혹은 OVER절에 ORDER BY구문이 없는 경우에 발생한다. where절의 filter 조건이 있다면, 전체 건을 Sort 하지 않지만 최종 결과 건(Window의 범위)에 대해서는 sort가 필요하다.

 

SELECT PROD_ID,

       SUM(AMOUNT_SOLD) OVER() AS SUM_AVG

  FROM SALES_T

WHERE PROMO_ID = 33;

 

---------------------------------------------------------------------------------

| Id  | Operation          | Name    | A-Rows |   A-Time   | Buffers | Used-Mem |

---------------------------------------------------------------------------------

|   0 | SELECT STATEMENT   |         |   2074 |00:00:00.04 |    4440 |          |

|   1 |  WINDOW BUFFER     |         |   2074 |00:00:00.04 |    4440 |59392  (0)|

|*  2 |   TABLE ACCESS FULL| SALES_T |   2074 |00:00:00.03 |    4440 |          |

---------------------------------------------------------------------------------

 

위의 결과를 보면 2074건에 대하여 Sort가 발생하였다. 하지만 아래는 다르다.

 

SELECT PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID, AMOUNT_SOLD,

       SUM(AMOUNT_SOLD) OVER(ORDER BY NULL) AS RN

  FROM SALES_T ;

 

---------------------------------------------------------------------------------

| Id  | Operation          | Name    | A-Rows |   A-Time   | Buffers | Used-Mem |

---------------------------------------------------------------------------------

|   0 | SELECT STATEMENT   |         |    918K|00:00:03.69 |    4440 |          |

|   1 |  WINDOW BUFFER     |         |    918K|00:00:03.69 |    4440 |   35M (0)|

|   2 |   TABLE ACCESS FULL| SALES_T |    918K|00:00:00.96 |    4440 |          |

---------------------------------------------------------------------------------

 

WHERE 절이 없으므로 WINDOW의 범위는 전체이다. 전체 건( 92만건) SORT해야 하므로 PGA 35M나 사용하였다. OVER절에 ORDER BY가 없지만, 전체 건을 SORT 했으므로 WINDOW BUFFER WINDOW SORT Operation이 발생한 것과 마찬가지 이다.

 

이제 인덱스를 사용하면서 OVER절에 ORDER BY가 있는 경우를 살펴보자.

 

SELECT /*+ INDEX(S PK_SALES_T) */

         SUM(AMOUNT_SOLD) OVER(ORDER BY S.CUST_ID, S.CHANNEL_ID, S.TIME_ID ) AS "누적금액",

        S.*

  FROM SALES_T S

 WHERE S.PROD_ID = 22

 ORDER BY S.CUST_ID, S.CHANNEL_ID, S.TIME_ID ;

 

----------------------------------------------------------------------------------------------

| Id  | Operation                    | Name       | A-Rows |   A-Time   | Buffers | Used-Mem |

----------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT             |            |   3441 |00:00:00.06 |    2911 |          |

|   1 |  WINDOW BUFFER               |            |   3441 |00:00:00.06 |    2911 |  206K (0)|

|   2 |   TABLE ACCESS BY INDEX ROWID| SALES_T    |   3441 |00:00:00.05 |    2911 |          |

|*  3 |    INDEX RANGE SCAN          | PK_SALES_T |   3441 |00:00:00.04 |      17 |          |

----------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   3 - access("S"."PROD_ID"=22)

 

인덱스 (PROD_ID + CUST_ID + CHANNEL_ID + TIME_ID)컬럼과 OVER절의 ORDER BY구문의 컬럼순서가 동일하므로 WINDOW SORT 가 발생하지 않고 WINDOW BUFFER 가 발생하였다. SQL의 마지막에 위치한 ORDER BY 구문은 인덱스 컬럼과 일치하므로 별도의 SORT가 필요하지 않다.

 

6. WINDOW BUFFER PUSHED RANK

OPERATION RANK가 있으므로 Ranking Family를 사용한 것이다. OPERATION을 볼 수 있으면 좋겠지만 10g에서 사라졌다. Oracle 9i 까지는 적절한 인덱스가 있는 경우에 Ranking Family를 사용하고, /인라인 뷰 외부에서 분석함수를 Filter로 사용한다면 WINDOW BUFFER PUSHED RANK가 발생하였다. 하지만 10g부터는 더 효율적인 WINDOW NOSORT(10g R1) 혹은 WINDOW NOSORT STOPKEY(10g R2)로 대체되었다. 참고로 야함나르 독자님에 의하면 버전 9.2.0.7에서도 WINDOW NOSORT가 발생한다고 한다. ()

 

9i에서 직접 테스트 해보면 좋겠지만 환경이 그렇지 못하므로 Julian Dyke의 테스트 내용을 인용한다.

 

This example was developed using Oracle 9.2.0.1 on Windows 2000

 

This example requires the following table definition

 

    CREATE TABLE t1 (c1 NUMBER NOT NULL,c2 NUMBER);

 

    CREATE INDEX i1 ON t1 (c1);

The table must be analysed

 

    ANALYZE TABLE t1 COMPUTE STATISTICS;

The statement

 

    SELECT c2,r1

FROM ( SELECT c2,RANK () OVER (ORDER BY c1) AS r1

               FROM t1 )

    WHERE r1 < 10;

 

9i의 실행계획

0     SELECT STATEMENT Optimizer=CHOOSE

1   0   VIEW

2   1     WINDOW (BUFFER PUSHED RANK)

3   2       TABLE ACCESS (BY INDEX ROWID) OF 'T1'

4   3         INDEX (FULL SCAN) OF 'I1' (NON-UNIQUE)

 

테스트의 출처 http://www.juliandyke.com/Optimisation/Operations/WindowBufferPushedRank.html

 

 

10g R2의 실행계획

---------------------------------------------------------------------------

| Id  | Operation                     | Name | Rows  | Bytes | Cost (%CPU)|

---------------------------------------------------------------------------

|   0 | SELECT STATEMENT              |      |     1 |    26 |     1   (0)|

|*  1 |  VIEW                         |      |     1 |    26 |     1   (0)|

|*  2 |   WINDOW NOSORT STOPKEY       |      |     1 |    26 |     1   (0)|

|   3 |    TABLE ACCESS BY INDEX ROWID| T1   |     1 |    26 |     1   (0)|

|   4 |     INDEX FULL SCAN           | I1   |     1 |       |     1   (0)|

---------------------------------------------------------------------------

 

10g R2에서는 WINDOW BUFFER PUSHED RANK 대신에 WINDOW NOSORT STOPKEY 가 발생하므로 SORT가 전혀 발생하지 않으며 STOPKEY가 발생하여 필요한 부분만 SCAN된다. 따라서 TOP SQL이나 페이징 처리시 상당한 성능개선 효과를 볼 수 있다.

 

7. WINDOW CHILD PUSHED RANK

Ranking Family를 사용하고 인라인 뷰 외부에서 분석함수를 Filter로 사용한다면 실행계획에 ~PUSHED RANK 혹은 ~STOPKEY가 발생한다고 하였다. WINDOW CHILD PUSHED RANK도 마찬가지이다. 단 하나의 차이점은 Parallel Query를 사용했다는 것이다.

 

SELECT *

  FROM (SELECT /*+ FULL(T) PARALLEL(T 4) */  PROMO_ID,

               RANK() OVER(ORDER BY PROD_ID) AS RN

          FROM SALES_T T)

WHERE RN = 1

 

--------------------------------------------------------------------------------------------

| Id  | Operation                    | Name     | A-Rows |   A-Time   | Buffers | Used-Mem |

--------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT             |          |   6002 |00:00:01.84 |       5 |          |

|*  1 |  VIEW                        |          |   6002 |00:00:01.84 |       5 |          |

|*  2 |   WINDOW SORT PUSHED RANK    |          |   6003 |00:00:01.83 |       5 |  142K (0)|

|   3 |    PX COORDINATOR            |          |   6006 |00:00:01.81 |       5 |          |

|   4 |     PX SEND QC (RANDOM)      | :TQ10000 |      0 |00:00:00.01 |       0 |          |

|*  5 |      WINDOW CHILD PUSHED RANK|          |      0 |00:00:00.01 |       0 |96256  (0)|

|   6 |       PX BLOCK ITERATOR      |          |      0 |00:00:00.01 |       0 |          |

|*  7 |        TABLE ACCESS FULL     | SALES_T  |      0 |00:00:00.01 |       0 |          |

--------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   1 - filter("RN"=1)

   2 - filter(RANK() OVER ( ORDER BY "PROD_ID")<=1)

   5 - filter(RANK() OVER ( ORDER BY "PROD_ID")<=1)

   7 - access(:Z>=:Z AND :Z<=:Z)

 

ID 5번에 WINDOW CHILD PUSHED RANK가 발생했음에도 ID 2번에 WINDOW SORT PUSHED RANK가 발생한 이유는 QUERY COORDINATOR가 개별 PARALLEL SLAVE들을 취합해야 하기 때문이다.

 

8. WINDOW IN SQL MODEL SORT

MODEL절에서 분석함수를 사용하는 경우에 발생한다.

 

SELECT PROD_ID,a1,a2

  FROM (SELECT PROD_ID,SUM(AMOUNT_SOLD) a1

          FROM SALES_T

         GROUP BY PROD_ID )

       MODEL DIMENSION BY (PROD_ID)

       MEASURES (a1,0 a2)

       RULES ( a2[any] = SUM (a1) OVER () ) ;  --> 분석함수 사용

 

------------------------------------------------------------------------------------------

| Id  | Operation                   | Name    | A-Rows |   A-Time   | Buffers | Used-Mem |

------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT            |         |     72 |00:00:01.91 |    4440 |          |

|   1 |  SQL MODEL ORDERED          |         |     72 |00:00:01.91 |    4440 |  405K (0)|

|   2 |   HASH GROUP BY             |         |     72 |00:00:01.91 |    4440 | 2516K (0)|

|   3 |    TABLE ACCESS FULL        | SALES_T |    918K|00:00:00.93 |    4440 |          |

|   4 |   WINDOW (IN SQL MODEL) SORT|         |     72 |00:00:00.01 |       0 | 2048  (0)|

------------------------------------------------------------------------------------------

 

위와 아래의 SQL은 같은 결과를 출력한다. 그리고 PGA 사용량도 비슷하다.

 

SELECT PROD_ID,SUM(AMOUNT_SOLD) a1, sum(SUM(AMOUNT_SOLD)) OVER() as a2

  FROM SALES_T

 GROUP BY PROD_ID ;

 

----------------------------------------------------------------------------------

| Id  | Operation           | Name    | A-Rows |   A-Time   | Buffers | Used-Mem |

----------------------------------------------------------------------------------

|   0 | SELECT STATEMENT    |         |     72 |00:00:01.91 |    4440 |          |

|   1 |  WINDOW BUFFER      |         |     72 |00:00:01.91 |    4440 | 2048  (0)|

|   2 |   HASH GROUP BY     |         |     72 |00:00:01.91 |    4440 | 2515K (0)|

|   3 |    TABLE ACCESS FULL| SALES_T |    918K|00:00:00.93 |    4440 |          |

----------------------------------------------------------------------------------

 

단순히 전체 합계를 출력하는 것이라면 MODEL절 보다는 분석함수를 사용하는 것이 조금 유리하다. SQL MODEL ORDERED OPERATION이 없기 때문이다. 참고로 MODEL절이 무엇인지 궁금한 사람은 SQL 포기하지마라 , Model 절에 대하여 라는 두 가지 글을 보기 바란다.

 

결론

 

WINDOW (SORT) :

-FTS를 사용하거나 인덱스가 적절하지 않은 경우 발생 

WINDOW (SORT PUSHED RANK) :

-위와 같으나 Ranking Family를 사용해야 하며, /인라인뷰 외부에서 분석함수를 filter로 사용하는 경우 발생

WINDOW (NOSORT) :

-인덱스가 적절한 경우나, OVER절에 ORDER BY가 없는 Ranking Family를 사용한 경우 발생

WINDOW (NOSORT STOPKEY) :

-위와 같으나 Ranking Family를 사용해야 하며, /인라인뷰 외부에서 분석함수를 filter로 사용하는 경우 발생

-10g R2 신기능

WINDOW (BUFFER) :

    - Ranking Family를 사용하지 않고 ORDER BY가 없는 경우이거나 적절한 인덱스를 사용하는 경우 발생

WINDOW (BUFFER PUSHED RANK) :

- Ranking Family를 사용하고, /인라인뷰 외부에서 분석함수를 filter로 사용해야 하며, 8i~9i 에서만 발생됨.

- 비효율을 개선하여 10g R1에서는 WINDOW (NOSORT)가 발생하며, 10g R2에서는 WINDOW (NOSORT STOPKEY)가  발생됨

WINDOW (CHILD PUSHED RANK) :

   - Ranking Family를 사용하고, /인라인뷰 외부에서 분석함수를 filter로 사용해야 하며, Parallel Query를 사용하는 경우 발생

WINDOW (IN SQL MODEL) SORT

   - Model 절에서 분석함수로 Rule을 정하는 경우 발생

 

심화학습이 필요한 이유

저번 글과 이번 글에서 분석함수를 사용할 때 나올 수 있는 실행계획을 정리해 보았다. 이제 분석함수의 실행계획을 보면 어떤 환경에서 실행되었는지, 어떤 비효율이 있는지 감을 잡을 수 있을 것이다. 다음 글에서는 위의 결론을 이용하여 좀더 고차원적인 심화과정에 도전할 것이다. 다음 글의 목적은 실전에서 분석함수를 사용할 때, 비효율을 잡아내는 능력을 향상시키고, 실제로 성능개선을 해보는 것이다.

Posted by extremedb
,

Oracle Data Access Pattern

Partition Access Pattern

Oracle 조인 방법 25가지

 

필자는 위의 글들을 이미 게시하였다. 그래서 Access Path Join Method는 정리가 되었고, 블로그로 배포되었으므로 그것으로 끝이라고 생각했다. 하지만 어느 개발자와 대화를 해보니 그런 생각이 짧았음을 알게 되었다.

 

 

개발자 : 위의 3가지 문서는 잘 보고 있습니다. 하지만 Access Path에 관해서는 큰 구멍이 있습니다.


필자 : ? 무슨 말인가요?


개발자 : 분석함수는 Access Path를 변경시킵니다. 즉 분석함수를 어떻게 사용하느냐에 따라 실행계획의 Operation이 변경되므로 여타의 함수와는 엄연히 다릅니다. 그러므로 수석님께서 분석함수와 관련된 Access Path를 정리해 주셨으면 합니다.


필자 : …. 프로젝트도 있고, 블로그도 관리해야 하고, 책도 써야 하고, 개인적인 공부도 해야 하는데요. 시간이 나질 않습니다. (! 말을 하자마자 실수였다는 것을 알아차렸다. 블로그 관리라니……)


개발자 : 지금 하시고 계신 네 가지 이외의 일을 해달라는 게 아닙니다. 분석함수의 실행계획을 정리하셔서 블로그에 올리시면, 그게 블로그 관리 아닌가요?


필자 : …..

 

 

이번에는 공짜가 아니다

이렇게 해서 이 글을 쓰게 되었다. 해당개발자는 언제 나에게 맥주 한잔 사기 바란다. 이런 식으로 말려들어서 글을 작성한 적이 한두번이 아니다. ^^

 

분석함수의 내부

분석함수를 이용하여 답을 잘 내었으므로 그것으로 만족하는 사람들이 있다. 답을 내는 것은 표면적인 것(문법)을 이해한 것이다. 하지만 그것만 알아서는 내부적인 매커니즘을 이해할 수 없다. 즉 분석함수를 사용함에 따라 Access Path(Operation)가 어떻게 변경되는지도 알아야 모두를 아는 것이다. 분석함수의 내부적인 Operation을 모른다면 SQL 작성은 할지 몰라도, 성능을 향상시킬 수는 없다. 오늘의 목표는 분석함수의 내부를 정복함으로써 튜닝을 가능하게 하는 것이다.

 

분석함수의 실행계획은 아래와 같이 총 8가지로 예상된다. 만약, 아래의 List 이외의 것을 발견하면 이 글을 보는 독자들을 위해 알려주기 바란다. 글이 너무 길어지므로, 이번 시간에는 8개중에 위의 4개를 알아보고자 한다. 어떤 경우에 아래의 4가지 실행계획이 발생하는지 알아보자.

 

WINDOW (SORT)  

WINDOW (SORT PUSHED RANK)

WINDOW (NOSORT)

WINDOW (NOSORT STOPKEY)   <-- 이번 시간에는 여기까지

WINDOW (BUFFER)

WINDOW (BUFFER PUSHED RANK)

WINDOW (CHILD PUSHED RANK)

WINDOW (IN SQL MODEL) SORT

 

환경: Oracle 11.2.0.1

테스트를 위해 테이블과 인덱스를 만든다.

 

CREATE TABLE SALES_T NOLOGGING AS SELECT * FROM SALES;

 

CREATE INDEX IX_PROD ON SALES_T( PROD_ID );

 

1. WINDOW SORT

분석함수를 사용하면 기본적으로 SORT가 발생한다. 이때 SORT를 대체할 수 있는 적절한 인덱스가 없다면 WINDOW SORT가 발생한다. 아래의 경우가 대표적이다.

 

-인덱스와 분석함수 OVER절의 컬럼이 일치하지 않을 때 발생한다.

-분석함수를 사용하고 Full Table Scan을 사용할 때 발생한다.

 

먼저 인덱스와 OVER 절의 컬럼이 일치하지 않는 경우를 살펴보자.

 

SELECT PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID, AMOUNT_SOLD,

       ROW_NUMBER() OVER(ORDER BY CUST_ID) AS RN

  FROM SALES_T

 WHERE PROD_ID = 30;

 

-------------------------------------------------------------------------------------------

| Id  | Operation                    | Name    | A-Rows |   A-Time   | Buffers | Used-Mem |

-------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT             |         |  29282 |00:00:00.18 |     424 |          |

|   1 |  WINDOW SORT                 |         |  29282 |00:00:00.18 |     424 | 1558K (0)|

|   2 |   TABLE ACCESS BY INDEX ROWID| SALES_T |  29282 |00:00:00.09 |     424 |          |

|*  3 |    INDEX RANGE SCAN          | IX_PROD |  29282 |00:00:00.03 |      60 |          |

-------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

 

   3 - access("PROD_ID"=30)

 

 인덱스는 PROD_ID 로 구성되었지만, OVER절은 CUST_ID로 되어 서로 다르므로 실행계획의 Id 2번에 WINDOW SORT가 발생하였다.

 

이제 분석함수를 사용하면서 FTS(Full Table Scan)이 발생하는 경우를 보자.

 

SELECT PROD_ID,

       SUM(AMOUNT_SOLD) OVER() AS SUM_AVG

  FROM SALES_T

WHERE PROMO_ID = 33

ORDER BY CUST_ID;

 

---------------------------------------------------------------------------------

| Id  | Operation          | Name    | A-Rows |   A-Time   | Buffers | Used-Mem |

---------------------------------------------------------------------------------

|   0 | SELECT STATEMENT   |         |   2074 |00:00:00.04 |    4440 |          |

|   1 |  WINDOW SORT       |         |   2074 |00:00:00.04 |    4440 |  102K (0)|

|*  2 |   TABLE ACCESS FULL| SALES_T |   2074 |00:00:00.04 |    4440 |          |

---------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

 

   2 - filter("PROMO_ID"=33)

 

WHERE 절이 PROD_ID가 아니라 PROMO_ID이므로 FTS가 발생하였다. 이때 분석함수를 사용하면 WINDOW SORT가 발생된다.

이때 ORDER BY절이 있더라도 별도의 SORT ORDER BY operation이 발생하지 않는다. 분석함수가 SORT를 대신하기 때문이다.  이때 SORT의 일량은 동일하다. 즉 분석함수와 ORDER BY절이 동시에 존재한다면, SORT의 일량은 분석함수가 없고 ORDER BY만 존재하는 SQL과 동일 하다. 아래의 SQL이 이 사실을 증명한다.

 

SELECT PROD_ID, AMOUNT_SOLD

  FROM SALES_T

WHERE PROMO_ID = 33

ORDER BY CUST_ID;

 

---------------------------------------------------------------------------------

| Id  | Operation          | Name    | A-Rows |   A-Time   | Buffers | Used-Mem |

---------------------------------------------------------------------------------

|   0 | SELECT STATEMENT   |         |   2074 |00:00:00.04 |    4440 |          |

|   1 |  SORT ORDER BY     |         |   2074 |00:00:00.04 |    4440 |  102K (0)|

|*  2 |   TABLE ACCESS FULL| SALES_T |   2074 |00:00:00.04 |    4440 |          |

---------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

 

   2 - filter("PROMO_ID"=33)

 

위에서 설명된 SQL의 분석함수 + ORDER BY SORT 일량과 ORDER BY만 사용한 SORT의 일량은 102K로 동일함을 알 수 있다. 즉 분석함수와 ORDER BY를 같이 사용하던지, 아니면 ORDER BY만 사용하던지 간에 SORT의 부하는 동일하다는 이야기 이다.

 

2.WINDOW SORT PUSHED RANK

위에서 적당한 인덱스가 없을 때, 분석함수를 사용하면 WINDOW SORT가 발생한다고 하였다. 이번에는 WINDOW SORT에 대해 좀더 깊이 들어가 보자.

 

SELECT *

  FROM ( SELECT PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID, AMOUNT_SOLD,

                ROW_NUMBER() OVER(ORDER BY CUST_ID) AS RN

           FROM SALES_T

          WHERE PROD_ID = 30 )

 WHERE RN <= 10;

 

--------------------------------------------------------------------------------------------

| Id  | Operation                     | Name    | A-Rows |   A-Time   | Buffers | Used-Mem |

--------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT              |         |     10 |00:00:00.13 |     424 |          |

|*  1 |  VIEW                         |         |     10 |00:00:00.13 |     424 |          |

|*  2 |   WINDOW SORT PUSHED RANK     |         |     11 |00:00:00.13 |     424 | 2048  (0)|

|   3 |    TABLE ACCESS BY INDEX ROWID| SALES_T |  29282 |00:00:00.10 |     424 |          |

|*  4 |     INDEX RANGE SCAN          | IX_PROD |  29282 |00:00:00.03 |      60 |          |

--------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   1 - filter("RN"<=10)

   2 - filter(ROW_NUMBER() OVER ( ORDER BY "CUST_ID")<=10)

   4 - access("PROD_ID"=30)

 

이 글에서 처음 소개된 SQL과 같지만, 인라인뷰 외부에서 분석함수 컬럼을 filter 조건으로 사용하였다. 즉 상위 10건만 보자고 한 것이다. 이런 경우 WINDOW SORT PUSHED RANK operation이 발생하며 SORT의 부하는 제한된다. 다시 말해 RN <= 10 조건에 의해 전체를 SORT할 필요가 없이 10건의 배열을 만들고, 그 배열만 SORT 하면 된다. 이 글에서 최초로 설명된 SQL SORT 일량과 WINDOW SORT PUSHED RANK의 일량을 비교해보면 1558K 2K 로 천지차이이다. 분석함수 중에 순위를 구하는 것(RANK, DENSE_RANK, ROW_NUMBER)을 인라인뷰 외수에서 filter로 사용하면 적절한 인덱스가 없더라도 Sort의 부하는 최소화 된다는 것을 기억하자.

 

이런 사실은 분석함수를 사용하지 않더라도 동일하게 발생한다. 즉 위에서 실행된 SQL SORT량과 아래의 SORT이 동일하다. 아래는 분석함수를 사용하지 않고 ROWNUM을 사용하였다.

 

SELECT *

  FROM ( SELECT /*+ INDEX(T IX_PROD) */ PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID, AMOUNT_SOLD

           FROM SALES_T T

          WHERE PROD_ID = 30

          ORDER BY CUST_ID)

 WHERE ROWNUM <= 10;

 

---------------------------------------------------------------------------------------------

| Id  | Operation                      | Name    | A-Rows |   A-Time   | Buffers | Used-Mem |

---------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT               |         |     10 |00:00:00.13 |     424 |          |

|*  1 |  COUNT STOPKEY                 |         |     10 |00:00:00.13 |     424 |          |

|   2 |   VIEW                         |         |     10 |00:00:00.13 |     424 |          |

|*  3 |    SORT ORDER BY STOPKEY       |         |     10 |00:00:00.13 |     424 | 2048  (0)|

|   4 |     TABLE ACCESS BY INDEX ROWID| SALES_T |  29282 |00:00:00.09 |     424 |          |

|*  5 |      INDEX RANGE SCAN          | IX_PROD |  29282 |00:00:00.03 |      60 |          |

---------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   1 - filter(ROWNUM<=10)

   3 - filter(ROWNUM<=10)

   5 - access("PROD_ID"=30)

 

WINDOW SORT PUSHED RANK 대신에 SORT ORDER BY STOPKEY operation이 사용되었지만 하는 일과 원리는 동일하므로 SORT량도 동일하다.

 

3.WINDOW NOSORT

적절한 인덱스가 없을 때 분석함수를 사용하면 WINDOW SORT가 발생한다고 하였다. 이번에는 동일한 SQL을 실행하되 적절한 인덱스를 만들고 실행해보자.

 

CREATE INDEX PK_SALES_T ON SALES_T(PROD_ID, CUST_ID, CHANNEL_ID, TIME_ID);

 

SELECT /*+ INDEX(T PK_SALES_T) */ PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID, AMOUNT_SOLD,

       ROW_NUMBER() OVER(ORDER BY CUST_ID) AS RN

  FROM SALES_T T

 WHERE PROD_ID = 30;

 

----------------------------------------------------------------------------------------------

| Id  | Operation                    | Name       | A-Rows |   A-Time   | Buffers | Used-Mem |

----------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT             |            |  29282 |00:00:00.34 |   28352 |          |

|   1 |  WINDOW NOSORT             |            |  29282 |00:00:00.34 |   28352 |          |

|   2 |   TABLE ACCESS BY INDEX ROWID| SALES_T    |  29282 |00:00:00.24 |   28352 |          |

|*  3 |    INDEX RANGE SCAN          | PK_SALES_T |  29282 |00:00:00.07 |     133 |          |

----------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

 

   3 - access("PROD_ID"=30)

 

WHERE절과 OVER절의 ORDER BY에 만족하는 인덱스를 생성하였더니 SORT가 전혀 발생하지 않는다. 또한 OPERATIONWINDOW SORT에서 WINDOW NOSORT로 바뀌었다. 적절한 인덱스가 왜 필요한지 알 수 있는 대목이다. 참고로 OVER 절의 ORDER BY 구문은 PROD_ID , CUST_ID 로 바꾸어도 동일한 결과를 얻을 수 있다.

 

또 다른 경우를 보자. 분석함수를 사용하고, Full Table Scan을 사용하더라도 WINDOW NOSORT가 발생할 수 있다.

 

SELECT PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID, AMOUNT_SOLD,

       ROW_NUMBER() OVER(ORDER BY NULL) AS RN

  FROM SALES_T ;

 

---------------------------------------------------------------------------------

| Id  | Operation          | Name    | A-Rows |   A-Time   | Buffers | Used-Mem |

---------------------------------------------------------------------------------

|   0 | SELECT STATEMENT   |         |    918K|00:00:04.81 |    4898 |          |

|   1 |  WINDOW NOSORT   |         |    918K|00:00:04.81 |    4898 |          |

|   2 |   TABLE ACCESS FULL| SALES_T |    918K|00:00:01.99 |    4898 |          |

---------------------------------------------------------------------------------

 

FTS를 사용했지만 분석함수에 ORDER BY절이 없으므로 SORT는 발생하지 않는다. 무작위로 10건을 추출하는 경우에 사용할 수 있다. 물론 ROWNUM을 사용해도 동일한 효과를 낼 것이다.

 

4.WINDOW NOSORT STOPKEY

인덱스의 컬럼순서와 분석함수 OVER절에 존재하는 ORDER BY 컬럼순서가 동일하며, Ranking Family 함수를 인라인뷰 외부에서 filter 조건으로 사용할 때 발생한다.

 

SELECT *

  FROM ( SELECT PROD_ID, CUST_ID, TIME_ID, CHANNEL_ID, AMOUNT_SOLD,

                ROW_NUMBER() OVER(ORDER BY PROD_ID) AS RN

           FROM SALES_T

          WHERE PROD_ID = 30 )

 WHERE RN <= 10;

 

--------------------------------------------------------------------------------------------

| Id  | Operation                     | Name    | A-Rows |   A-Time   | Buffers | Used-Mem |

--------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT              |         |     10 |00:00:00.01 |       6 |          |

|*  1 |  VIEW                         |         |     10 |00:00:00.01 |       6 |          |

|*  2 |   WINDOW NOSORT STOPKEY    |         |     10 |00:00:00.01 |       6 |          |

|   3 |    TABLE ACCESS BY INDEX ROWID| SALES_T |     11 |00:00:00.01 |       6 |          |

|*  4 |     INDEX RANGE SCAN          | IX_PROD |     11 |00:00:00.01 |       4 |          |

--------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

 

   1 - filter("RN"<=10)

   2 - filter(ROW_NUMBER() OVER ( ORDER BY "PROD_ID")<=10)

   4 - access("PROD_ID"=30)

 

WINDOW NOSORT STOPKEY는 부분범위처리에 최적화되어 있다

분석함수를 Rownum처럼 사용하였기 때문에 WINDOW NOSORT STOPKEY가 발생하였다. 1건을 더 읽었지만 비효율은 느낄 수 없는 정도이다. WINDOW NOSORT STOPKEY와 이미 위에서 설명된 WINDOW SORT PUSHED RANK와의 차이는 단순히 SORT의 유무뿐만이 아니다. Block I/O량도 차이가 난다. WINDOW SORT PUSHED RANK는 전체 건을 읽은 후에 다 버리고 10건만 출력한다. 이에 반해 WINDOW NOSORT STOPKEY 11건을 읽고 한 건만 버리므로 부분범위처리의 효율이 좋다. 2번과 4번의 Block I/O량을 비교해보기 바란다. 몇 십 배나 차이가 난다..    

 

참고:  Ranking Family 분석함수란 RANK, DENSE_RANK, ROW_NUMBER를 말한다.

 

결론


WINDOW (SORT) :

-FTS를 사용하거나 인덱스가 적절하지 않은 경우 발생 

WINDOW (SORT PUSHED RANK) :

-위와 같으나 Ranking Family를 사용해야 하며, 인라인뷰 외부에서 분석함수를 filter로 사용하는 경우 발생

WINDOW (NOSORT) :

-인덱스가 적절한 경우나, OVER절에 ORDER BY가 없는 Ranking Family를 사용한 경우 발생

WINDOW (NOSORT STOPKEY) :

-위와 같으나 Ranking Family를 사용해야 하며, 인라인뷰 외부에서 분석함수를 filter로 사용하는 경우 발생

 

실행계획만 보고도 SQL을 어느 정도 짐작 할 수 있다라고 주장하는 튜닝의 고수가 있다. 필자는 때에 따라 그럴 수도, 아닐 수도 있다고 생각한다. 하지만 최소한 분석함수에 관해서는 어느 정도 가능하다고 생각한다. 분석함수의 내부를 이해했다면 충분히 그럴 수 있다. 이제 위의 결론을 이용하여, 실행계획만 보고도 어떤 분석함수를 어떤 상황에서 사용한 것인지 짐작 할 수 있겠는가? 대충 짐작 할 수 있다면 성공한 것이다.

 

다음 글(2 )에서 소개될 내용

WINDOW (BUFFER)

WINDOW (BUFFER PUSHED RANK)

WINDOW (CHILD PUSHED RANK)

WINDOW (IN SQL MODEL) SORT

Posted by extremedb
,

-대표적인 페이징 처리방법

-누적집계가 필요할 때 페이징(부분범위) 처리방법

-Pagination의 단점을 이용하는 방법

 

주의사항

이 글에서 사용되는 분석함수는 현재 row 까지의 누적집계(Cumulative total) 이다. 이와 반대로 전체집계(Grand Total)나 그룹집계(Sub total)는 부분범위처리를 할 수 없다. 왜냐하면 데이터를 모두 읽어야만 결과를 낼 수 있기 때문이다. 하지만 누적집계는 데이터가 sort 되어 있고, 이미 출력된 컬럼들의 값을 알 수 있다면 부분범위처리가 가능하다. 우리는 이점을 이용할 것이다.

 

상황

Time Out이 발생하여 개발자가 종이 한 장을 들고 급하게 뛰어왔다.

 

개발자: 페이징 처리를 했고, 최적의 인덱스도 존재하고, 그 인덱스를 잘 타는데도 Time Out이 발생합니다.

필자  : 그럴 리가요?

개발자: SQL입니다. 한번 봐주세요.

필자  : ….분석함수 때문에 전체 건을 읽고, 전체 건을 sort하는 군요. 페이징 처리방법을 약간 변경하면 됩니다.

개발자: 이 방법은 SQL 작성 가이드에 나온 방법입니다. 이 방법을 쓰지 않으면 사수에게 혼납니다.

필자  : 이 방법을 사용하지 말라는 이야기가 아니라, 분석함수의 위치만 옮기라는 이야기 입니다.

개발자: 그렇군요. 감사합니다.

 

이렇게 해서 장애상황은 없어졌다. 이후에 SQL 작성가이드에 페이징 처리시 누적집계가 있는 경우의 처리방법을 추가하였다고 한다.

 

Pagination SQL

개발자가 사용한 페이징 처리용 SQL은 아래와 같았다.

 

SELECT *

  FROM (SELECT a.*, ROWNUM rnum

          FROM (         ) a --> 여기에 order by 가 포함된 SQL 을 넣는다.

         WHERE ROWNUM <= :v_max_row )

 WHERE rnum >= :v_min_row ;

        

 

인라인뷰 a SQL을 넣기만 하면 페이징 처리가 된다. 물론 조회시 정렬이 필요하다면 order by가 포함된 SQL을 넣어야 한다. 이 방법은 토마스 카이트가 제시하였다. 이 기법은 약간의 비효율이 있다. 첫 페이지에서는 최적이지만, 뒤쪽 페이지를 읽을 때는 이전 페이지의 데이터를 모두 scan 해야 한다.(화면에 출력되지는 않는다.) 하지만 경험적으로 볼 때 비효율이 크지 않다. 왜냐하면 우리가 구글이나 네이버로 검색을 할 때 통상적으로 앞쪽의 몇 페이지만 보고 검색을 끝내기 때문이다. 만약 네이버에서 트위터라는 단어로 검색을 했더니 5729 페이지가 나왔다고 치면, 대부분 첫 페이지 혹은 두 번째, 세 번째 페이지에서 찾고자 하는 정보를 볼 수 있을 것이다. 5729 페이지를 모두 넘겨본 사람은 거의 없을 것이다. (만약 있다면 존경스럽다.) 따라서 위의 방법을 사용한다고 해도 성능저하는 거의 발생하지 않는다.

 

그런데 인라인뷰 a에 포함될 SQL에 누적집계용 분석함수가 포함될 때는 위의 방법에 약간의 변형을 가해야 한다. 그렇지 않고 위의 방법을 그대로 사용하면 심각한 성능저하가 발생할 수 있다. 즉 분석함수가 존재한다면 위의 방법은 무늬만 페이징 처리가 되며 실제로는 전체범위를 처리하여 Time Out이 발생 할 수 있다. 이 글에서는 누적집계용 분석함수가 있는 경우에 기존방법의 문제점을 제시하고 효과인 페이징 처리방법에 대해 논의 한다.

 

테스트를 위해 테이블과 인덱스를 생성한다.

 

CREATE TABLE sales_t AS SELECT * FROM sales;

 

CREATE INDEX ix_prod ON sales_t (prod_id);

 

먼저 인라인뷰 a 에 들어갈 SQL을 보자.

 

SQL1

 

SELECT   /*+ INDEX(S IX_PROD) */

         s.prod_id, s.cust_id,  s.channel_id, s.time_id, amount_sold,

         SUM (amount_sold) OVER (PARTITION BY s.cust_id ORDER BY s.channel_id, s.time_id) AS sum_amt

    FROM sales_t s

   WHERE s.prod_id = :v_prod_id  --> 30 대입

ORDER BY s.cust_id, s.channel_id, s.time_id ;

 

 

----------------------------------------------------------------------------------------------------

| Id  | Operation                    | Name    | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

----------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT             |         |      1 |  29282 |00:00:00.12 |     424 |          |

|   1 |  WINDOW SORT                 |         |      1 |  29282 |00:00:00.12 |     424 | 1621K (0)|

|   2 |   TABLE ACCESS BY INDEX ROWID| SALES_T |      1 |  29282 |00:00:00.10 |     424 |          |

|*  3 |    INDEX RANGE SCAN          | IX_PROD |      1 |  29282 |00:00:00.03 |      60 |          |

----------------------------------------------------------------------------------------------------

 

고객별로 channel_idtime_id sort하여 누적합계를 구하는 SQL이다. 위의 SQL은 페이징 처리(부분범위 처리)가 되지 않은 것이다. 따라서 29282건이 결과로 출력되었고 424 블럭을 Scan 하였다. WINDOW SORT라는 operation이 존재하는 이유는 분석함수 때문이다. SQL order by가 있지만 별도의 SORT ORDER BY operation이 존재하지 않는다. 그 이유는 WINDOW SORT order by가 할 일을 대신해 주고 있기 때문이다. WINDOW SORT operation 때문에 PGA 1621K만큼 사용하였다.

 

이제 페이징 처리를 해보자. 먼저 차이를 보여주기 위하여 분석함수를 제거하고 페이징 처리를 하였다.

 

SQL2

 

SELECT *

  FROM (SELECT a.*, ROWNUM rnum

          FROM (SELECT   /*+ INDEX(S IX_PROD) */

                         s.prod_id, s.cust_id,  s.channel_id, s.time_id, amount_sold

                    FROM sales_t s

                   WHERE s.prod_id = :v_prod_id  --> 30 대입

                ORDER BY s.cust_id, s.channel_id, s.time_id

               ) a

         WHERE ROWNUM <= :v_max_row --> 20 대입

       )

 WHERE rnum >= :v_min_row ;         --> 1 대입

 

-------------------------------------------------------------------------------------------------------

| Id  | Operation                       | Name    | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

-------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT                |         |      1 |     20 |00:00:00.02 |     424 |          |

|*  1 |  VIEW                           |         |      1 |     20 |00:00:00.02 |     424 |          |

|*  2 |   COUNT STOPKEY                 |         |      1 |     20 |00:00:00.02 |     424 |          |

|   3 |    VIEW                         |         |      1 |     20 |00:00:00.02 |     424 |          |

|*  4 |     SORT ORDER BY STOPKEY       |         |      1 |     20 |00:00:00.02 |     424 | 2048  (0)|

|   5 |      TABLE ACCESS BY INDEX ROWID| SALES_T |      1 |  29282 |00:00:00.13 |     424 |          |

|*  6 |       INDEX RANGE SCAN          | IX_PROD |      1 |  29282 |00:00:00.03 |      60 |          |

-------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   1 - filter("RNUM">=:V_MIN_ROW)

   2 - filter(ROWNUM<=:V_MAX_ROW)

   4 - filter(ROWNUM<=:V_MAX_ROW)

   6 - access("S"."PROD_ID"=:V_PROD_ID)

 

페이징 처리를 하였음에도 똑같이 전체 블록인 424 블럭을 scan 하였다. 그 이유는 전체 건을 읽어서 정렬작업을 해야 하기 때문이다. 반면에 PGA의 사용은 2048에 불과하다. 왜냐하면 부분범위를 처리할 때는 전체 건을 sort하는 것이 아니라, 20 row 짜리 배열을 만들고 그 배열만 관리하면 되기 때문이다. 자세한 내용은 관련 을 참조하라.

 

이제 분석함수를 추가해 보자.

 

SELECT *

  FROM (SELECT a.*, ROWNUM rnum

          FROM (SELECT   /*+ INDEX(S IX_PROD) */

                         s.prod_id, s.cust_id,  s.channel_id, s.time_id, amount_sold,

                         SUM(amount_sold) OVER (PARTITION BY s.cust_id ORDER BY s.channel_id, s.time_id) AS sum_amt

                    FROM sales_t s

                   WHERE s.prod_id = :v_prod_id  --> 30 대입

                ORDER BY s.cust_id, s.channel_id, s.time_id

               ) a

         WHERE ROWNUM <= :v_max_row --> 20 대입

       )

 WHERE rnum >= :v_min_row ;         --> 1 대입

 

-------------------------------------------------------------------------------------------------------

| Id  | Operation                       | Name    | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

-------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT                |         |      1 |     20 |00:00:00.03 |     424 |          |

|*  1 |  VIEW                           |         |      1 |     20 |00:00:00.03 |     424 |          |

|*  2 |   COUNT STOPKEY                 |         |      1 |     20 |00:00:00.03 |     424 |          |

|   3 |    VIEW                         |         |      1 |     20 |00:00:00.03 |     424 |          |

|   4 |     WINDOW SORT                 |         |      1 |     20 |00:00:00.03 |     424 | 1621K (0)|

|   5 |      TABLE ACCESS BY INDEX ROWID| SALES_T |      1 |  29282 |00:00:00.15 |     424 |          |

|*  6 |       INDEX RANGE SCAN          | IX_PROD |      1 |  29282 |00:00:00.03 |      60 |          |

-------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   1 - filter("RNUM">=:V_MIN_ROW)

   2 - filter(ROWNUM<=:V_MAX_ROW)

   6 - access("S"."PROD_ID"=:V_PROD_ID)

 

성능저하의 원인은 분석함수

분석함수를 사용하자 PGA사용량이 급격히 늘었다. 분석함수가 없는 경우와 비교해보면 무려 791배나 차이가 난다. SQL1 PGA 사용량과 위 실행계획의 PGA 사용량을 비교해 보면 분석함수의 PGA 사용량은 페이징 처리를 하지 않았을 때와 똑같다. 즉 페이징 처리를 하였지만 분석함수의 영향으로 전체범위 처리가 되어버린 것이다. 바로 이점이 페이징 처리를 하였음에도 Time-Out이 발생하는 이유였다. 어떻게 하면 비효율을 제거할 수 있을까? 아래의 SQL이 정답이다.

 

SELECT *

  FROM (SELECT s.*, ROWNUM rnum,

               SUM (amount_sold) OVER (PARTITION BY s.cust_id ORDER BY s.channel_id, s.time_id) AS sum_amt

          FROM (SELECT   /*+ INDEX(S IX_PROD) */

                         s.prod_id, s.cust_id,  s.channel_id, s.time_id, amount_sold

                    FROM sales_t s

                   WHERE s.prod_id = :v_prod_id  --> 30 대입

                ORDER BY s.cust_id, s.channel_id, s.time_id

               ) s

         WHERE ROWNUM <= :v_max_row --> 20 대입

       )

 WHERE rnum >= :v_min_row ;         --> 1 대입

 

 

--------------------------------------------------------------------------------------------------------

| Id  | Operation                        | Name    | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

--------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT                 |         |      1 |     20 |00:00:00.02 |     424 |          |

|*  1 |  VIEW                            |         |      1 |     20 |00:00:00.02 |     424 |          |

|   2 |   WINDOW BUFFER                  |         |      1 |     20 |00:00:00.02 |     424 | 2048  (0)|

|*  3 |    COUNT STOPKEY                 |         |      1 |     20 |00:00:00.02 |     424 |          |

|   4 |     VIEW                         |         |      1 |     20 |00:00:00.02 |     424 |          |

|*  5 |      SORT ORDER BY STOPKEY       |         |      1 |     20 |00:00:00.02 |     424 | 2048  (0)|

|   6 |       TABLE ACCESS BY INDEX ROWID| SALES_T |      1 |  29282 |00:00:00.14 |     424 |          |

|*  7 |        INDEX RANGE SCAN          | IX_PROD |      1 |  29282 |00:00:00.04 |      60 |          |

--------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   1 - filter("RNUM">=:V_MIN_ROW)

   3 - filter(ROWNUM<=:V_MAX_ROW)

   5 - filter(ROWNUM<=:V_MAX_ROW)

   7 - access("S"."PROD_ID"=:V_PROD_ID)

 

분석함수는 인라인뷰 밖으로 빼라

분석함수를 뷰의 외부로 위치를 바꾸자 PGA를 거의 사용하지 않는다. 분석함수가 추가되었음에도 PGA 사용량이 분석함수를 사용하지 않은 경우(SQL2)와 비슷하다. 그 이유는 20건에 대해서만 분석함수가 실행되었기 때문이다. ID 2번에서 사용한 PGA SORT를 위한 것이 아니다. 왜냐하면 이미 인라인뷰 내에서 SORT가 되었으므로 같은 작업을 반복할 필요가 없기 때문이다. 이런 경우는 order by절의 컬럼과 분석함수 OVER절의 컬럼이 일치한 경우만 나타난다. 이에 따라 OperationWINDOW SORT가 아니라 WINDOW BUFFER로 바뀌었다. 20 row로 구성된 배열만 관리하면 된다. Order by 작업 또한 전체 건을 sort하지 않고 페이징 처리된 20건에 대해서 배열만 관리한 것이다.

 

절반의 성공

위의 실행계획이 best 인가 하면 그렇지는 않다. 왜냐하면 페이징 처리가 되지 않은 SQL1의 실행계획을 보면 29282건을 모두 읽었고, 페이징 처리가 된 위의 SQL 또한 마찬가지 이다. 다시 말해 위의 SQL은 결과적으로 20건만 출력되므로 비효율적인 전체범위를 처리한 것이다. PGA 사용(Sort)의 관점에서는 부분범위 처리가 되었지만 Block I/O의 관점에서는 전체범위를 처리하고 말았다.

 

이제 Block I/O 문제를 해결하기 위해 인덱스를 생성해보자.


CREATE UNIQUE INDEX PK_SALES_T ON SALES_T(PROD_ID, CUST_ID, CHANNEL_ID, TIME_ID);


이제 위의 인덱스를 이용하여 페이징 처리되지 않은 SQL을 실행해 보자.

 

SELECT /*+ INDEX(S PK_SALES_T) */

       s.prod_id, s.cust_id,  s.channel_id, s.time_id, amount_sold,

       SUM (amount_sold) OVER (PARTITION BY s.cust_id ORDER BY s.channel_id, s.time_id) AS sum_amt

  FROM sales_t s

 WHERE s.prod_id = :v_prod_id  --> 30 대입

 ORDER BY s.cust_id, s.channel_id, s.time_id ;

 

-------------------------------------------------------------------------------------------------------

| Id  | Operation                    | Name       | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

-------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT             |            |      1 |  29282 |00:00:00.11 |   28337 |          |

|   1 |  WINDOW BUFFER               |            |      1 |  29282 |00:00:00.11 |   28337 | 1495K (0)|

|   2 |   TABLE ACCESS BY INDEX ROWID| SALES_T    |      1 |  29282 |00:00:00.12 |   28337 |          |

|*  3 |    INDEX RANGE SCAN          | PK_SALES_T |      1 |  29282 |00:00:00.03 |     118 |          |

-------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   3 - access("S"."PROD_ID"=:V_PROD_ID)

 

28337 블록을 scan 하였고 PGA 1495K나 사용하였다. WINDOW BUFFER operation을 본다면 전체 건을 sort한 것은 아니다. 하지만 배열(WINDOW)의 크기가 20건이 아니라 29282건이나 되므로 전체 건을 sort한 경우와 PGA 사용량이 비슷해져 버렸다. 전체 건을 sort SQL1 PGA 사용량이 1621K 이므로 비슷하다고 할 수 있다.

 

페이징 처리를 해도...

이런 현상은 페이징 처리를 해도 분석함수를 인라인뷰 외부로 이동하지 않으면 마찬가지로 발생한다. 아래의 SQL을 보자.

 

SELECT *

  FROM (SELECT a.*, ROWNUM rnum

          FROM (SELECT   /*+ INDEX(S PK_SALES_T) */

                         s.prod_id, s.cust_id,  s.channel_id, s.time_id, amount_sold,

                         SUM (amount_sold) OVER (PARTITION BY s.cust_id ORDER BY s.channel_id, s.time_id) AS sum_amt

                    FROM sales_t s

                   WHERE s.prod_id = :v_prod_id  --> 30 대입

                ORDER BY s.cust_id, s.channel_id, s.time_id

               ) a

         WHERE ROWNUM <= :v_max_row --> 20 대입

       )

 WHERE rnum >= :v_min_row ;         --> 1 대입

 

----------------------------------------------------------------------------------------------------------

| Id  | Operation                       | Name       | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

----------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT                |            |      1 |     20 |00:00:00.04 |   28337 |          |

|*  1 |  VIEW                           |            |      1 |     20 |00:00:00.04 |   28337 |          |

|*  2 |   COUNT STOPKEY                 |            |      1 |     20 |00:00:00.04 |   28337 |          |

|   3 |    VIEW                         |            |      1 |     20 |00:00:00.04 |   28337 |          |

|   4 |     WINDOW BUFFER               |            |      1 |     20 |00:00:00.04 |   28337 | 1495K (0)|

|   5 |      TABLE ACCESS BY INDEX ROWID| SALES_T    |      1 |  29282 |00:00:00.13 |   28337 |          |

|*  6 |       INDEX RANGE SCAN          | PK_SALES_T |      1 |  29282 |00:00:00.03 |     118 |          |

----------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   1 - filter("RNUM">=:V_MIN_ROW)

   2 - filter(ROWNUM<=:V_MAX_ROW)

   6 - access("S"."PROD_ID"=:V_PROD_ID)

 

부분범위 처리가 아니라 전체범위 처리이다

많은 이들이 착각하는 것이 위의 SQL이다. 다시 말해 “order by와 분석함수의 over절에 최적화된 인덱스를 생성하면 부분처리가 되겠지라고 생각한다. 하지만 사실은 이와 다르다. 인덱스의 영향으로 Plan상에 sort order by window sort operation이 없으므로 부분범위 처리가 된 것으로 판단하면 안 된다. 20건을 읽은 것이 아니라 전체 건인 29282건을 읽었으며 PGA 사용량도 전체 건을 sort했던 경우(SQL1)와 비슷하다.

 

이런 상황에서도 해결방법은 분석함수를 밖으로 빼는 것이다. 아래의 SQL을 보자.

 

SELECT *

  FROM (SELECT s.*, ROWNUM rnum,

               SUM (amount_sold) OVER (PARTITION BY s.cust_id ORDER BY s.channel_id, s.time_id) AS sum_amt

          FROM (SELECT   /*+ INDEX(S PK_SALES_T) */

                         s.prod_id, s.cust_id,  s.channel_id, s.time_id, amount_sold

                    FROM sales_t s

                   WHERE s.prod_id = :v_prod_id  --> 30 대입

                ORDER BY s.cust_id, s.channel_id, s.time_id

               ) s

         WHERE ROWNUM <= :v_max_row --> 20 대입

       )

 WHERE rnum >= :v_min_row ;         --> 1 대입

 

 

----------------------------------------------------------------------------------------------------------

| Id  | Operation                       | Name       | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |

----------------------------------------------------------------------------------------------------------

|   0 | SELECT STATEMENT                |            |      1 |     20 |00:00:00.01 |      23 |          |

|*  1 |  VIEW                           |            |      1 |     20 |00:00:00.01 |      23 |          |

|   2 |   WINDOW BUFFER                 |            |      1 |     20 |00:00:00.01 |      23 | 2048  (0)|

|*  3 |    COUNT STOPKEY                |            |      1 |     20 |00:00:00.01 |      23 |          |

|   4 |     VIEW                        |            |      1 |     20 |00:00:00.01 |      23 |          |

|   5 |      TABLE ACCESS BY INDEX ROWID| SALES_T    |      1 |     20 |00:00:00.01 |      23 |          |

|*  6 |       INDEX RANGE SCAN          | PK_SALES_T |      1 |     20 |00:00:00.01 |       3 |          |

----------------------------------------------------------------------------------------------------------

 

Predicate Information (identified by operation id):

---------------------------------------------------

   1 - filter("RNUM">=:V_MIN_ROW)

   3 - filter(ROWNUM<=:V_MAX_ROW)

   6 - access("S"."PROD_ID"=:V_PROD_ID)

 

정확히 20건에 대해서만 WINDOW BUFFER operation 이 발생하였다. 이에 따라 PGA 사용량도 최적이 되었다. 또한 Block I/O 관점에서도 최상이다. 28337 Block scan한 것이 아니라 고작 23 Block scan 하였다. 분석함수의 위치가 성능에 얼마나 큰 영향을 미치는지 알 수 있는 장면이다.

 

결론

페이징 처리가 되었음에도 Time Out이 발생한다면 누적집계용 분석함수를 의심해보아야 한다. 만약 분석함수가 존재한다면 인라인뷰 밖으로 빼야 한다. 그렇게 한다면 분석함수의 실행이 최소화되며 이에 따라 성능이 향상된다. 또한 order by와 분석함수에 최적화된 인덱스를 만든다면 전체 건을 읽지 않아도 되며 sort의 부하 또한 없어질 것이다. 다시 말해 비효율이 없는 페이징 처리가 가능하다.

 

원리는 따로 있다

이 글의 결론까지 보았음에도 한가지 의문점을 떠올리지 못한다면 핵심원리를 놓친 것이다. 의문점이란 분석함수를 인라인뷰 밖으로 빼도 답이 달라지지 않는가?” 이다. 분석함수를 인라인뷰 밖으로 빼는 방법이 가능한 이유가 뭐라고 생각하는가? 답을 보기 전에 잠시 이유를 생각해보기 바란다. 답은 아래에 있다.

 

답을 보려면 아래의 글을 마우스로 드래그 하시오

 

이 글의 처음에 언급했던 페이징 처리시 약간의 비효율 있다고 했는데 이것이 원리이다. Tomas Kyte가 제시한 pagination 방법을 사용하면 뒤쪽 페이지를 읽을 때는 이전 페이지의 데이터를 모두 scan 해야 만 한다. 이 비효율을 이용하는 것이 핵심이다. 왜냐하면 한 페이지의 누적집계를 구하려면 이전 페이지의 값들을 모두 알아야 하기 때문이다. 예를 들어 홍길동 고객의 실적이 1 페이지와 2 페이지에 걸쳐서 나온다고 할 때, 1 페이지 있는 홍길동의 실적과 2페이지에 있는 홍길동의 실적을 더해야만 2 페이지의 누적집계를 구할 수 있다. 그런데 위의 방법을 사용하면 분석함수를 인라인뷰 밖으로 빼더라도 이전 페이지의 값을 보존하기 때문에 누적집계의 값은 정확하다.

 

 페이징 처리시 누적집계용 분석함수를 인라인뷰 밖으로 빼라고 누군가에게 guide할 때 단점(비효율)을 장점으로 이용했음을 같이 알려주기 바란다. 그것이 원리이자 핵심이기 때문이다.

 

PS

즐거운 성탄절을 보내시기 바랍니다.

지난 1년간 이 블로그를 이용해 주셔서 감사합니다.


'Oracle > SQL Tuning' 카테고리의 다른 글

Sort 부하를 좌우하는 두 가지 원리  (11) 2011.03.29
SQL튜닝 방법론  (20) 2011.01.27
오라클의 Update문은 적절한가?  (15) 2010.04.14
Connect By VS ANSI SQL  (7) 2010.02.11
USE_CONCAT 힌트 제대로 알기  (5) 2009.07.17
Posted by extremedb
,

필자는 가끔 분석함수의 기능에 관해 질문을 받는다. 그때마다 대답을 하지 않고 대신에 질문에 부합하는 Analytic SQL을 보여주고 결과를 분석하게 한다. 바로 답을 주게 되면 개발자가 의존적이 되고 분석함수 각각의 기능도 금방 잊어버리기 때문이다.

 

개발자만 모르는 것이 아니다

얼마 전에 어느 DBA로 부터 요청이 왔다. 자신을 포함한 개발자들이 분석함수를 어려워하니 블로그에 분석함수의 모든 것을 정리한 문서를 올려달라는 것이었다. 물론 오라클 매뉴얼이나 Tomas Kyte Expert One on One 등의 책에 이 함수들의 기능이 있지만 페이지 수가 너무 많고 영문이라는 단점이 있다는 것이었다. 이것은 놀라운 일이다. 개발자뿐 아니라 DBA, 심지어 컨설턴트까지 Analytic Function에 관해 필자에게 질문을 던지곤 한다. Oracle8i 부터 기능이 구현되었으니 기간으로 따지자면 10년 이상 존재했던 함수인데 아직까지......

 

도대체 Analytic Function이 얼마나 어려우면 전문가 까지도 개념이 서지 않는 걸까? 고민 끝에 핵심만 정리한 문서를 올리기로 했다.

 

핵심은 3가지다

분석함수의 기능이 복잡한 것 같지만 사실은 3가지만 알면 90%를 이해한 것이다.

1) Over 절에서 사용하는 Order by의 기능

2) Over 절에서 사용하는 Partition by의 기능

3) Over 절에서 사용하는 Windowing 기능

 

이것이 90% 이다. 대부분의 개발자와 DBA들은 1)번과 2)번에 대해서 많이 알고 있지만 이상하게도 3)번에 대해서 개념이 서질 않는다고 하였다. 따라서 아래의 문서를 다운받아서 공부할 때 3)번을 집중적으로 보기 바란다.

 

그럼 나머지 10%?

나머지는 아래와 같다. 위의 3가지를 안다면 아래의 함수들은 쉽게 이해할 수 있다. 그저 종류가 많을 뿐이다.

 

      RANK, DENSE_RANK, and ROW_NUMBER --> 3가지 함수의 차이점

      FIRST/LAST

      NTILE, WIDTH_BUCKET, CUME_DIST and PERCENT_RANK

      Hypothetical Functions

      FIRST_VALUE/LAST_VALUE , LAG/LEAD

      Reporting Functions/RATIO_TO_REPORT

      Handling null

 

빨강색 부분은 개발자들이 많이 질문하는 것들이다. 참고하기 바란다.

 

Paper라고 다 같은 것은 아니다

매뉴얼이나 관련서적의 문제점은 페이지 수가 많다는 것이다. 예를 들어 분석함수 부분이 60페이지가 넘어간다면 기능을 익히는데 며칠 혹은 몇 주가 걸릴 수 있다. 필자는 페이지 수가 많은 것을 아주 싫어한다. 아래의 문서는 앞쪽의 목차와 중요성, 그리고 뒤쪽의 마무리 부분을 제외하면 9(18 페이지)으로 모든 기능과 개념을 설명하였다. 아마 한 두 시간 이내에 다 볼 수 있을 것이다.

 

invalid-file

테이블 생성 파일

invalid-file

Mastering Oracle Analytic Function ppt 파일



PS

분석함수를 문법이라고 치부해 버리는 사람들이 있다. 그렇지 않다. 많은 경우에 분석함수를 쓰는 것이 SQL 튜닝이 된다. 오죽하면 옵티마이져가 평범한 SQL을 분석함수를 사용하는 것으로 바꾸겠는가?
이제부터 필자에게 분석함수를 질문하는 개발자가 없기를 바란다. ^^


Posted by extremedb
,

  대부분의 TOP SQL 은 ROWNUM 을 이용하여 부분범위처리 하는 개념임을 알것이다.
하지만 RANK 등의 분석함수를 이용하여 TOP 처리를 하는경우가 많이 있다.
이런경우에 대부분의 개발자및 DBA 들이 범하는 오류가 있으므로 오늘은 그부분을 바로잡고자 한다.
환경은 Oracle 10.2.0.4 이며 모든 예제는 오라클의 SH 스키마의 SALES 테이블을 이용한다.

들어가며
  ROWNUM 을 이용한 TOP SQL과 분석함수를 이용한 것의 다른점이 무엇인가?.
분석함수를 이용하면 1등이 여러명일때도 처리가 가능 하지만 ROWNUM 을 이용할경우 단한건만 가져오므로 처리가 불가능하다는 것이다.  아래는 그러한 경우에 분석함수를 하여 간단하게 해결한 경우이다.

SELECT /*+ gather_plan_statistics */
       *
  FROM (SELECT cust_id, prod_id, time_id, amount_sold,
               RANK () OVER (ORDER BY amount_sold DESC) top_sales
          FROM sh.sales a)
 WHERE top_sales <= :V_RANK;   --> 숫자 1 대입


먼저 매출액기준으로 순위를 만든후에 인라인뷰의 밖에서 FILTER 하는 형태의 SQL 이다.   
금액기준으로 가장 많이 팔린 상품과 해당고객 그리고 해당매출일자 등을 SELECT 하고 있다.

----------------------------------------------------------------------------------------------
| Id  | Operation                | Name  | Starts | A-Rows |   A-Time   | Buffers | Used-Mem |
----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT         |       |      1 |    108 |00:00:01.09 |    1718 |          |
|*  1 |  VIEW                    |       |      1 |    108 |00:00:01.09 |    1718 |          |
|*  2 |   WINDOW SORT PUSHED RANK|       |      1 |    109 |00:00:01.09 |    1718 |   45M (0)|
|   3 |    PARTITION RANGE ALL   |       |      1 |    918K|00:00:00.01 |    1718 |          |
|   4 |     TABLE ACCESS FULL    | SALES |     28 |    918K|00:00:00.01 |    1718 |          |
----------------------------------------------------------------------------------------------
 
A-Rows 항목을 보면 1등이 108 건임을 알수 있다.
 
Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter("TOP_SALES"<=:V_RANK)
   2 - filter(RANK() OVER ( ORDER BY INTERNAL_FUNCTION("AMOUNT_SOLD") DESC )<=:V_RANK)
 
울며 겨자먹기
  약 91만건을 full scan 하였으며 일량(Buffers)과 메모리 사용량(Used-Mem)이 엄청나다.
분명히 문제가 있다.
분석함수로 TOP SQL 을 구현할경우 이와 관련된 일반적인 3가지 미신(오류)이 있다.

첫번째 오류로는 일반적으로 금액컬럼에 인덱스를 만들지 않는다는 것이다.

두번째 오류로는 금액컬럼에  인덱스를 만들어도 분석함수를 사용하였으므로 인덱스를 타지 않는다는 것이다.
(참고로 분석함수로는 Function Based Index를 만들수 없다.)

세번째 오류가 가장 심각하다.
인덱스를 사용하지 못할 바에야 전체건을 sort 해야하므로 울며 겨자먹기로 순위컬럼을 모델에 추가하고 그컬럼에 인덱스를 생성하며 데이터가 바뀔때마다 실시간으로 순위컬럼에 UPDATE 하는방식을 많이 사용하고 있다.
이런것을 추출속성이라고 하는데 정합성을 해칠수 있으므로 물리모델링시에 최소화 시켜야 한다.  
그렇다면 이러한 문제를 어떻게 풀수 있을까?

먼저 AMOUNT_SOLD에 인덱스를 하나 만든다.

CREATE INDEX SH.SALES_IDX01 ON SH.SALES
(AMOUNT_SOLD);

SELECT /*+ gather_plan_statistics FIRST_ROWS(1) */
       *
  FROM (SELECT prod_id, amount_sold,
               RANK () OVER (ORDER BY amount_sold DESC) top_sales
          FROM sh.sales a)
 WHERE top_sales <= :V_RANK;  --> 숫자 1 대입

-----------------------------------------------------------------------------------------------------
| Id  | Operation                            | Name        | Starts | A-Rows |   A-Time   | Buffers |
----------------------------------------------------------------------------------------------------|
|   0 | SELECT STATEMENT                     |             |      1 |    108 |00:00:00.01 |      11 |
|*  1 |  VIEW                                |             |      1 |    108 |00:00:00.01 |      11 |
|*  2 |   WINDOW NOSORT STOPKEY              |             |      1 |    108 |00:00:00.01 |      11 |
|   3 |    TABLE ACCESS BY GLOBAL INDEX ROWID| SALES       |      1 |    109 |00:00:00.01 |      11 |
|   4 |     INDEX FULL SCAN DESCENDING       | SALES_IDX01 |      1 |    109 |00:00:00.01 |       4 |
-----------------------------------------------------------------------------------------------------
 
 
Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter("TOP_SALES"<=:V_RANK)
   2 - filter(RANK() OVER ( ORDER BY INTERNAL_FUNCTION("AMOUNT_SOLD") DESC )<=:V_RANK)
 
 
해결책은 인덱스 생성
  금액 컬럼으로 인덱스를 하나 만들었을 뿐인데 성능과 정합성이라는 두가지 이슈를 모조리 해결 하였다.
일량이(Buffers ) 170 배정도 차이가 나며 메모리 사용은 하지도 않는다.
분석함수 사용시 인라인뷰의 바깥에서 filtering 시의 비밀은 3가지로 요약할수 있다.

1.해당컬럼 컬럼에 인덱스가 있을경우 rownum 과 같이 Stop Key Operation 이 발생 한다는 것이다.

2.Predicate Information 을 보면 뷰를 만들기 전에 filter 하는 것을 볼수 있다.(id 기준으로 2번)
  이때 인덱스를 사용하는 것이다.

3.id 기준으로 2번 Operation 을 보면 WINDOW NOSORT ~ 가 있다.
이것은 인덱스가 이미 정렬이 되어있기 때문에 Sort 가 필요없다는 뜻이다.

마치 Sort Group By 시에 해당컬럼이 인덱스로 생성되어 있을경우 NOSORT 가 나오는 원리와 같다.  
하지만 인덱스가 없으면 Full Table Scan + filter 만 일어난다. 이때 WINDOW SORT PUSHED RANK Operation 이 발생하게 된다.(가장 위의 인덱스가 없을때의 plan 을 참조 할것)


ROWNUM VS RANK

  WINDOW NOSORT STOPKEY 사용시 PLAN 을 보면 1건 더 SCAN했음을 알수 있다.
즉 1위그룹은 108 건  이지만 109 건을 scan 하였고 WINDOW FILTER 과정에서 1건이 제거되었다.
하지만 ROWNUM 을 이용한다면 어떻게 될까?
아래는 ROWNUM 으로 108 건(1위그룹의 전체건수)을 가져오는 예제이다.

SELECT /*+ GATHER_PLAN_STATISTICS FIRST_ROWS(1) */ *
  FROM (SELECT prod_id, amount_sold
          FROM sh.sales a
         ORDER BY amount_sold DESC )
WHERE ROWNUM <= :V_RANK --> 108 대입;        


-----------------------------------------------------------------------------------------------------
| Id  | Operation                            | Name        | Starts | A-Rows |   A-Time   | Buffers |
-----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                     |             |      1 |    108 |00:00:00.01 |      10 |
|*  1 |  COUNT STOPKEY                       |             |      1 |    108 |00:00:00.01 |      10 |
|   2 |   VIEW                               |             |      1 |    108 |00:00:00.01 |      10 |
|   3 |    TABLE ACCESS BY GLOBAL INDEX ROWID| SALES       |      1 |    108 |00:00:00.01 |      10 |
|   4 |     INDEX FULL SCAN DESCENDING       | SALES_IDX01 |      1 |    108 |00:00:00.01 |       4 |
-----------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter(ROWNUM<:V_RANK)
  
정확히 108 건만 SCAN 했음을 알수 있다.
이것이 ROWNUM 과의 차이이다. ROWNUM 은 정확히 건수를 잘라내지만 WINDOW STOP KEY 는 한건을 더읽어야만 한다.
하지만 이정도(1건)의 비효율은 눈감아 줄수 있다. 하지만 아래의 경우는 비효율이 심하므로 반드시 피해야 한다.

SELECT /*+ gather_plan_statistics FIRST_ROWS(1) */
       *
  FROM (SELECT prod_id, amount_sold,
               RANK () OVER (ORDER BY amount_sold DESC) top_sales
          FROM sh.sales a)
 WHERE top_sales < :V_RANK;  --> 숫자 2 대입


-----------------------------------------------------------------------------------------------------
| Id  | Operation                            | Name        | Starts | A-Rows |   A-Time   | Buffers |
-----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                     |             |      1 |    108 |00:00:00.01 |      13 |
|*  1 |  VIEW                                |             |      1 |    108 |00:00:00.01 |      13 |
|*  2 |   WINDOW NOSORT STOPKEY              |             |      1 |    115 |00:00:00.01 |      13 |
|   3 |    TABLE ACCESS BY GLOBAL INDEX ROWID| SALES       |      1 |    116 |00:00:00.01 |      13 |
|   4 |     INDEX FULL SCAN DESCENDING       | SALES_IDX01 |      1 |    116 |00:00:00.01 |       4 |
-----------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter("TOP_SALES"<:V_RANK)
   2 - filter(RANK() OVER ( ORDER BY INTERNAL_FUNCTION("AMOUNT_SOLD") DESC )<:V_RANK)

RANK <= 1과 RANK < 2 는 결과는 같다. 하지만 성능은?
  WHERE 절에서 = 을 제거하고 < 로 바꾸었으므로 :V_RANK 변수에 2 를 대입 해야 같은결과를 넏을수 있다.
결과는 <= 를 사용한 것과 같지만 일량(Buffers)이 11 에서 13 으로 늘었다.
이것또한 분석함수를 이용한 TOP SQL 처리시 기억해야할 특징중의 하나인데 :V_RANK 변수에 2를 대입 하면 2위그룹까지 SCAN 한다.(정확히 말하면 3위그룹의 첫번째 ROW 까지 SCAN 한다.)
따라서 WINDOW STOPKEY 사용시에 = 를 생략하면 안된다.
 
결론:
여러 사이트에서 분석함수에 관한 몇가지 오해 때문에 성능이 나빠짐은 물론이고 정합성이 위배될수도 있는 일들이 벌어지고 있다.
두가지만 기억하자.
1.분석함수로 TOP SQL 처리시 반드시 인덱스를 만들것. (빠른성능을 원한다면)
2.RANKING 비교시 <= 를 사용할것.

편집후기 : 주의사항이 있으므로 반드시 댓글을 참고할것.

Posted by extremedb
,
필자는 며칠전 고객에게 아래와 같은 문의 사항을 받았다.

고객 :"우리지점은 9월 달에 매출액이 112 억 에 전체 지점중에서 9등을 하였습니다.
          만약 9월 달에 우리지점이 매출 130억을 달성 했다면 전체지점에서 몇등인지
          볼수 있는 기능을 오라클 에서 제공 합니까?"

필자 : "제공합니다. "
         "현재는 어떻게 개발하시고 계시나요?"

고객 : "현재는 개발자가 월별 매출액 테이블에 한건(130억)을 insert 하고 COMMIT 되지 않은 상태에서
          RANK 함수를 써서 순위를 구하고 나머지 지점들의 순위는 화면단 로직으로 다걸러서 버리고
          해당지점의 순위가 화면에 출력되면 ROLLBACK 하고 있습니다."

필자 : "켁"(마시던 커피가 넘어가다가 목에서 막히는 소리임.^^)  

필자는 고객에게 Hypothetical Functions(가정에 근거한 함수) 를 추천 하였다.
이상하게도 개발자들 뿐아니라 DBA들 또한 Ranking Family (Rank/Dense Rank/Percent rank 등등) 함수는
즐겨사용함 에도 불구하고 가정함수(Hypothetical Functions) 는 쓰지 않는다.

아래는 2001년에 고객이 매출한 금액별로 순위를 정한 SQL 문이다.

SELECT cust_nbr, SUM(tot_sales) cust_sales,
  RANK( ) OVER (ORDER BY SUM(tot_sales) DESC) rank,
  DENSE_RANK( ) OVER (ORDER BY SUM(tot_sales) DESC) dense_rank,
  CUME_DIST( ) OVER (ORDER BY SUM(tot_sales) DESC) cume_dist,
  PERCENT_RANK( ) OVER (ORDER BY SUM(tot_sales) DESC) percent_rank
FROM orders
WHERE year = 2001
GROUP BY cust_nbr
ORDER BY rank;

  CUST_NBR CUST_SALES       RANK DENSE_RANK  CUME_DIST PERCENT_RANK
---------- ---------- ---------- ---------- ---------- ------------
.....................................................      중간생략
         1    1151162         23         21 .766666667    .75862069
         8    1141638         24         22         .8   .793103448
        16    1068467         25         23 .833333333   .827586207
        22    1036146         26         24 .866666667   .862068966
       
21    1020541         27         25         .9   .896551724
       
28     986964         28         26 .933333333   .931034483
         7     971585         29         27 .966666667   .965517241
        29     903383         30         28          1            1

분석함수의 전형적인 결과이다.
위쿼리의 결과를 보고 오라클에게 이렇게 질문할수 있다.
만약 내가 백만원(1000000 원)의 매출(CUST_SALES)을 올렸다면 나의 순위(RANK)는 몇등인가?
위결과에서 밑줄친 부분을 보고 생각을 해보면 나의 순위는 28위임을 알수 있다.
즉 백만원이 28등 이라는것만 알면되는 요구사항이다. --> 결과가 한줄로 나와야 한다.
하지만 이것을 SQL 로 구현하기란 만만치 않다는걸 알수 있다.
이럴때 아래처럼 Hypothetical Functions을 사용한다면 손쉽게 불필요한 고객들의 정보를 제외하면서
보고싶은 나의 순위만 화면에 출력된다.

SELECT
  RANK(1000000) WITHIN GROUP (ORDER BY SUM(tot_sales) DESC) hyp_rank,
  DENSE_RANK(1000000) WITHIN GROUP (ORDER BY SUM(tot_sales) DESC) hyp_dense_rank,
  CUME_DIST(1000000) WITHIN GROUP (ORDER BY SUM(tot_sales) DESC) hyp_cume_dist,
  PERCENT_RANK(1000000) WITHIN GROUP (ORDER BY SUM(tot_sales) DESC) hyp_pcent_rank
FROM orders
WHERE year = 2001
GROUP BY cust_nbr;


  HYP_RANK HYP_DENSE_RANK HYP_CUME_DIST   HYP_PCENT_RANK
---------- -------------- ------------- ----------------
        28             26    .903225806               .9

결과가 너무나 환상적이지 않은가?
원하는 결과 한줄만 나오는 것은 물론이고 순위는 28위(Dense Rank 로는 26위) 라고 친절하게 나온다.

결론:
위의 상황과 같이 순위나 백분율 등을 예측하고 싶을때 Hypothetical Functions 을 쓰지 않는다면
고생을 하는것은 물론이고 성능이 좋을리도 없음을 기억하자.

편집후기 : 참고로 필자는 Hypothetical Function 이라는 말을 싫어한다.
                What IF Function 이라고 기억해야 머리에 오래남을 것이다.

'Oracle > SQL Pattern' 카테고리의 다른 글

Upgrade Select For Update  (3) 2009.05.19
응용력 발휘하기  (2) 2009.03.20
Hierarchical Queries  (3) 2008.06.23
가로를 세로로 바꾸기  (0) 2008.05.27
Jonathan Lewis 의 퀴즈 접근법  (0) 2008.05.15
Posted by extremedb
,