-변경이력에서 full table scan을 동반하는 대용량 배치의 성능관점
이 글을 이해하기 위해 이전 글들을 먼저 읽기 바란다. 

 

이전 글의 요약

첫 번째 글두 번째 글에서 변경이력에 종료일자를 추가하는 것이 성능상 유리하다는 네 가지 주장이 사실과 다름을 증명해 보았다. 즉 시작일자만으로도 종료일자+시작일자 인덱스와 같은 성능을 발휘하며, 때에 따라서는 시작일자 인덱스가 더 빠르기까지 하다. 종료일자를 추가해야 한다는 네 가지 주장을 정리하면 다음과 같다.

 

1. 비교적 최근 데이터를 구할 때는 종료일자 + 시작일자가 빠르다. 그러므로 종료일자를 추가해야 한다.
2.
특정 시점의 데이터를 보기 위해서는 종료일자 + 시작일자 인덱스를 이용하여 BETWEEN을 쓰면 되므로 시작일자만 사용하는 것에 비해 빠르다
.
3. max
값을 구할 때 종료일자에 = '99991231' 만 주면 되므로 시작일자만 사용하는 것에 비해 빠르다.

4. SQL의 결과가 한 건이 아니라 여러 건인 경우 rownum = 1 조건을 사용할 수 없으므로 역정규화를 하여 종료일자를 추가하는 것이 성능상 유리하다.

 

이 네 가지 주장이 사실이 아님을 증명하였는데, 이 과정에서 독자들이 두 가지 오해를 할 수 있으므로 이를 밝히고자 한다.

 

첫 번째, max값을 구하기 위해 인라인뷰 내부에서 order by를 사용하고 인라인뷰 밖에서 rownum = 1을 사용할 때 결코 Sort가 발생하지 않는다. 따라서 Sort area도 소모하지 않는다. Sort를 하는 경우는 인덱스(고객번호 + 시작일자)가 존재하는 않는 경우뿐이다. 즉 아래의 SQL은 인덱스를 사용하므로 추가적인 Sort를 발생시키지 않는다. 다만 인덱스를 Drop하는 실수나 장애상황에서 답이 틀려지지 않게 조치된 것뿐이다.

 

SELECT *

FROM (SELECT /*+ INDEX_DESC(a 인덱스명) */ *

FROM test1 a

WHERE cust_no = 5

ORDER BY a.start_dt DESC)

WHERE ROWNUM = 1 ;

 

두 번째, "SQL의 결과가 여러 건일 때(주장 4번의 반박에 해당함) 테이블을 중복해서 사용해야 하므로 불리하다. 또한 이력테이블을 두 번 Scan하지 않으려면 type을 써야 하는데 이는 불편하다." 라는 두 가지 이유를 들어 사용할 수 없다고 주장하였다. (이메일로 의견을 받았음) 하지만 이 또한 인덱스가 있다면 테이블을 두 번 Scan 하지 않는다. 아래의 SQL을 보라.

 

SELECT /*+ use_nl(a b c d) */ a.svc_no, a.cust_no, a.acct_no, a.svc_date,

a.txt, b.start_dt, b.txt, c.start_dt, c.txt, d.start_dt, d.txt

  FROM (SELECT a.*,

               (SELECT b.rowid||c.rowid||d.rowid as rid

                  FROM ( SELECT * FROM svc_hist  b ORDER BY start_dt DESC) b,

                       ( SELECT * FROM cust_hist c ORDER BY start_dt DESC) c,

                       ( SELECT * FROM acct_hist d ORDER BY start_dt DESC) d   

                 WHERE b.svc_no    = a.svc_no

                   AND c.cust_no   = a.cust_no

                   AND d.acct_no   = a.acct_no

                   AND b.start_dt <= a.svc_date

                   AND c.start_dt <= a.svc_date

                   AND d.start_dt <= a.svc_date

                   AND ROWNUM = 1) AS rid

          FROM svc a

         WHERE a.svc_date BETWEEN SYSDATE - 201 AND SYSDATE - 1 ) a,

       svc_hist b, cust_hist C, acct_hist D 

 WHERE SUBSTR(A.RID, 1,                     LENGTH(A.RID)/3) = B.ROWID

   AND SUBSTR(A.RID, LENGTH(A.RID)/3 + 1,   LENGTH(A.RID)/3) = C.ROWID

   AND SUBSTR(A.RID, 2*LENGTH(A.RID)/3 + 1, LENGTH(A.RID)/3) = D.ROWID ;

 

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

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

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

