▶적절한 인덱스가 없을 때 Sort의 부하를 줄이는 방법

Pagination에서 Sort의 부하 줄이기

주의사항

 

Order by 절에 의한 Sort의 부하는 성능에 치명적이다. Block I/O의 최소화는 분명 튜닝의 핵심이다. 하지만, Block I/O를 최소화 해도 Sort의 부하가 심하다면 결코 만족스런 성능을 내지 못한다. 특히페이징 처리용 SQL에는 Sort를 대신할 수 있는 인덱스가 있어야 성능을 확보할 수 있다고 많은 튜너들이 주장한다. 맞는 말이다. 그렇게만 된다면 Sort가 전혀 발생하지 않을 테니까. 하지만, 다음과 같은 어려움도 있다.

 

인덱스 최적화가 힘든 이유
첫 번째, 인덱스를 모든 조회화면의 기준에 맞게 만들려면 테이블마다 많은 수의 인덱스가 필요할 것이다. 두 번째, 운영중인 환경에서 인덱스를 생성 혹은 변경하기는 매우 어렵다. 따라서, 인덱스를 만들기 어렵다면, Sort의 부하를 최소화하는 다른 방법은 없는지를 고려해야 한다. 분명히 방법은 있다. 이 방법을 알지 못한다면 오직 인덱스에만 목숨을 거는 사람이 될 가능성이 높다. 오늘은 인덱스를 전혀 만들지 않은 상태에서 Sort의 부하를 최소화 하는 방법에 대해 알아볼 것이다.

 

단 한 가지 개념만 안다면, Order By에 의한 Sort의 부하를 이해한 것이다. Sort의 부하량은 면적에 비례한다는 것. 이 개념은 아래와 같이 표현할 수 있다. 참고로 는 비례한다는 의미이다.

 

Sort의 부하량(PGA 사용량) ∝ 세로(결과 건수) X 가로(컬럼 Size 합계)

 

공식의 이해가 부족하다
주위의 지인들에게 위의 식을 질문한 결과 거의 모두가 세로에 대해서는 정확히 이해하고 있었다. , Sort할 건수가 많아지면 Sort의 부하가 증가 한다는 것이다. 이에 반해서 가로에 대해서는 정확한 이해를 하는 사람이 드물었다. 대부분, Order By절에 의해 Sort의 부하가 발생하므로 Order By절에 존재하는 컬럼 Size의 합계가 가로라고 생각하는 것이다. 다시 말해, Order By절의 컬럼이 세 개라면, 세 컬럼의 Size를 합친 것이 가로라는 것이다. 과연 그럴까?


위의 주장을 검증하기 위해 테이블을 하나 만들고, 추가적으로 컬럼을 3개 만들자.

 

CREATE TABLE SALES_T NOLOGGING AS SELECT * FROM SALES;

ALTER TABLE SALES_T ADD (char_100  CHAR(100)  DEFAULT 'a' NOT NULL );

ALTER TABLE SALES_T ADD (char_1000 CHAR(1000) DEFAULT 'a' NOT NULL );

ALTER TABLE SALES_T ADD (char_2000 CHAR(2000) DEFAULT 'a' NOT NULL );

 

추가된 컬럼은 모두 Char Type이며 Default 값이 ‘a’ 이다. Char Type이므로 Default값인 ‘a’가 들어오는 경우 컬럼 size는 각각 100, 1000, 2000 바이트씩 채워진다. 이제 이 컬럼들을 이용하여 SQL을 각각 실행해보자. 100 byte, 1000 byte, 2000 byte 컬럼으로 각각 Sort하여 Sort의 부하가 어떻게 달라지는지 알아보자.

 

CREATE TABLE SORT_100 NOLOGGING AS

SELECT /*+ full(p) full(c) */

       s.prod_id, p.prod_name, s.cust_id, c.cust_first_name,

       c.cust_last_name, s.time_id, s.channel_id, s.char_100

  FROM sales_t s, customers c, products p

 WHERE s.cust_id = c.cust_id

   AND s.prod_id = p.prod_id

   AND s.prod_id = 30

 ORDER BY s.char_100 ;

 

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

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

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

|   0 | CREATE TABLE STATEMENT |           |      1 |      0 |00:00:01.41 |    7323 |          |

|   1 |  LOAD AS SELECT        |           |      1 |      0 |00:00:01.41 |    7323 |  521K (0)|

|   2 |   SORT ORDER BY        |           |      1 |  29282 |00:00:01.23 |    5915 | 4708K (0)|

|*  3 |    HASH JOIN           |           |      1 |  29282 |00:00:01.15 |    5915 | 3471K (0)|

|   4 |     TABLE ACCESS FULL  | CUSTOMERS |      1 |  55500 |00:00:00.21 |    1468 |          |

|   5 |     NESTED LOOPS       |           |      1 |  29282 |00:00:00.66 |    4447 |          |

|*  6 |      TABLE ACCESS FULL | PRODUCTS  |      1 |      1 |00:00:00.01 |       7 |          |

|*  7 |      TABLE ACCESS FULL | SALES_T   |      1 |  29282 |00:00:00.59 |    4440 |          |

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

 

Predicate Information (identified by operation id):

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

   3 - access("S"."CUST_ID"="C"."CUST_ID")

   6 - filter("P"."PROD_ID"=30)

   7 - filter("S"."PROD_ID"=30)

 

100 byte컬럼으로 Sort하니 PGA4.7MB 사용하였다. 이제 100 byte보다 10배나 큰 1000 byte 컬럼으로 Sort 하여 PGA 사용량을 비교해보자.

 

CREATE TABLE SORT_1000 NOLOGGING AS

SELECT /*+ full(p) full(c) */

       s.prod_id, p.prod_name, s.cust_id, c.cust_first_name,

       c.cust_last_name, s.time_id, s.channel_id, s.char_1000

  FROM sales_t s, customers c, products p

 WHERE s.cust_id = c.cust_id

   AND s.prod_id = p.prod_id

   AND s.prod_id = 30

 ORDER BY s.char_1000 ;

 

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

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

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

|   0 | CREATE TABLE STATEMENT |           |      1 |      0 |00:00:02.17 |   13162 |          |

|   1 |  LOAD AS SELECT        |           |      1 |      0 |00:00:02.17 |   13162 |  521K (0)|

|   2 |   SORT ORDER BY        |           |      1 |  29282 |00:00:01.75 |    5915 |   30M (0)|

|*  3 |    HASH JOIN           |           |      1 |  29282 |00:00:01.56 |    5915 | 3486K (0)|

|   4 |     TABLE ACCESS FULL  | CUSTOMERS |      1 |  55500 |00:00:00.22 |    1468 |          |

|   5 |     NESTED LOOPS       |           |      1 |  29282 |00:00:01.05 |    4447 |          |

|*  6 |      TABLE ACCESS FULL | PRODUCTS  |      1 |      1 |00:00:00.02 |       7 |          |

|*  7 |      TABLE ACCESS FULL | SALES_T   |      1 |  29282 |00:00:00.98 |    4440 |          |

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

 

Predicate Information (identified by operation id):

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

   3 - access("S"."CUST_ID"="C"."CUST_ID")

   6 - filter("P"."PROD_ID"=30)

   7 - filter("S"."PROD_ID"=30)

 

Sort 컬럼의 size 100에서 1000 byte로 늘리자 PGA 사용량도 4.7 MB에서 30 MB로 크게 늘었다. 내친김에 Order By절을 2000 byte 컬럼으로 바꿔서 PGA 사용량이 얼마나 늘어나는지 테스트 해보자.

 

CREATE TABLE SORT_2000 NOLOGGING AS

SELECT /*+ full(p) full(c) */

       s.prod_id, p.prod_name, s.cust_id, c.cust_first_name,

       c.cust_last_name, s.time_id, s.channel_id, s.char_2000

  FROM sales_t s, customers c, products p

 WHERE s.cust_id = c.cust_id

   AND s.prod_id = p.prod_id

   AND s.prod_id = 30

 ORDER BY s.char_2000 ;

 

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

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

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

|   0 | CREATE TABLE STATEMENT |           |      1 |      0 |00:00:03.16 |   19298 |          |

|   1 |  LOAD AS SELECT        |           |      1 |      0 |00:00:03.16 |   19298 |  521K (0)|

|   2 |   SORT ORDER BY        |           |      1 |  29282 |00:00:02.06 |    5915 |   58M (0)|

|*  3 |    HASH JOIN           |           |      1 |  29282 |00:00:01.74 |    5915 | 3515K (0)|

|   4 |     TABLE ACCESS FULL  | CUSTOMERS |      1 |  55500 |00:00:00.24 |    1468 |          |

|   5 |     NESTED LOOPS       |           |      1 |  29282 |00:00:01.19 |    4447 |          |

|*  6 |      TABLE ACCESS FULL | PRODUCTS  |      1 |      1 |00:00:00.02 |       7 |          |

|*  7 |      TABLE ACCESS FULL | SALES_T   |      1 |  29282 |00:00:01.12 |    4440 |          |

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

 

Predicate Information (identified by operation id):

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

   3 - access("S"."CUST_ID"="C"."CUST_ID")

   6 - filter("P"."PROD_ID"=30)

   7 - filter("S"."PROD_ID"=30)

 

예상대로 Sort 대상 컬럼을 1000에서 2000 byte로 바꾸자 PGA 사용량이 30 MB에서 58MB 로 약 두 배 늘었다. 위의 결과를 언뜻 보면, Order By절에 존재하는 컬럼 Size의 합계가 가로라고 생각할 수 있다. 왜냐하면, Sort 대상컬럼의 Size에 비례하여 PGA 사용량이 증가되었다고 판단하기 때문이다. 하지만 이런 생각은 절반만 옳고 나머지 절반은 틀렸다. 제대로 된 식은 다음과 같다.

 

Sort의 부하를 좌우하는 원리

Sort
의 부하량(PGA 사용량) ∝ 세로 X 가로

세로: SQL의 결과 건수

가로: Order by 절의 컬럼 size + Order by 절을 제외한 나머지 컬럼의 size

 

근거 있는 주장인가?
이 공식이 글 전체의 핵심이다. 하지만, 많은 사람들이 위와 같은 가로 세로 개념을 주장할 수 있는 근거가 무엇인지 궁금해한다. 이제 가로가 Order by 절의 컬럼 size + 나머지 컬럼의 size라는 주장에 대한 근거를 보자.

 

SELECT s.channel_id, s.char_2000

  FROM sales_t s

 WHERE s.prod_id = 30

 ORDER BY s.channel_id;

 

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

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

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

|   0 | SELECT STATEMENT   |         |      1 |  29282 |00:00:00.84 |    4440 |          |

|   1 |  SORT ORDER BY     |         |      1 |  29282 |00:00:00.84 |    4440 |   56M (0)|

|*  2 |   TABLE ACCESS FULL| SALES_T |      1 |  29282 |00:00:00.68 |    4440 |          |

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

 

Predicate Information (identified by operation id):

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

   2 - filter("S"."PROD_ID"=30)

 

Order By절에는 Size가 작은 channel_id 컬럼 뿐이다. 그런데 PGA 사용량은 56 MB나 된다. 세로가 3만 건도 안 되는 집합을 Sort하는데 그 부하는 56 MB나 된다. 이상하지 않은가? 과부하의 이유는 Select절의 char_2000 컬럼 때문이다. 이 컬럼을 Select 절에서 제거하고 다시 실행해 보자.

 

SELECT s.channel_id

  FROM sales_t s

 WHERE s.prod_id = 30

 ORDER BY s.channel_id;

 

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

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

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

|   0 | SELECT STATEMENT   |         |      1 |  29282 |00:00:00.64 |    4440 |          |