|   0 | SELECT STATEMENT                 |              |      1 |    200 |00:00:00.03 |    1998 |

|   1 |  NESTED LOOPS                    |              |      1 |    200 |00:00:00.03 |    1998 |

|*  2 |   COUNT STOPKEY                  |              |    200 |    200 |00:00:00.01 |    1403 |

|   3 |    NESTED LOOPS                  |              |    200 |    200 |00:00:00.01 |    1403 |

|   4 |     NESTED LOOPS                 |              |    200 |    200 |00:00:00.01 |     801 |

|   5 |      VIEW                        |              |    200 |    200 |00:00:00.01 |     401 |

|*  6 |       INDEX RANGE SCAN DESCENDING| PK_CUST_HIST |    200 |    200 |00:00:00.01 |     401 |

|   7 |      VIEW                        |              |    200 |    200 |00:00:00.01 |     400 |

|*  8 |       INDEX RANGE SCAN DESCENDING| PK_ACCT_HIST |    200 |    200 |00:00:00.01 |     400 |

|   9 |     VIEW                         |              |    200 |    200 |00:00:00.01 |     602 |

|* 10 |      INDEX RANGE SCAN DESCENDING | PK_SVC_HIST  |    200 |    200 |00:00:00.01 |     602 |

|  11 |   NESTED LOOPS                   |              |      1 |    200 |00:00:00.03 |    1798 |

|  12 |    NESTED LOOPS                  |              |      1 |    200 |00:00:00.02 |    1605 |

|  13 |     VIEW                         |              |      1 |    200 |00:00:00.02 |    1410 |

|* 14 |      FILTER                      |              |      1 |    200 |00:00:00.01 |       7 |

|  15 |       TABLE ACCESS BY INDEX ROWID| SVC          |      1 |    200 |00:00:00.01 |       7 |

|* 16 |        INDEX RANGE SCAN          | IX_SVC_01    |      1 |    200 |00:00:00.01 |       4 |

|  17 |     TABLE ACCESS BY USER ROWID   | CUST_HIST    |    200 |    200 |00:00:00.01 |     195 |

|  18 |    TABLE ACCESS BY USER ROWID    | ACCT_HIST    |    200 |    200 |00:00:00.01 |     193 |

|  19 |   TABLE ACCESS BY USER ROWID     | SVC_HIST     |    200 |    200 |00:00:00.01 |     200 |

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

 

Predicate Information (identified by operation id):

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

   2 - filter(ROWNUM=1)

   6 - access("C"."CUST_NO"=:B1 AND "C"."START_DT"<=:B2)

   8 - access("D"."ACCT_NO"=:B1 AND "D"."START_DT"<=:B2)

  10 - access("B"."SVC_NO"=:B1 AND "B"."START_DT"<=:B2)

  14 - filter(SYSDATE@!-201<=SYSDATE@!-1)

  16 - access("A"."SVC_DATE">=SYSDATE@!-201 AND "A"."SVC_DATE"<=SYSDATE@!-1)

 

실행계획을 보면 인덱스 PK_SVC_HIST와 테이블 SVC_HIST를 각각 한번씩 만 Scan한다. rowid를 사용했기 때문이다. SVC_HIST 테이블 이외의 나머지 변경이력도 마찬가지이다. 물론 스칼라 서브쿼리에 추가적인 filter 조건이 있다면 테이블을 두 번 Scan 하게 된다. 하지만 이때에도 인덱스를 추가하면 테이블을 두 번 Scan하지 않는다. 예를 들어 고객변경이력에 col1 > ‘1’ 이라는 조건이 추가되었다면 고객번호 + 시작일자 + col1 인덱스를 추가하면 된다. Source 테이블의 값이 매우 자주 변경되어 이력 테이블이 insert에 의한 부하가 심하다면 새로운 인덱스를 추가하는 것은 부담이 될 것이다. 그럴 때는 Type을 사용하면 된다. Type을 사용하기 어렵다면 그냥 테이블을 두 번 Scan 해도 큰 무리가 없다.