|   1 |  SORT ORDER BY     |         |      1 |  29282 |00:00:00.64 |    4440 |  424K (0)|

|*  2 |   TABLE ACCESS FULL| SALES_T |      1 |  29282 |00:00:00.58 |    4440 |          |

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

 

Predicate Information (identified by operation id):

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

   2 - filter("S"."PROD_ID"=30)

 

Order By절 이외의 컬럼에 주목하라 
Select
절의 char_2000 컬럼을 제거하자 Sort의 부하는 424K로 급격히 줄어들었다. 왜냐하면, Sort Area에는 Order By절의 컬럼을 Sort할뿐만 아니라 나머지 컬럼 List Loading 되기 때문이다. Order By절 뿐만 아니라, Select 절에도 size가 큰 컬럼이 있다면 성능이 급격히 저하됨을 알 수 있다. 지금까지 Sort부하량 공식에 의해 가로는 Order By절 컬럼 Size + Sort 대상 이외의 컬럼 Size가 됨을 증명해 보았다.

 

이제 이 개념을 실제 SQL 프로그래밍에 적용해보자. 아래는 전형적인 Pagination SQL이다. 아래의 예제에서 처음의 약속을 지키기 위해 인덱스를 만들지도, 사용하지도 않을 것이다.
 

SELECT *

  FROM (SELECT a.*, ROWNUM rnum

          FROM (SELECT s.prod_id, p.prod_name, p.prod_category_desc, s.cust_id,

c.cust_first_name, c.cust_last_name, s.time_id, s.channel_id,

                       s.char_100, s.char_1000, s.char_2000

                  FROM sales_t s, customers c, products p

                 WHERE s.cust_id = c.cust_id

                   AND s.prod_id = p.prod_id

                   AND s.channel_id = 3

                 ORDER BY c.cust_first_name, c.cust_last_name, p.prod_category_desc, s.time_id ) a

         WHERE ROWNUM <= :v_max_row ) --> 200 대입

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

 

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

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

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

|   0 | SELECT STATEMENT         |           |      1 |    200 |00:00:08.71 |    5915 |          |

|*  1 |  VIEW                    |           |      1 |    200 |00:00:08.71 |    5915 |          |

|*  2 |   COUNT STOPKEY          |           |      1 |    200 |00:00:08.71 |    5915 |          |

|   3 |    VIEW                  |           |      1 |    200 |00:00:08.71 |    5915 |          |

|*  4 |     SORT ORDER BY STOPKEY|           |      1 |    200 |00:00:08.71 |    5915 | 3321K (0)|

|*  5 |      HASH JOIN           |           |      1 |    540K|00:00:06.30 |    5915 | 1176K (0)|

|   6 |       TABLE ACCESS FULL  | PRODUCTS  |      1 |     72 |00:00:00.01 |       7 |          |

|*  7 |       HASH JOIN          |           |      1 |    540K|00:00:03.91 |    5908 | 3568K (0)|

|   8 |        TABLE ACCESS FULL | CUSTOMERS |      1 |  55500 |00:00:00.21 |    1468 |          |

|*  9 |        TABLE ACCESS FULL | SALES_T   |      1 |    540K|00:00:01.14 |    4440 |          |

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

 

Predicate Information (identified by operation id):

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

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

   2 - filter(ROWNUM<=:V_MAX_ROW)

   4 - filter(ROWNUM<=:V_MAX_ROW)

   5 - access("S"."PROD_ID"="P"."PROD_ID")

   7 - access("S"."CUST_ID"="C"."CUST_ID")

   9 - filter("S"."CHANNEL_ID"=3)

 

페이징 처리된 SQL Sort 부하량은 3321K 이다. 이제 Sort의 부하를 줄이기 위해 select 절의 모든 컬럼을 제거하자.

 

SELECT s.prod_id, p.prod_name, p.prod_category_desc, s.cust_id,

c.cust_first_name, c.cust_last_name, s.time_id, s.channel_id,

        s.char_100, s.char_1000, s.char_2000

  FROM (SELECT a.*, ROWNUM rnum

          FROM (SELECT s.rowid as s_rid, p.rowid as p_rid, c.rowid as c_rid

                  FROM sales_t s, customers c, products p

                 WHERE s.cust_id = c.cust_id

                   AND s.prod_id = p.prod_id

                   AND s.channel_id = 3

                 ORDER BY c.cust_first_name, c.cust_last_name, p.prod_category_desc, s.time_id ) a

         WHERE ROWNUM <= :v_max_row ) a,   --> 200 대입   

       sales_t s, customers c, products p

   WHERE s.rowid  = a.s_rid

   and c.rowid = a.c_rid

   and p.rowid = a.p_rid

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

 

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

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

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

|   0 | SELECT STATEMENT              |           |      1 |    200 |00:00:06.51 |    6168 |          |

|   1 |  NESTED LOOPS                 |           |      1 |    200 |00:00:06.51 |    6168 |          |

|   2 |   NESTED LOOPS                |           |      1 |    200 |00:00:06.51 |    5969 |          |

|   3 |    NESTED LOOPS               |           |      1 |    200 |00:00:06.51 |    5918 |          |

|*  4 |     VIEW                      |           |      1 |    200 |00:00:06.51 |    5915 |          |

|*  5 |      COUNT STOPKEY            |           |      1 |    200 |00:00:06.51 |    5915 |          |

|   6 |       VIEW                    |           |      1 |    200 |00:00:06.51 |    5915 |          |

|*  7 |        SORT ORDER BY STOPKEY  |           |      1 |    200 |00:00:06.51 |    5915 |96256  (0)|

|*  8 |         HASH JOIN             |           |      1 |    540K|00:00:05.81 |    5915 | 1193K (0)|

|   9 |          TABLE ACCESS FULL    | PRODUCTS  |      1 |     72 |00:00:00.01 |       7 |          |

|* 10 |          HASH JOIN            |           |      1 |    540K|00:00:03.65 |    5908 | 4514K (0)|

|  11 |           TABLE ACCESS FULL   | CUSTOMERS |      1 |  55500 |00:00:00.22 |    1468 |          |

|* 12 |           TABLE ACCESS FULL   | SALES_T   |      1 |    540K|00:00:01.06 |    4440 |          |

|  13 |     TABLE ACCESS BY USER ROWID| CUSTOMERS |    200 |    200 |00:00:00.01 |       3 |          |

|  14 |    TABLE ACCESS BY USER ROWID | PRODUCTS  |    200 |    200 |00:00:00.01 |      51 |          |

|  15 |   TABLE ACCESS BY USER ROWID  | SALES_T   |    200 |    200 |00:00:00.01 |     199 |          |

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

 

Predicate Information (identified by operation id):

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

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

   5 - filter(ROWNUM<=:V_MAX_ROW)

   7 - filter(ROWNUM<=:V_MAX_ROW)

   8 - access("S"."PROD_ID"="P"."PROD_ID")

  10 - access("S"."CUST_ID"="C"."CUST_ID")

  12 - filter("S"."CHANNEL_ID"=3)

 

 

Trade Off가 유리한 경우

Sort 부하량이 3321K에서 96K로 약 34.5배 줄어들었다. 이렇게 ROWID만 남기고 select 절의 모든 컬럼을 제거해도 결과는 같다. 왜냐하면, Sort된 상태로 rowid가 보관되어있기 때문이다. 페이징 처리가 모두 끝나고 200건에 대해서만 rowid로 테이블에 접근하기 때문에 테이블의 중복사용에 의한 비효율은 매우 적다. Buffers 항목을 비교해보면 5915 블록에서 6168 블록으로 비효율은 253 블록(4%) 밖에 차이가 나지 않는다. 하지만 Sort의 부하는 34.5배나 줄어들었다. 약간의 Block I/O를 손해 보더라도 Sort의 부하가 아주 큰 경우는 같은 블록을 중복해서 읽어야 함을 알 수 있다.

 

장점 + 장점

이렇게 Rowid를 제외한 Select List를 인라인뷰 외부로 빼면, Sort 부하의 최소화 이외에 또 다른 효과를 누릴 수도 있다. 인덱스만 읽고 테이블로의 접근을 하지 않을 수 있다. Where 조건에 최적화된 인덱스가 존재하고, 그 인덱스가 Order By절 컬럼을 포함 한다면 인라인뷰 내부에서는 테이블 접근을 하지 않는다. 물론 Select List의 모든 컬럼들을 가져오려면 테이블을 접근해야 한다. 하지만 위의 예제처럼 Rowid를 사용했다면 페이징 처리가 끝난 후에, 특정 페이지에 해당하는 건들만 테이블로 접근할 수 있으므로 Random Access 도 최소화 된다. Sort를 최소화 하려고 했더니 Block I/O를 최소화 하는것까지 덤으로 얻을 수 있는 것이다.    

 

주의사항

SELECT 절에 상수나 변수 심지어 NULL이 오더라도 PGA 사용량은 증가하므로 주의해야 한다.
 

SELECT s.cust_id

  FROM sales_t s

 ORDER BY s.cust_id;

 

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

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

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

|   0 | SELECT STATEMENT   |         |      1 |    918K|00:00:03.38 |    4440 |          |

|   1 |  SORT ORDER BY     |         |      1 |    918K|00:00:03.38 |    4440 |   13M (0)|

|   2 |   TABLE ACCESS FULL| SALES_T |      1 |    918K|00:00:01.38 |    4440 |          |

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

 

Select 절에 다른 컬럼이 없기 때문에 PGA 13MB 사용 하였다. 이번에는 Select절에 Null을 추가해보자.

 

SELECT s.cust_id, null

  FROM sales_t s

 ORDER BY s.cust_id;

 

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

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

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

|   0 | SELECT STATEMENT   |         |      1 |    918K|00:00:03.48 |    4440 |          |

|   1 |  SORT ORDER BY     |         |      1 |    918K|00:00:03.48 |    4440 |   17M (0)|

|   2 |   TABLE ACCESS FULL| SALES_T |      1 |    918K|00:00:01.37 |    4440 |          |

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

 

Select List Null을 추가하자 PGA사용량이 4MB 증가했다. 오라클은 Null이나 상수도 컬럼처럼 취급함을 알 수 있다. 따라서 Order by절이 있는 인라인뷰의 Select List에 상수나 변수 혹은 Null을 집어넣는 것은 Sort의 부하를 증가시킨다. 상수나 변수는 Order By가 있는 인라인뷰에 넣지 말고 외부로 빼서 사용하면 된다.

결론
Sort의 부하를 최소화 하려면 Order By절의 컬럼에만 집중해서는 안되며, 전체 컬럼 List를 바라보아야 한다. 또한 프로그래밍을 할 때 상수 하나, 변수 하나의 위치도 고려해야 최적의 성능을 가진 프로그램이 됨을 알 수 있다. 즉 Sort의 최적화는 튜닝의 문제일 뿐만 아니라 프로그래밍의 문제인 것이다. 이점은 Sort 부하량이 무엇으로 결정되는지 개발자도 알아야 하는 이유가 된다.

면적은 가로와 세로로 구성된다.

Sort의 부하량은 면적의 크기에 비례한다.

Sort의 부하량 ∝ 결과 건수 X 전체 컬럼 Size

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

COPY_T 테이블 필요한가?  (6) 2011.04.04
SQL튜닝 방법론  (20) 2011.01.27
Pagination과 분석함수의 위험한 조합  (26) 2010.12.23
오라클의 Update문은 적절한가?  (15) 2010.04.14
Connect By VS ANSI SQL  (7) 2010.02.11
Posted by extremedb
,

11.2.0.2에서 파티션 생성시 용량부족 현상

Partition Large Extents란 무엇인가

Deferred Segment Creation과 Partition Large Extents의 관계 