이전 글의 예제를 본다면 테이블을 두 번씩 Scan해도 0.02초 혹은 0.03초의 성능을 보장한다. 다시 말하면 테이블을 두 번 Scan 하였음에도 한번만 Scan하는 경우(위의 예제에서 0.03)와 비교해보면 속도차이는 미미하다. 왜냐하면 스칼라 서브쿼리에서 먼저 읽었던 테이블의 블럭은 대부분 buffer cache에 올라가 있으므로 인라인뷰 외부에서 다시 한번 읽을 때는 매우 가볍다. 이것은 "Scan한 블럭수는 차이가 나는데 Elapsed Time은 왜 동일한가요?" 에 대한 대답이다.

 

변경이력 테이블을 FTS(Full Table Scan) 하는 대용량 배치의 경우

인덱스를 사용할 수 없는 경우에 대해 알아보자. 천만 건에 해당하는 데이터와 그 데이터의 변경이력 1억건 중에 특정시점의 데이터를 구하려고 할 때는 인덱스를 사용할 수 없다. 이때에는 변경이력에 FTS를 사용해야 한다. 이 경우에 종료일자를 이용하여 between 조인을 사용하는 것과 시작일자 인덱스만 사용하는 것의 성능을 비교해보자. 실습을 할 사람들은 환경을 만들기 위해 아래 첨부파일을 다운 받기 바란다. 필자는 2010 11 28일을 사용하였으나 실습을 진행할 사람들은 일자가 달라지므로 sysdate – 4 를 사용하기 바란다. 이제 테스트를 진행해보자. 노트북에서 테스트를 진행할 때 건수가 많아 느려짐으로 테스트를 진행 할 수 없었다. 따라서 노트북이 아닌 개발 DB에서 테스트를 진행 하였다.


invalid-file

종료일자가 관리되는 테이블과 시작일자만 존재라는 테이블을 따로 생성함


 

테이블의 건수

svc : 2천만건 (active_yn = 1 인건은 천만 건)

svc_hist1 : 1억건

acct_hist1: 3천만건

cust_hist1: 5천만건

 

alter session set statistics_level = all;

 

CREATE TABLE TMP_HIST_END_DT NOLOGGING AS

SELECT /*+ leading(a b d c) use_hash(b d c) swap_join_inputs(d) swap_join_inputs(c) */ 

       a.*, b.start_dt as svc_st_dt, b.txt as svc_txt, c.start_dt as acct_st_dt,

       c.txt as acct_txt, d.start_dt as cust_st_dt, d.txt as cust_txt

  FROM svc a, svc_hist1 b, acct_hist1 c, cust_hist1 d

 WHERE a.active_yn = 1

   AND b.svc_no = a.svc_no

   AND d.cust_no = a.cust_no

   AND c.acct_no = a.acct_no

   AND TO_DATE('20101128', 'YYYYMMDD') BETWEEN b.start_dt AND b.end_dt

   AND TO_DATE('20101128', 'YYYYMMDD') BETWEEN c.start_dt AND c.end_dt

   AND TO_DATE('20101128', 'YYYYMMDD') BETWEEN d.start_dt AND d.end_dt ;

 

select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));

 

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

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

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

|   1 |  LOAD AS SELECT       |            |      1 |00:04:42.50 |    1651K|   1540K|  519K (0)|         |

|*  2 |   HASH JOIN           |            |   9990K|00:03:51.15 |    1421K|   1540K| 1237K (0)|         |

|*  3 |    TABLE ACCESS FULL  | ACCT_HIST1 |    500 |00:00:22.56 |     207K|    207K|          |         |

|*  4 |    HASH JOIN          |            |   9990K|00:03:18.60 |    1214K|   1333K| 1250K (0)|         |

|*  5 |     TABLE ACCESS FULL | CUST_HIST1 |    833 |00:00:34.23 |     346K|    346K|          |         |

|*  6 |     HASH JOIN         |            |   9990K|00:02:24.38 |     868K|    987K|  453M (1)|     967K|