오라클 11.2.0.2 에서 황당한 일이 발생했다. DBA는 데이터가 없는 빈 테이블을 파티션으로 생성하려고 했다. 그런데 느닷없이 용량부족 에러가 발생한 것이다. 디스크용량은 10 Giga Bytes나 여유가 있으므로 이런 에러메시지를 만날 이유가 없다. 아무리 파티션이 많은 테이블이라도 기껏해야 100MB 정도의 공간만 있으면 테이블을 문제없이 생성했던 DBA로서는 이유를 알 수 없었다. 어찌되었건 문제를 해결해야 하므로 디스크를 30 Giga Bytes 추가했더니 더 이상 에러가 발생하지 않았다고 한다.

 

데이터가 한 건도 없는 파티션 테이블 하나 생성하는데 10 Giga Bytes 이상의 저장공간이 필요하다니? 이런 사실이 이상하다고 생각한다면 오라클 11.2.0.2의 새 기능을 모르고 있는 것이다. 새 기능을 모르면 원인을 파악 할 수 없고, 문제를 해결할 수도 없다. 이 글은 오라클의 새 기능이 어떻게 용량부족 에러를 발생시키는지, 그 에러를 예방하는 방법은 무엇인지 설명한다.

 

해당 테이블 설명

생성하려는 파티션 테이블은 보관기간이 10년이고, 거래일자로 월별 Range 파티션이 되어있고, 서브파티션은 Hash 파티션이 12개씩 잡혀있었다. 그 테이블을 Create 하려면 Disk가 얼마나 필요할까? dba_segments에서 해당 테이블의 용량을 조회 해보니 빈 테이블 하나가 11.52 Giga Bytes나 차지하고 있었다. 이에 따라 다음과 같이 추론 할 수 있다.

 

주 파티션( 10 x 12 개월 ) x 서브 파티션( 12 Hash ) x 8MB = 11520 MB

 

데이터도 없는데 8MB를 소모하는 이유

위의 계산식을 본다면 데이터가 한 건도 없음에도 불구하고, 오라클이 파티션 하나당 8MB를 할당했다는 결론이 나온다. 이런 현상은 11.2.0.2부터 파티션 테이블에서만 발생한다. 파티션 테이블을 만들었을 뿐, 데이터는 한 건도 없다. 그럼에도 불구하고 각 파티션 마다 저장공간을 8MB씩 낭비한 셈이다. 오라클은 왜 이런 무모한 결정을 했을까? 언제나 그렇듯이 비밀은 매뉴얼에 있는 경우가 많다. 아래의 내용이 저장공간을 낭비하는 이유이다.

 

Note:

Beginning with release 11.2.0.2 of Oracle Database, the default size of the first extent of any new segment for a partitioned table is 8 MB instead of 64 KB. This helps improve performance of inserts and queries on partitioned tables.

 

매뉴얼 참조: Estimating the Space Use of a Table

 

위의 글을 해석해보면 성능 향상을 위한 목적임을 알 수 있다. , 처음에 8MB 만큼 공간을 미리 할당하면, 추후에 INSERT가 들어올 때 공간을 다시 할당할 필요가 없으므로 속도가 좋아진다는 것. 그리고 자잘한 extent로 쪼개짐을 방지하여 쿼리의 성능을 높이자는 것이다. 이를 partition large extent 라고 한다. 이론적으로는 무모한 결정이 아님을 알 수 있다.

 

성능 때문에 용량 문제가 발생해도 좋은가?

하지만 이 개념 때문에 파티션 개수가 많은 테이블을 생성하지 못하는 문제가 생긴다. 즉 빈 공간이 90 기가 바이트라고 해도, 파티션 개수가 많은 테이블들은 몇 개도 생성하지 못한다는 결론이 나온다. 이런 기능을 성능이 빨라진다고 해서 오라클이 무조건 적용 할 수 있을까? 오라클은 무언가 믿는 구석이 있었기 때문에 자신 있게 적용할 수 있었다. 그것이 무엇일까? 11.2 매뉴얼을 읽어본 사람이라면 추측 할 수 있을 것이다.

 

빗나간 예측
Understand Deferred Segment Creation note 부분을 참조하면 오라클이 자신감 있게 파티션 마다 8MB를 할당할 수 있었던 이유가 나온다. Deferred Segment Creation(이하 DSC)이라는 기능 때문이다. DSC 기능 덕분에 테이블이 생성될 때는 물리적인 Disk 공간을 차지하지 않는다. 다시 말해, 테이블을 만들 때 공간을 할당하는 것이 아니라, 추후에 데이터가 insert 될 때 각 파티션에 대해 8MB씩 할당한다. DSC는 버전 11.2.0.2 부터 일반테이블 뿐만 아니라, 파티션 테이블에도 적용이 가능해졌다. 오라클은 파티션마다 8MB씩 할당해야 되지만 DSC 기능이 있으므로, 파티션을 생성할 때는 용량문제가 없을 것이라고 생각했을 것이다.

 

하지만, 항상 문제가 되는 것은 이론이 아니라 현실에서 발생한다. 현실에서는 여러 가지 버그 때문에 DSC를 사용할 수 없다. 따라서 해당 파라미터인 DEFERRED_SEGMENT_CREATION False로 놓을 수 밖에 없다. DSC를 사용할 수 없으므로 테이블을 만들 때 각 파티션마다 물리적인 디스크 공간을 8MB씩 차지하게 된다. 따라서, 파티션 개수가 많은 테이블을 만들 때 DSC 기능이 꺼져 있다면 용량부족 현상이 발생할 수 밖에 없다. 오라클로서도 어쩔 수 없는 일이다. 버그를 예상할 수는 없는 노릇 아닌가? DSC의 버그가 해결되면 이런 문제는 자연스럽게 사라질 것이다.

 

DSC에 관한 버그는 조동욱 님의 글 Deferred Segment Creation 재미있는 두가지 버그를 참고하기 바란다.

 

해결방법

DSC는 현재 버그가 있으므로, 기능을 사용하지 못한다. 따라서 무조건 각 파티션에 8MB씩 할당이 되므로 엄청난 Disk 공간이 필요한 것이다. 파티션 테이블을 11.2.0.2 이상에서 생성할 계획이라면 수동으로 STORAGE (INITIAL 65536) 를 주면 된다. 이렇게 하면 처음에 8MB가 아니라 64KB만 공간을 차지한다. 이렇게 설정하면 이전 버전 11.2.0.1처럼 사용하는 것이다.

 

관련 파라미터

_partition_large_extents Default 값은 True이다. True로 되어있으면 각 파티션에 64kb가 아니라 8MB가 할당된다. 이 파라미터는 버전 11.2.0.1cell_partition_large_extents 파라미터를 대체한다. _index_partition_large_extents 도 위의 파라미터와 같은 역할을 한다. 다만 파티션이 아니라 파티션 인덱스라는 점만 다르다. 두 가지 파라미터 _partition_large_extents_index_partition_large_extents 모두 11.2.0.2에서 소개되었다.

 

결론

11.2.0.2에서 버그 때문에 DSC 기능을 막아 놓았다면, 파티션 테이블을 생성할 때 STORAGE 절에 적절한 INITIAL 값을 주기 바란다. 그렇지 않으면 Disk 공간을 많이 차지하게 된다. 특히 각 파티션당 용량이 많지 않지만 관리목적상 파티션을 하는 경우는 INITIAL 값을 주는 것이 유리하다. 이렇게 하면 저장공간을 낭비하지 않고 테이블을 관리 할 수 있다. 또한 개발 DB는 운영 DB에 비해 Disk가 부족하므로 이런 사실을 잘 알아야 에러가 나더라도 당황하지 않는다. 개인적인 목적으로 PC나 노트북등에 오라클을 설치하는 경우도 Disk가 넉넉하지 않을 것이므로 마찬가지이다.

 

오라클 매뉴얼은 여러 번 보아야 한다. 예를 들어 11.2.0.1 매뉴얼을 이미 정독한 사람은 11.2.0.2 매뉴얼을 볼 필요가 없다고 생각 할 수 있다. 결코 그렇지 않다. 이 글에서 나타난 문제만 보아도 11.2.0.2 매뉴얼을 보았다면 전혀 문제가 되지 않을 것이다. 따라서 이전 매뉴얼을 정독 했더라도 11.2.0.2 버전의 새로운 기능은 익혀야 한다. 물론 매뉴얼 전체를 다시 볼 필요는 없다. 예를 들어 Admin가이드의 경우 매뉴얼의 첫 부분인 What's New in Oracle Database Administrator's Guide? 만 참조하면 무슨 기능이 추가된 것인지 쉽게 파악 할 수 있다. 어디에 무슨 내용이 있는지 안다면, 문제의 절반은 해결한 셈이다.

 

관련문서

매뉴얼: Oracle® Database Administrator's Guide 11g Release 2 (11.2)
오라클 노트: 1295484.1

'Oracle > Partition' 카테고리의 다른 글

Oracle 10g Partitioning 가이드  (12) 2009.08.05
Posted by extremedb
,

창의성을 얻는 방법의 문제점

같은 결과를 놓고 왜 해석이 다를까?

▶창의성을 얻는 방법 세 가지

 

많은 사람들이 창의성을 원하고 있다. 이에 따라 여러 가지 창의성 관련서적이 나오고 있다. 그 서적들에서 창의성을 키우는 방법으로 다음과 같은 것들을 공통적으로 이야기 한다. 아마도 새로운 것을 발견할 때 사용하는 방법인 모양이다.

틀을 깨라

반대로 생각하라

다른 관점으로 생각하라


원하는 것만 보게 된다
하지만 위의 말들을 이해하더라도 창의성을 확보할 수 없다. 왜냐하면, 사람은 보고 싶은 것만 보고, 듣고 싶은 것만 듣기 때문이다. 만약 당신도 그렇다면, 옆의 그림에서 컵만 보일 가능성이 높다.


물론 일부의 사람들은 정확한 관찰을 통해 남들이 못보는 것을 관찰 할 수 있다. 하지만 많은 사람들은 그림을 있는 그대로 보지 못한다. 왜냐하면 대부분의 창의성 서적에는 아래의 것들은 언급되지 않기 때문이다.

 

무엇의 틀을 깨라는 것인지?

무엇을 반대로 생각하라는 것인지?

무엇을 다른 관점으로 생각하라는 것인지?

 

이 글의 목적
어떻게 하라라는 말만 있고 '무엇을 하라'는 말은 없다. 다시 말해, 목적어가 빠졌다. 도대체 무엇을 하라는 것인가? 이것이 창의성과 관련된 많은 책이 실용서가 될 수 없는 이유이다. 특히 이공계에서 필요한 창의성은 거의 얻을 수 없다. 이 글에서는 무엇을 할 것인지에 관해서 논의한다.


그림의 출처
: http://kr.blog.yahoo.com/ism_kihan/34



무엇을 어떻게 할 것인지 알아야만 창의성을 확보할 수 있다. 질문을 다음처럼 바꾼다면 위의 그림에서 컵 이외의 것을 볼 수 있을 것이다.

 

지금 보고 있는 것이 컵이라는 틀을 깨라

지금 보고 있는 것이 컵이라고? 반대로 생각하라

지금 보고 있는 것이 컵이라고? 다른 관점으로 생각해보라



이렇게 생각한다면 위의 컵은 다른 것으로 보일 수 있다. 만약 여전히 컵 이외의 것을 못 보는 사람들은, 글을 계속 읽어 나가면 직관적으로 알 수 있으므로 걱정하지 말기 바란다.