|*  7 |      TABLE ACCESS FULL| SVC        |   9990K|00:00:09.99 |     149K|    149K|          |         |

|*  8 |      TABLE ACCESS FULL| SVC_HIST1  |     19M|00:01:08.50 |     718K|    718K|          |         |

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

 

Predicate Information (identified by operation id):

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

   2 - access("C"."ACCT_NO"="A"."ACCT_NO")

   3 - filter(("C"."END_DT">=TO_DATE(' 2010-11-28 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND "C"."START_DT"<=TO_DATE(' 2010-11-28 00:00:00', 'syyyy-mm-dd hh24:mi:ss')))

   4 - access("D"."CUST_NO"="A"."CUST_NO")

   5 - filter(("D"."END_DT">=TO_DATE(' 2010-11-28 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND "D"."START_DT"<=TO_DATE(' 2010-11-28 00:00:00', 'syyyy-mm-dd hh24:mi:ss')))

   6 - access("B"."SVC_NO"="A"."SVC_NO")

   7 - filter("A"."ACTIVE_YN"=1)

   8 - filter(("B"."START_DT"<=TO_DATE(' 2010-11-28 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND "B"."END_DT">=TO_DATE(' 2010-11-28 00:00:00', 'syyyy-mm-dd hh24:mi:ss')))

 

모든 변경이력에 FTS를 사용하였지만 Between 조인에 의해서 불필요한 Sort가 발생하지 않는다. 따라서 성능도 최적이다.

 

이제 종료일자가 없는 테이블로 테스트를 진행해 보자.

 

CREATE TABLE TMP_HIST_START_DT1 NOLOGGING AS

SELECT /*+ leading(a b d c) use_hash(b d c) swap_join_inputs(d) swap_join_inputs(c) */

       a.*, b.start_dt as svc_st_dt, b.txt as svc_txt, c.start_dt as acct_st_dt,

       c.txt as acct_txt, d.start_dt as cust_st_dt, d.txt as cust_txt

  FROM svc a,

       (SELECT b.*,

               ROW_NUMBER () OVER (PARTITION BY svc_no ORDER BY start_dt DESC) AS rnum

          FROM svc_hist b

         WHERE TO_DATE ('20101128', 'YYYYMMDD') >= start_dt ) b,

       (SELECT c.*,

               ROW_NUMBER () OVER (PARTITION BY acct_no ORDER BY start_dt DESC) AS rnum

          FROM acct_hist c

         WHERE TO_DATE ('20101128', 'YYYYMMDD') >= start_dt) c,

       (SELECT d.*,

               ROW_NUMBER () OVER (PARTITION BY cust_no ORDER BY start_dt DESC)  AS rnum

          FROM cust_hist d

         WHERE TO_DATE ('20101128', 'YYYYMMDD') >= start_dt) d

 WHERE a.active_yn = 1

   AND b.svc_no = a.svc_no

   AND d.cust_no = a.cust_no

   AND c.acct_no = a.acct_no

   AND b.rnum = 1

   AND c.rnum = 1

   AND d.rnum = 1 ;

 

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

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

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

|   1 |  LOAD AS SELECT              |           |      1 |00:10:59.63 |    1450K|   1575K|  519K (0)|         |

|*  2 |   HASH JOIN                  |           |   9990K|00:10:14.07 |    1221K|   1574K|   46M (0)|         |

|*  3 |    VIEW                      |           |    500 |00:01:24.11 |     173K|    173K|          |         |

|*  4 |     WINDOW SORT PUSHED RANK  |           |   1000 |00:01:24.11 |     173K|    173K|   97M (0)|    1024 |

|*  5 |      TABLE ACCESS FULL       | ACCT_HIST |     29M|00:00:30.00 |     173K|    173K|          |         |

|*  6 |    HASH JOIN                 |           |   9990K|00:08:39.91 |    1048K|   1401K|   47M (0)|         |

|*  7 |     VIEW                     |           |    833 |00:02:19.91 |     289K|    289K|          |         |

|*  8 |      WINDOW SORT PUSHED RANK |           |   1666 |00:02:19.91 |     289K|    289K|   97M (0)|    1024 |

|*  9 |       TABLE ACCESS FULL      | CUST_HIST |     49M|00:00:49.98 |     289K|    289K|          |         |

|* 10 |     HASH JOIN                |           |   9990K|00:05:59.96 |     758K|   1111K|  377M (1)|     947K|

|* 11 |      TABLE ACCESS FULL       | SVC       |   9990K|00:00:19.99 |     149K|    149K|          |         |

|* 12 |      VIEW                    |           |     19M|00:04:16.25 |     608K|    844K|          |         |

|* 13 |       WINDOW SORT PUSHED RANK|           |     39M|00:03:56.27 |     608K|    844K|   97M (1)|    1848K|

|* 14 |        TABLE ACCESS FULL     | SVC_HIST  |     39M|00:01:12.45 |     608K|    608K|          |         |

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

                          

Predicate Information (identified by operation id):

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

   2 - access("C"."ACCT_NO"="A"."ACCT_NO")

   3 - filter("C"."RNUM"=1)

   4 - filter(ROW_NUMBER() OVER ( PARTITION BY "ACCT_NO" ORDER BY INTERNAL_FUNCTION("START_DT") DESC )<=1)

   5 - filter("START_DT"<=TO_DATE(' 2010-11-28 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))

   6 - access("D"."CUST_NO"="A"."CUST_NO")

   7 - filter("D"."RNUM"=1)

   8 - filter(ROW_NUMBER() OVER ( PARTITION BY "CUST_NO" ORDER BY INTERNAL_FUNCTION("START_DT") DESC )<=1)

   9 - filter("START_DT"<=TO_DATE(' 2010-11-28 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))

  10 - access("B"."SVC_NO"="A"."SVC_NO")

  11 - filter("A"."ACTIVE_YN"=1)

  12 - filter("B"."RNUM"=1)

  13 - filter(ROW_NUMBER() OVER ( PARTITION BY "SVC_NO" ORDER BY INTERNAL_FUNCTION("START_DT") DESC )<=1)

  14 - filter("START_DT"<=TO_DATE(' 2010-11-28 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))

 

종료일자를 사용한 between 조인이 없으므로 전체 건을 sort 해야 한다. 따라서 부하가 상당하며 성능이 두 배 이상 저하되었다. FTS를 사용한 것은 같지만 모든 변경이력 테이블(1 8천만건) Sort해야 한다. (WINDOW SORT PUSHED RANK 부분참조) 이런 경우는 종료일자를 사용하여 between으로 처리하는 것이 확실히 빠르다. 물론 Parallel을 사용하여 degree 2혹은 3정도 준다면 해결할 수 있지만 신중해야 한다. 초를 다투는 중요한 배치인 경우만 적용해야 하며, 동 시간대 CPU 사용량과 PGA 사용량을 감안해야 한다. sort_area_size hash_area_size를 수동으로 튜닝 하는 것 또한 마찬가지 이다. Parallel을 사용하거나 수동으로 PGA를 조절하는 것은 자원을 독점하는 것이므로 다른 배치 프로그램에 악영향을 줄 수 있다.

 

이제 지난 글과 이번 글에서 나타난 특징을 표로 정리해보자.
 

비교항목

시작일자만 관리

종료일자도 관리

사용 빈도수

중요도

정합성(데이터 품질)을 보장하는가?

우수

나쁨

N/A

매우 중요

성능관점

최근 시점의 값 조회시 성능

우수

우수

90%

중요

중간 시점의 값 조회시 성능

우수

중간

9%

오래된 시점의 값 조회시 성능

우수

나쁨

1%

FTS를 동반하는 대용량 배치 성능

나쁨

우수

0.01%

보통

SQL의 복잡성

나쁨

우수

N/A

보통

추가적인 비용(노력)이 얼마나 드는가?

우수

나쁨

N/A

보통


종료일자를 사용하면 정합성을 보장하지 못한다. 성능관점에서 인덱스를 사용하는 경우(온라인 업무)는 시작일자만으로 인덱스를 사용하는 것이 유리하다. 왜냐하면 종료일자+시작일자 인덱스는 최종(max)값을 구할 때 가장 빠르지만 최종 값에서 멀어질수록 점점 성능이 떨어진다. 하지만 시작일자 인덱스는 항상 빠르다.
FTS를 동반하는 대용량 배치에서는 종료일자를 사용하여 between 조인을 하면 두 배 ~ 세 배 정도 빠르다. SQL의 복잡성 측면에서는 시작일자만 관리하는 경우는 SQL이 길어지므로 종료일자를 사용하는 것이 유리하다.
 

역정규화하여 종료일자를 사용하려면 아래와 같은 추가적인 비용이 든다.
1. Source
테이블의 데이터가 변경되면 트리거성으로 변경이력에 Update하는 프로그램을 추가로 작성해야 한다.
2.
이때 데이터가 자주 변경되는 경우는 Update 자체의 부하도 무시할 수 없다.
3.
데이터가 틀어진 경우를 대비하여 정합성을 보정하는 프로그램을 추가로 작성해야 한다.

 

정합성을 보정하는 프로그램은 상당히 복잡하다. 왜냐하면 점의 중복제거뿐만 아니라 선분의 중복도 제거해야 하기 때문이다. 예를 들어 고객변경이력이라고 한다면 점(고객번호 + 시작일시)이 중복이 되어선 안되므로 Cleansing이 필요하다. 또한 선분(고객번호 + 시작일시 + 종료일시)의 중복도 해결해야 한다. 아래의 그림을 보자.
사용자 삽입 이미지

위의 경우 구간1과 구간2의 시작점은 다르므로 점의 중복은 없다. 하지만 선분이 겹치므로 구간을 3등분해야 한다. 따라서 Insert가 추가로 발생한다. 위의 그림은 하나의 경우만 나타낸 것이다. 하지만 구간2가 왼쪽 혹은 오른쪽으로 이동되어 겹치는 구간이 달라질 수 있으므로 각각의 경우에 처리하는 SQL이 달라질 수 있다.


역정규화를 했을 때 정합성 보정 프로그램은 동시성 제어(원본소스를 변경시키는 update, 변경이력에 insert, 변경이력의 종료일자에 update)를 하여 one transaction으로 관리하더라도 필요하다. 급한 경우 프로그램을 통하지 않고 직접 DBinsert를 날릴 수 있고, 이때는 작업자가 실수 할 수 있기 때문이다. 어떠한 실수가 있더라도 정합성을 보정하는 프로그램이 있다면 데이터 품질을 유지할 수 있다.

 

결론: 습관적인 종료일자의 추가는 위험하다

결과적으로 표의 결과는 간발의 차이로 나쁨 2개인 시작일자만 관리하자는 측의 승리이다. 이제부터 역정규화를 할 때는 표의 항목을 비교해보고 많은 고민을 해야 한다. 왜냐하면 역정규화의 장점보다 단점이 더 클수 있으며 데이터의 정합성(품질)은 성능이나 개발생산성과 바꿀 수 있는 성격이 아니기 때문이다.

 

변경이력을 실시간으로 조회하는 온라인 프로그램이 많고 조회빈도수도 많으므로 성능이 중요하다. 따라서 종료일자를 사용해야 한다.” 라는 주장은 사실과 다르다. 오히려 이런 경우는 종료일자의 성능상 장점이 없으므로 시작일자만 사용하면 된다. 또한 변경이력을 full table scan하는 대용량 배치프로그램의 성능이 느리다고 무작정 종료일자를 추가해서는 안 된다. 그 배치프로그램이 종료일자를 사용하는 경우보다는 느려지겠지만, 속도가 목표시간 내에 들어온다면 느리다고 할 수 없다. 많은 경우에 배치프로그램은 늦은 저녁에 시작하여 다음날 새벽 6시까지 끝나면 된다.

 

반대로 온라인 프로그램이 아닌 대용량 배치프로그램의 성능이 매우 중요한 경우(example: 대금청구 시스템)이고 속도가 느리다면 표에 나타난 다른 항목을 희생해서라도 역정규화를 고려할 수 있다. SQL의 길이가 길어지므로 종료일자를 추가하자는 주장은 장단점을 비교하여 역정규화 할 수 있다. 예를 들어 정합성이 틀어질 위험이 있고, 역정규화에 의한 추가적인 노력(비용) 들더라도 SQL 실력이 약한 신입개발자가 과반수라면 종료일자를 고려해야 한다. 하지만 이경우에도 '아주 복잡한 정합성 보정 프로그램을 SQL 실력이 약한 신입이 개발할 수 있을까?' 라는 의문은 남는다. 쉬운 SQL을 사용하려다 보니 더욱 어려운 SQL을 만날 수 있다는 말이다.

 

PS

표를 만드는 동안 양측(시작일자만 관리 VS 종료일자도 관리)의 집중 견제를 받았다.

 

종료일자를 사용해야 한다는 측의 주장

원래는 표에 우수나쁨만 있었는데 중간이라는 것이 생겼다. 종료일자를 관리해야 한다는 측의 주장에 따라 중간시점의 조회성능은 나쁨이 아니라 중간으로 바뀌었다. 원래는 상대적으로 불리하면 나쁨이라 표시하고 유리하면 우수로 표시 했었다. 또한 조회빈도수를 추가했다. 조회빈도수를 추가하지 않으면 시작일자만 관리한다는 측이 유리해 보인다는 것 이었다. 또 다른 의견으로는 추가적인 노력(비용)이 증가하는 것은 원래는 아래의 세 개의 항목으로 나타내었다.

1. Source가 변경되면 변경이력에 update 하는 프로그램을 추가로 작성해야 한다.

2. Source가 자주 변경된다면 그 Update가 부하가 될 수 있다.

3. 역정규화에 의한 정합성 보정 프로그램을 추가로 작성해야 한다.

 

이렇게 세 항목으로 구분하여 우수나쁨으로 나타내었지만 노력(비용)이 증가하는 것하나의 항목으로 나타내 달라고 주장했다. 세 항목이 전부 나쁨으로 표시되면 불리하게 보일 수 있으므로 하나의 항목으로 나타내자는 것 이었다. 받아들였다. 마지막 주장은 조회시점 별 성능항목 세가지를 인덱스를 사용할 때의 성능항목 하나로 바꾸고 성능의 안정성(시점 별로 성능이 좌지우지 되는지)을 추가하자는 의견이 있었으나 받아들이지 않았다. 바꾸어 보아도 성능의 안정성은 나쁨이 될 것이기 때문이다.  

시작일자만 사용해도 된다는 측의 주장

시작일자만 관리하자는 측도 가만히 보고 있진 않았다. SQL의 복잡성 항목에 나쁨대신에 중간으로 바꿔달라고 했다. 이 정도면 복잡한 정도는 아니고 길이만 조금 길어진다는 것이었다. 받아들이지 않고 그냥 나쁨으로 두었다. 또한 FTS를 동반하는 대용량 배치에서 나쁨이 아니라 중간으로 하자는 주장도 만만치 않았다. 1억건 단위의 FTS와 조인 그리고 18천만건의 Sort가 고작 11분 걸렸는데 그것의 성능이 나쁜 것은 아니라는 것이었다. 배치가 매우 중요하여 초를 다투는 상황이라 하더라도 튜닝의 여지가 있으므로 중간으로 하자는 의견도 있었다. 이 두가지 의견은 받아들이지 않았다. 왜냐하면 일단 성능이 두 배 이상 느리고, 튜닝을 하자는 의견은 종료일자 + 시작일자의 단점인 오래된 데이터를 조회할 때에도 똑같이 튜닝으로 해결 할 수 있다. 위의 표는 튜닝을 하자는 관점이 아니라 장단점을 나타내는 관점이다. 마지막으로 주장한 것이 중요도 항목이다. 데이터 정합성(품질)은 성능이나 SQL 복잡성 보다 훨씬 중요하다는 것이었다. 이것은 받아들였다.


마지막으로 의견을 제시하신 양측 분들께 감사 드린다. 양측의 주장을 모두 조율하였지만 그래도 양측의 불만은 여전히 존재할 것이다. 어쩔 수 없는 일이다. 양측의 주장이 워낙 강하다 보니 이제는 블로그의 글을 내 논리대로 쓰지 못하는 시기가 온 것 같다.



Posted by extremedb
,