본다는 것은 의미를 부여하는 것
아는 사람이 많겠지만, 위와 아래의 두 가지 그림들은 '루빈의 컵'이라는 유명한 심리학자가 설명한 것이다. 아래의 그림에서 두 사람만 보았다면 이 또한 착각이다. 다섯 명의 사람을 발견할 수 있어야 한다. 이런 착시현상으로 놀라운 사실을 얻을 수 있다. 본다는 것은 우리가 시각적으로 보는 것이 아니라, 우리가 의미를 부여하는 것이다. 다시 말해, 카메라로 사진을 찍는 것(망막으로 보는것)은 아무 의미도 없으며, 우리가 그 사진에 의미를 부여하는 순간 사실이 된다는 것이다. 바로 이점이 사람과 컴퓨터의 차이이다. 컴퓨터로 그림을 저장하거나 스캔할 수 있지만, 그 그림에 의미를 부여하지 못한다. 물론, 인지과학자들이 인공지능 분야에서 많은 노력을 하고 있지만, 아직까지 갈길이 멀다.


그림의 출처
: http://blog.daum.net/visitor/15709185


착시일 뿐인가?
혹자는 이 현상을 두고 착시현상을 유발하는 그림일 뿐이라고 이야기 할 수도 있다. 정말 그럴까? 그렇다면 다행이지만 실제로는 그렇지 않다. 예를 들면, 기원전부터 현재 21세기까지 수많은 과학자들이 있었다. 하지만, 위대하고 유명한 과학자의 수는 많지 않은 이유는 무엇일까? 그것은 착시현상이 위의 그림들뿐만 아니라 과학적인 실험과 관찰에서도 나타나기 때문이다.

실제 과학에서의 창의성

갈릴레이, 다윈, 아인슈타인, 테슬라의 공통점은 무엇일까? 위대한 과학자라는 공통점을 제외하면 창의성이 뛰어난 사람이다. 그럼 네 명의 과학자가 무슨 일을 어떻게 했는지 살펴보자.

 

갈릴레이는 천동설을 거부하고 망원경을 이용하여, 지동설을 증명하였다.

다윈은 창조론을 거부하고, 진화론을 주장하였다.

아인슈타인은 뉴턴역학의 시간과 공간은 변하지 않는다는 이론을 거부하고, 상대성이론을 주장하였다.

테슬라는 에디슨의 직류시스템에 만족하지 않고, 교류시스템을 발명하였다.

 

위의 업적을 본다면 네 사람은 기존의 이론을 더욱 발전시키거나, 변형시켜서 위대한 발견이나 발명을 한 사람들이다. 그렇다면 위의 네 사람은 어떻게 창의적인 작업을 할 수 있었을까? 그것은 바로 이론 적재성 관찰이라는 착시현상을 극복 했기 때문에 창의성을 발휘할 수 있었다.

 

이론 적재성 관찰이란 무엇인가?

이론 적재성 관찰 (theory-laden observation)이란 기존의 이론을 과신하여 이론의 틀 안에서만 실험과 관찰을 하는 것이다. 즉 실험을 할 때 개인의 주관적인 배경지식 때문에, 이론에 실험결과를 맞추어 버리는 것이다. 바로 이 문제 때문에 어떠한 실험을 하더라도, 항상 이론에 종속적인 결론이 나오게 된다. 이론 적재성 관찰을 하는 경우 심각한 오류에 빠질 수 있다. 왜냐하면, 실험을 할 때 이론과 상반되는 결과가 나오면 자신이 실험을 잘못 했다고 생각하기 때문이다. 사실은, 실험이 잘못된 것이 아니라 기발하고, 새로운 것을 발견한 경우에도, 잘못된 실험을 했다고 착각하는 것이다. 만약 위에서 언급한 네 명의 과학자가 이론의 틀이라는 색안경을 끼고 관찰을 했다면 위대한 과학자가 될 수 있었을까
 
같은 결과를 놓고, 해석이 다른 이유
관찰의 이론 적재성을 최초로 주장한 사람은 과학철학자인 핸슨이다. 이 개념으로 인해 관찰의 객관성이 과학지식의 발전에 중요한 역활을 한다고 믿고 있던 귀납주의와 반증주의에게 치명타를 남긴다. 그의 개념이 집약된 책이 Patterns of Discovery(과학적 발견의 패턴)이다. 그 책에서 예를 든 티코 브라헤와 케플러의 해돋이 장면에 관한 대화는 잘 알려져 있다: 티코 브라헤는 “태양이 떠오르는 군”이라고 말한다. 그런데, 케플러는 반대로 “지구가 내려가고 있군”이라고 말한다. 대부분의 과학자는 티코 브라헤 처럼 이론 적재성 관찰을 한다고 핸슨은 말한다. 그도 그럴것이 어느 누가 함부로 기존의 이론을 뒤집을 수 있겠는가? 위에서 언급한 네 명의 과학자는 예외에 속한다. 대담하게 이론을 거부했기 때문이다.


그림의 출처: http://nircissus.tistory.com/417

해돋이를 정확히 해석할 수 있을까?
동일한 해돋이 광경을 보았지만, 서로 다른 해석을 하는 이유는 관찰을 할 때 한 명은 이론의 틀을 벗어나지 못했고, 다른 한 명은 이론을 배제한 관찰을 했기 때문이다. 이론을 배제한 관찰을 할 때만 정확하고, 공정한 해석이 가능할 뿐만 아니라, 새로운 것을 발견할 수 있다

이론을 배제한 과학적 관찰법의 가치
관찰이 이론의 틀 안에서만 해석된다는 이 개념의 여파는 대단했다. , 의도적으로 이론을 무시하고 관찰한다면 새로운 이론을 발견할 수 있다는 것이다. 이 개념은 20세기 초반의 과학적 방법론인 귀납주의와 반증주의의 단점을 극복하였다. 귀납법과 반증법은 가설을 검증하는 방법일뿐, 가설을 발견하는 방법인 핸슨의 과학적 관찰법과는 다르다. 가설을 발견이라도 해야 검증 할 것이 아닌가? 따라서 논리실증주의자와 비판적 합리주의자가 신주 모시듯 했던 귀납법, 반증법이라는 것들은 과학적 관찰법이 선행되지 않으면 의미가 없다

20세기의 가장 위대한 저작물 중 하나인 쿤의 The Structure of Scientific Revolutions(과학 혁명의 구조)도 핸슨이 주장한 이론 적재성 관찰의 영향을 받았다. 쿤은 이론 적재성 관찰을 하는 사람을 정상과학자로 분류하고, 이론을 배제한 관찰을 하여 새로운 패러다임으로 이론을 만들고, 동료과학자의 지지를 받는 과학자를 과학 혁명가로 분류했다. 패러다임이라는 의미를 과학 혁명과 관련된 의미로 최초로 정의한 사람이 토머스 쿤이다. 과학혁명의 구조가 나오기 전 까지 패러다임의 의미는 지금과 달랐다.

귀추법은 발견의 방법
핸슨은 관찰뿐만 아니라 과학적 사실이라고 믿는 것, 원인과 결과, 이론의 생성과정에서도 여러  사례를 들어 선이론 적재성을 증명하였다. 또한 그때까지만 해도 이상적인 과학방법이라고 생각했던 가설-연역법도 가설이 어떻게 생성되는 지 설명하지 못한다고 지적하였다. 즉 과학적인 이론을 만들 때 가설부터 생성하지 않는다는 것이다. 핸슨에 의하면, 과학자는 가설이아니라 실험데이터로부터 시작하며데이터를 짜 맞추어 이해될 수 있는 개념적 패턴을 만듦으로써 가설이 생성된다 라고 이야기 한다. 즉 물리학자가 가설을 생성하는 방법은 오직 실험데이터에 대해 지적으로 들어맞는 개념적 패턴을 추구하는 것이다. 이것을 귀추법이라 하며, 현재로써는 오직 귀추법만이 실험결과에 새로운 의미를 부여하여 가설을 생성하는 방법이라고 할 수 있다.


틀을 어떻게 깰 것인가?
이제 우리는 최초에 언급한 세가지 질문에 대해 아래와 같이 답할 수 있다.

무엇의 틀을 깨라는 것인지?                  à 이론의 틀을 깨라

무엇을 반대로 생각하라는 것인지         à 이론과 반대로 생각하라

무엇을 다른 관점으로 생각하라는 것인지? à 이론과 다른 관점으로 생각하라

 

이론을 많이 알수록 관찰의 이론 적재성은 심해진다. 그럼 이론은 모를수록 좋은 것인가?

기본은 되어 있어야 한다

이렇게 글을 써 놓으니, 이론이 필요 없다고 느낄 수 있다. 결코 그렇지 않다. 정석을 모르고 좋은 바둑을 둘 수 있을까? 이론이 얼마나 중요한지는 이미 언급했던 창의성을 키우는 방법을 다시 음미해 보면 알 수 있다.

 

이론의 틀을 깨라                  à 이론의 틀이 어디까지인지 알아야 깰 것 아닌가?

이론을 반대로 생각하라          à 이론과 반대로 생각하려면 이론을 알아야 한다.

이론을 다른 방향으로 생각하라 à 이론을 알아야 그것을 비틀어 생각할 것이 아닌가?

 

독자가 속한 분야의 이론을 많이 알수록 창의성을 증가시킨다.(창조의 조건 4장 참조) 이론이 방해가 되는 이유는 오직 이론 적재성 관찰을 할 때이다. 필자도 천성적으로 창의적인 사람이 아니므로 연구를 할 때는 이론이라는 색안경을 끼지 않도록 노력하고 있다.

 

두 가지를 동시에 사용하라

이 글의 최초에 언급한 무엇을 해야 하는가가 이제 명확해 졌는가? 같은 결과를 놓고 왜 해석이 다른지 이해가 되는가? 가설이 어떻게 만들어 질 수 있는지 알았는가? 이제 여러분이 속해있는 분야에서 창의적인 결과물을 만들고 싶다면 책을 읽을 때, 사고할 때, 실험과 관찰을 할 때 1 이론을 배제하고, 2 실험 데이터에 대해 잘 설명되는 패턴을 도출하기 바란다. 관찰을 할때 이론의 틀을 버리고, 가설을 만들 때 귀추법을 사용한다면, 그 두 가지를  동시에 할 수 있다면, 새로운 것을 발견할 가능성이 훨씬 높다. 이 두가지 방법은 필자가 자주 사용하는 것이다.  

결론
나는 우리 이공계가 실력이 없어서 창의력을 발휘하지 못한다고 생각하지 않는다. 만약 다른 선진국의 이공계보다 창의성이 없다면, 그 이유는 새로운 것을 발견하지 못하기 때문이 아닐까? 해당분야의 이론을 잘 습득하고, 두 가지 방법(관찰에서 이론을 배재할 것, 데이터에 새로운 패턴을 적용하여 의미를 부여할 것) 을 활용한다면 그 들보다 못할 이유는 없다고 본다.

창의성, 독창성이라는 것은 거대하고, 위대한 것만 있는 것은 아니다. 다시 말해, 발견의 방법(창의성)을 이론과 같이 큰 것에만 적용할 것이 아니라, 작은 것에도 적용할 수 있다는 것이다. 각 분야의 이론에는 수많은 개념이 있다. 이론에 달려있는 수많은 개념에 대해서도 위에서 언급한 두 가지를 적용한다면 새로운 사실을 발견할 확률은 매우 높아진다. 그렇게 하여 각자 자기의 분야에서 조그만 것을 발견했다 하더라도, 세상이 알아주지 않더라도, 한발자국 앞으로 나아갔다면, 그것은 의미 있는 일이다.

 




참조서적



좌로부터 핸슨의 '과학적 발견의 패턴', 쿤의 '과학혁명의 구조', 테레사 M. 아마빌레의 '창조의 조건' 이다. 세권 모두 쉽게 이해되는 책은 아니다. 과학방법론(과학철학)에 대한 입문서로는 쿤 & 포퍼 (정재승)가 적당하다.


Posted by extremedb
,