-변경이력에서 여러 건을 조회 할 때의 해결방법
-변경이력 조회시 rownum의 활용
들어가기 전에
이 글을 읽기 위해서 지난번 글(변경이력 테이블에 종료일자가 필요한가?)을 먼저 읽고 오기 바란다. 그렇지 않으면 이해하기 힘들며 이 글과 어떻게 연결되는지 알 수 없다.
이전 글이 다분히 이론적이었다면 이번에는 실전적인 문제를 다룬다. 이 글에서 언급된 기법을 알지 못하면 실무에서 종료일자가 없이 시작일자만을 사용했을때 성능이 떨어질 것이다.
이 글은 종료일자의 장단점에 대해 충분한 고민후에 사용했던 사람들에 대한 공격이 아님을 일러둔다. 역정규화에 대한 위험성을 알리고, 정규형 또한 빠른 성능을 가졌다는 것을 증명함으로써 종료일자를 사용할 때는 많은 고민이 있어야 한다는 것을 주장하는 것이다.
글의시작
지난 글에서 뜨거운 토론이 있었다. 무려 댓글이 28개나 달렸으며 의견도 크게 두 가지로 갈렸다. 즉 데이터 정합성을 위해 종료일자를 자제해야 한다는 측과 성능관점에서는 장점이 워낙 크므로 종료일자를 사용해야 한다는 측의 두 가지이다. 여러 반론들도 쏟아졌다. 현실 세계의 다양하고 복잡한 SQL이 있으므로 지난 시간에 언급했던 세가지 유형만으로는 충분하지 않다는 것. 일리가 있다. 그래서 이번 시간에는 현실에서 많이 나오는 패턴에 대하여 알아보기로 하자. 즉 MAX 값 한 건이나 특정시점의 한 건을 구하는 것이 아니라 여러 건이 조회되는 패턴에 대해 알아보겠다. 지난 글에서 여러 사람들이 이야기 하는 현실적인 예제란 아래와 같았다.
SELECT 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 svc a, svc_hist b, acct_hist c, cust_hist d
WHERE a.svc_date BETWEEN SYSDATE - 201 AND SYSDATE - 1
AND b.svc_no = a.svc_no
AND d.cust_no = a.cust_no
AND c.acct_no = a.acct_no
AND a.svc_date BETWEEN b.start_dt AND b.end_dt -- 서비스 변경이력
AND a.svc_date BETWEEN c.start_dt AND c.end_dt -- 계정 변경이력
AND a.svc_date BETWEEN d.start_dt AND d.end_dt -- 고객 변경이력
AND b.txt > ' '
AND c.txt > ' '
AND d.txt > ' ' ;
시작일자만으로 이 SQL을 빠르게 조회할 수 있니?
조인조건을 보면 특정시점이 아닌 a.svc_date로 변경이력과 between 조인을 하고 있다. 이런 스타일의 SQL에서는 시작일자 컬럼만으로는 between을 사용할 수 없으므로 느리다는 것. 그리고 SQL의 결과는 한 건이 아니라 여러 건이기 때문에 rownum = 1 조건을 사용할 수 없으므로 역정규화를 하여 종료일자를 추가하는 것이 성능상 유리하다는 의견이 있었다. 이것이 사실일까?
환경 Oracle11g R2
테스트를 위하여 object를 생성하고 통계정보를 수집한다.
CREATE TABLE CUST_HIST AS --고객이력
SELECT A.CUST_NO, B.*
FROM ( SELECT LEVEL AS CUST_NO
FROM DUAL
CONNECT BY LEVEL <= 50) A,
( SELECT SYSDATE - level AS start_dt, --시작일시
SYSDATE - level + 1 - 1/24/60/60 AS end_dt, --종료일시
'종료일자의 필요성 테스트' as txt
FROM DUAL
CONNECT BY LEVEL <= 2000) B;
ALTER TABLE CUST_HIST ADD CONSTRAINT PK_CUST_HIST PRIMARY KEY (CUST_NO, START_DT) USING INDEX;
CREATE INDEX IX_CUST_HIST_01 ON TLO.CUST_HIST (CUST_NO, END_DT, START_DT) ;
CREATE TABLE ACCT_HIST AS -- 계정이력
SELECT A.*, B.*
FROM ( SELECT LEVEL AS ACCT_NO
FROM DUAL
CONNECT BY LEVEL <= 50) A,
( SELECT SYSDATE - level AS start_dt, --시작일시
SYSDATE - level + 1 - 1/24/60/60 AS end_dt, --종료일시
'종료일자의 필요성 테스트' as txt
FROM DUAL
CONNECT BY LEVEL <= 2000) B;
ALTER TABLE ACCT_HIST ADD CONSTRAINT PK_ACCT_HIST PRIMARY KEY (ACCT_NO, START_DT) USING INDEX;
CREATE INDEX IX_ACCT_HIST_01 ON ACCT_HIST (ACCT_NO, END_DT, START_DT) ;
CREATE TABLE SVC AS --서비스
SELECT *
FROM ( SELECT LEVEL AS svc_no,
TRUNC(dbms_random.value(1,51)) AS CUST_NO,
TRUNC(dbms_random.value(1,51)) AS ACCT_NO,
SYSDATE - level + 1 - 0.5 AS SVC_DATE, --서비스 가입일시
'종료일자의 필요성 테스트' as txt
FROM DUAL CONNECT BY LEVEL <= 2000) A ;
ALTER TABLE SVC ADD CONSTRAINT PK_SVC PRIMARY KEY (SVC_NO) USING INDEX;
CREATE INDEX IX_SVC_01 ON SVC (SVC_DATE) ;
CREATE TABLE SVC_HIST AS --서비스이력
SELECT A.svc_no, B.*
FROM ( SELECT svc_no
FROM SVC) A,
( SELECT SYSDATE - level AS start_dt, --시작일시
SYSDATE - level + 1 - 1/24/60/60 AS end_dt, --종료일시
'종료일자의 필요성 테스트' as txt
FROM DUAL
CONNECT BY LEVEL <= 2000) B;
ALTER TABLE SVC_HIST ADD CONSTRAINT PK_SVC_HIST PRIMARY KEY (SVC_NO, START_DT) USING INDEX NOLOGGING;
CREATE INDEX IX_SVC_HIST_01 ON SVC_HIST (SVC_NO, END_DT, START_DT) NOLOGGING;
begin
dbms_stats.gather_table_stats(user, 'SVC_HIST', cascade => true);
dbms_stats.gather_table_stats(user, 'CUST_HIST', cascade => true);
dbms_stats.gather_table_stats(user, 'ACCT_HIST', cascade => true);
dbms_stats.gather_table_stats(user, 'SVC', cascade => true);
end;
이제 테스트를 수행해보자. 종료일자 + 시작일자 인덱스의 장점은 최근 데이터를 구할 때 효율적이다. 따라서 종료일자 + 시작일자 인덱스를 이용하여 비교적 최근 데이터 200 건을 조회해보자. 종료일자 + 시작일자 인덱스를 강제로 사용하기 위해 힌트를 사용하였다.
SELECT /*+ USE_NL(A B C D) INDEX(A IX_SVC_01) INDEX(B IX_SVC_HIST_01)
INDEX(C IX_ACCT_HIST_01) INDEX(D IX_CUST_HIST_01) */
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 svc a, svc_hist b, acct_hist c, cust_hist d
WHERE a.svc_date BETWEEN SYSDATE - 201 AND SYSDATE - 1
AND b.svc_no = a.svc_no
AND d.cust_no = a.cust_no
AND c.acct_no = a.acct_no
AND a.svc_date BETWEEN b.start_dt AND b.end_dt
AND a.svc_date BETWEEN c.start_dt AND c.end_dt
AND a.svc_date BETWEEN d.start_dt AND d.end_dt
AND b.txt > ' '
AND c.txt > ' '
AND d.txt > ' ' ;
--------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
--------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 200 |00:00:00.04 | 1937 |
|* 1 | FILTER | | 1 | | 200 |00:00:00.04 | 1937 |
| 2 | NESTED LOOPS | | 1 | | 200 |00:00:00.04 | 1937 |
| 3 | NESTED LOOPS | | 1 | 180M| 200 |00:00:00.09 | 1738 |
| 4 | NESTED LOOPS | | 1 | 1868K| 200 |00:00:00.04 | 1352 |
| 5 | NESTED LOOPS | | 1 | 19399 | 200 |00:00:00.02 | 783 |
| 6 | TABLE ACCESS BY INDEX ROWID| SVC | 1 | 201 | 200 |00:00:00.01 | 7 |
|* 7 | INDEX RANGE SCAN | IX_SVC_01 | 1 | 201 | 200 |00:00:00.01 | 4 |
|* 8 | TABLE ACCESS BY INDEX ROWID| SVC_HIST | 200 | 96 | 200 |00:00:00.02 | 776 |
|* 9 | INDEX RANGE SCAN | IX_SVC_HIST_01 | 200 | 96 | 200 |00:00:00.01 | 576 |
|* 10 | TABLE ACCESS BY INDEX ROWID | ACCT_HIST | 200 | 96 | 200 |00:00:00.02 | 569 |
|* 11 | INDEX RANGE SCAN | IX_ACCT_HIST_01 | 200 | 96 | 200 |00:00:00.01 | 371 |
|* 12 | INDEX RANGE SCAN | IX_CUST_HIST_01 | 200 | 96 | 200 |00:00:00.01 | 386 |
|* 13 | TABLE ACCESS BY INDEX ROWID | CUST_HIST | 200 | 96 | 200 |00:00:00.01 | 199 |
--------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter(SYSDATE@!-201<=SYSDATE@!-1)
7 - access("A"."SVC_DATE">=SYSDATE@!-201 AND "A"."SVC_DATE"<=SYSDATE@!-1)
8 - filter("B"."TXT">' ')
9 - access("B"."SVC_NO"="A"."SVC_NO" AND "B"."END_DT">=SYSDATE@!-201 AND "B"."END_DT" IS NOT NULL)
filter(("B"."START_DT"<=SYSDATE@!-1 AND "A"."SVC_DATE">="B"."START_DT" AND
"A"."SVC_DATE"<="B"."END_DT"))
10 - filter("C"."TXT">' ')
11 - access("C"."ACCT_NO"="A"."ACCT_NO" AND "C"."END_DT">=SYSDATE@!-201 AND "C"."END_DT" IS NOT
NULL)
filter(("C"."START_DT"<=SYSDATE@!-1 AND "A"."SVC_DATE">="C"."START_DT" AND
"A"."SVC_DATE"<="C"."END_DT"))
12 - access("D"."CUST_NO"="A"."CUST_NO" AND "D"."END_DT">=SYSDATE@!-201 AND "D"."END_DT" IS NOT
NULL)
filter(("D"."START_DT"<=SYSDATE@!-1 AND "A"."SVC_DATE">="D"."START_DT" AND
"A"."SVC_DATE"<="D"."END_DT"))
13 - filter("D"."TXT">' ')
1937 블럭을 Scan하였다. 수행시간도 1초미만으로 최적이다. 그럼 이제 최근 데이터가 아닌 오래된 데이터를 구해보자. 종료일자 + 시작일자 인덱스의 단점은 오래된 데이터를 구할 때 드러난다.
SELECT /*+ USE_NL(A B C D) INDEX(A IX_SVC_01) INDEX(B IX_SVC_HIST_01)
INDEX(C IX_ACCT_HIST_01) INDEX(D IX_CUST_HIST_01) */
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 svc a, svc_hist b, acct_hist c, cust_hist d
WHERE a.svc_date BETWEEN SYSDATE - 1999 AND SYSDATE - 1799 -- 오래된 데이터 조회
AND b.svc_no = a.svc_no
AND d.cust_no = a.cust_no
AND c.acct_no = a.acct_no
AND a.svc_date BETWEEN b.start_dt AND b.end_dt
AND a.svc_date BETWEEN c.start_dt AND c.end_dt
AND a.svc_date BETWEEN d.start_dt AND d.end_dt
AND b.txt > ' '
AND c.txt > ' '
AND d.txt > ' ' ;
--------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
--------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 200 |00:00:02.15 | 6498 |
|* 1 | FILTER | | 1 | | 200 |00:00:02.15 | 6498 |
| 2 | NESTED LOOPS | | 1 | | 200 |00:00:02.15 | 6498 |
| 3 | NESTED LOOPS | | 1 | 182M| 200 |00:00:01.31 | 6301 |
| 4 | NESTED LOOPS | | 1 | 1889K| 200 |00:00:01.21 | 4425 |
| 5 | NESTED LOOPS | | 1 | 19528 | 200 |00:00:01.11 | 2344 |
| 6 | TABLE ACCESS BY INDEX ROWID| SVC | 1 | 202 | 200 |00:00:00.01 | 7 |
|* 7 | INDEX RANGE SCAN | IX_SVC_01 | 1 | 202 | 200 |00:00:00.01 | 3 |
|* 8 | TABLE ACCESS BY INDEX ROWID| SVC_HIST | 200 | 97 | 200 |00:00:00.77 | 2337 |
|* 9 | INDEX RANGE SCAN | IX_SVC_HIST_01 | 200 | 97 | 200 |00:00:00.65 | 2137 |
|* 10 | TABLE ACCESS BY INDEX ROWID | CUST_HIST | 200 | 97 | 200 |00:00:00.09 | 2081 |
|* 11 | INDEX RANGE SCAN | IX_CUST_HIST_01 | 200 | 97 | 200 |00:00:00.09 | 1887 |
|* 12 | INDEX RANGE SCAN | IX_ACCT_HIST_01 | 200 | 97 | 200 |00:00:00.09 | 1876 |
|* 13 | TABLE ACCESS BY INDEX ROWID | ACCT_HIST | 200 | 97 | 200 |00:00:00.01 | 197 |
--------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter(SYSDATE@!-1999<=SYSDATE@!-1799)
7 - access("A"."SVC_DATE">=SYSDATE@!-1999 AND "A"."SVC_DATE"<=SYSDATE@!-1799)
8 - filter("B"."TXT">' ')
9 - access("B"."SVC_NO"="A"."SVC_NO" AND "B"."END_DT">=SYSDATE@!-1999 AND "B"."END_DT" IS NOT NULL)
filter(("B"."START_DT"<=SYSDATE@!-1799 AND "A"."SVC_DATE">="B"."START_DT" AND
"A"."SVC_DATE"<="B"."END_DT"))
10 - filter("D"."TXT">' ')
11 - access("D"."CUST_NO"="A"."CUST_NO" AND "D"."END_DT">=SYSDATE@!-1999 AND "D"."END_DT" IS NOT NULL)
filter(("D"."START_DT"<=SYSDATE@!-1799 AND "A"."SVC_DATE">="D"."START_DT" AND
"A"."SVC_DATE"<="D"."END_DT"))
12 - access("C"."ACCT_NO"="A"."ACCT_NO" AND "C"."END_DT">=SYSDATE@!-1999 AND "C"."END_DT" IS NOT NULL)
filter(("C"."START_DT"<=SYSDATE@!-1799 AND "A"."SVC_DATE">="C"."START_DT" AND
"A"."SVC_DATE"<="C"."END_DT"))
13 - filter("C"."TXT">' ')
무려 6498 블럭을 Scan 하였다. 즉 3배 이상 느려져서 수행시간도 2초가 넘어버렸다. 물론 오래된 데이터를 자주 사용하지 않는다면 피해가 줄어들 것이다. 하지만 가끔이라도 오래된 데이터를 조회한다면 Timeout이 발생할 수 있다. 즉 성능을 위해 종료일자 컬럼을 추가하였지만 시점이 언제인가에 따라 성능이 더 느려질 수 있다는 것이다. 이 예제에서는 오래된 데이터를 2005년도로 가정하고 조회하였다. 현실에서는 2005년이면 그리 오래된 것이 아닐 수도 있다. 만약 테스트 데이터를 더 많이 만들어 2000년도 이전 데이터를 조회한다면 더 느려질 것이다.
그럼 이 문제를 어떻게 해결할 거니?
먼저 type을 하나 만들자. 이제부터는 종료일자 컬럼은 없다고 가정한다. 그리고 인덱스도 시작일자 인덱스만 사용한다.
CREATE OR REPLACE TYPE hist_type AS OBJECT
(b_st_dt date,
b_txt char(24),
c_st_dt date,
c_txt char(24),
d_st_dt date,
d_txt char(24) );
/
SELECT a.svc_no, a.cust_no, a.acct_no, a.svc_date, a.txt,
a.h.b_st_dt, a.h.b_txt, a.h.c_st_dt, a.h.c_txt, a.h.d_st_dt, a.h.d_txt
FROM (SELECT a.*,
(SELECT hist_type(b.start_dt, b.txt, c.start_dt, c.txt, d.start_dt, d.txt)
FROM ( SELECT * FROM svc_hist b ORDER BY start_dt DESC) b, --변경이력1
( SELECT * FROM cust_hist c ORDER BY start_dt DESC) c, --변경이력2
( SELECT * FROM acct_hist d ORDER BY start_dt DESC) d --변경이력3
WHERE b.svc_no = a.svc_no
AND b.start_dt <= a.svc_date
AND b.txt > ' '
AND c.cust_no = a.cust_no
AND c.start_dt <= a.svc_date
AND c.txt > ' '
AND d.acct_no = a.acct_no
AND d.start_dt <= a.svc_date
AND d.txt > ' '
AND ROWNUM = 1) AS h -- ROWNUM 사용
FROM svc a
WHERE a.svc_date BETWEEN SYSDATE - 201 AND SYSDATE - 1) a ;
위의 SQL이 복잡해 보이지만 스칼라 서브쿼리에 집중해 주기 바란다. 스칼라 서브쿼리 내부의 인라인뷰는 변경이력 테이블들이다. 여러 개의 서로 다른 변경이력 테이블을 조회한다고 해도 from 절에 인라인뷰를 계속 추가하면 된다. 위의 예제에서는 변경이력을 3개만 사용하였다. 그리고 여러 건을 조회할 때에도 rownum을 사용할 수 있음을 알 수 있다. 여기서 인라인뷰에 order by를 사용한 것은 Index_desc 힌트와 rownum = 1 조합은 안전한가? 에서 언급된 order by가 적용된 인라인뷰와 rownum의 원리를 이용한 것이다. 그리고 FPD(Filter Push Down : 조건이 뷰 내부로 파고듦)을 활용한 것이다.
-----------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 200 |00:00:00.01 | 7 |
|* 1 | COUNT STOPKEY | | 200 | | 200 |00:00:00.02 | 2006 |
| 2 | NESTED LOOPS | | 200 | 1 | 200 |00:00:00.01 | 2006 |
| 3 | NESTED LOOPS | | 200 | 1 | 200 |00:00:00.01 | 1202 |
| 4 | VIEW | | 200 | 1 | 200 |00:00:00.01 | 600 |
|* 5 | TABLE ACCESS BY INDEX ROWID | CUST_HIST | 200 | 100 | 200 |00:00:00.01 | 600 |
|* 6 | INDEX RANGE SCAN DESCENDING| PK_CUST_HIST | 200 | 18 | 200 |00:00:00.01 | 400 |
| 7 | VIEW | | 200 | 1 | 200 |00:00:00.01 | 602 |
|* 8 | TABLE ACCESS BY INDEX ROWID | ACCT_HIST | 200 | 100 | 200 |00:00:00.01 | 602 |
|* 9 | INDEX RANGE SCAN DESCENDING| PK_ACCT_HIST | 200 | 18 | 200 |00:00:00.01 | 402 |
| 10 | VIEW | | 200 | 1 | 200 |00:00:00.01 | 804 |
|* 11 | TABLE ACCESS BY INDEX ROWID | SVC_HIST | 200 | 100 | 200 |00:00:00.01 | 804 |
|* 12 | INDEX RANGE SCAN DESCENDING | PK_SVC_HIST | 200 | 18 | 200 |00:00:00.01 | 604 |
|* 13 | FILTER | | 1 | | 200 |00:00:00.01 | 7 |
| 14 | TABLE ACCESS BY INDEX ROWID | SVC | 1 | 201 | 200 |00:00:00.01 | 7 |
|* 15 | INDEX RANGE SCAN | IX_SVC_01 | 1 | 201 | 200 |00:00:00.05 | 4 |
-----------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter(ROWNUM=1)
5 - filter("C"."TXT">' ')
6 - access("C"."CUST_NO"=:B1 AND "C"."START_DT"<=:B2)
8 - filter("D"."TXT">' ')
9 - access("D"."ACCT_NO"=:B1 AND "D"."START_DT"<=:B2)
11 - filter("B"."TXT">' ')
12 - access("B"."SVC_NO"=:B1 AND "B"."START_DT"<=:B2)
13 - filter(SYSDATE@!-201<=SYSDATE@!-1)
15 - access("A"."SVC_DATE">=SYSDATE@!-201 AND "A"."SVC_DATE"<=SYSDATE@!-1)
성능 또한 최적이다. 물론 종료일자 + 시작일자 인덱스에 비해 조금 많은 블럭을 Scan 하였지만 무시할 수 있는 정도이다. 이 정도면 종료일자 + 시작일자 인덱스 보다 우월하다. 그 이유는 오래된 데이터를 조회 할 때에도 성능이 동일 하다는 것이다. 아래의 SQL을 보자.
SELECT a.svc_no, a.cust_no, a.acct_no, a.svc_date, a.txt,
a.h.b_st_dt, a.h.b_txt, a.h.c_st_dt, a.h.c_txt, a.h.d_st_dt, a.h.d_txt
FROM (SELECT a.*,
(SELECT hist_type(b.start_dt, b.txt, c.start_dt, c.txt, d.start_dt, d.txt)
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 b.start_dt <= a.svc_date
AND b.txt > ' '
AND c.cust_no = a.cust_no
AND c.start_dt <= a.svc_date
AND c.txt > ' '
AND d.acct_no = a.acct_no
AND d.start_dt <= a.svc_date
AND d.txt > ' '
AND ROWNUM = 1) AS h
FROM svc a
WHERE a.svc_date BETWEEN SYSDATE - 1999 AND SYSDATE - 1799) a ;
-----------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 200 |00:00:00.01 | 7 |
|* 1 | COUNT STOPKEY | | 200 | | 200 |00:00:00.02 | 2004 |
| 2 | NESTED LOOPS | | 200 | 1 | 200 |00:00:00.01 | 2004 |
| 3 | NESTED LOOPS | | 200 | 1 | 200 |00:00:00.01 | 1202 |
| 4 | VIEW | | 200 | 1 | 200 |00:00:00.01 | 602 |
|* 5 | TABLE ACCESS BY INDEX ROWID | CUST_HIST | 200 | 100 | 200 |00:00:00.01 | 602 |
|* 6 | INDEX RANGE SCAN DESCENDING| PK_CUST_HIST | 200 | 18 | 200 |00:00:00.01 | 402 |
| 7 | VIEW | | 200 | 1 | 200 |00:00:00.01 | 600 |
|* 8 | TABLE ACCESS BY INDEX ROWID | ACCT_HIST | 200 | 100 | 200 |00:00:00.01 | 600 |
|* 9 | INDEX RANGE SCAN DESCENDING| PK_ACCT_HIST | 200 | 18 | 200 |00:00:00.01 | 400 |
| 10 | VIEW | | 200 | 1 | 200 |00:00:00.01 | 802 |
|* 11 | TABLE ACCESS BY INDEX ROWID | SVC_HIST | 200 | 100 | 200 |00:00:00.01 | 802 |
|* 12 | INDEX RANGE SCAN DESCENDING | PK_SVC_HIST | 200 | 18 | 200 |00:00:00.01 | 602 |
|* 13 | FILTER | | 1 | | 200 |00:00:00.01 | 7 |
| 14 | TABLE ACCESS BY INDEX ROWID | SVC | 1 | 202 | 200 |00:00:00.01 | 7 |
|* 15 | INDEX RANGE SCAN | IX_SVC_01 | 1 | 202 | 200 |00:00:00.01 | 3 |
-----------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter(ROWNUM=1)
5 - filter("C"."TXT">' ')
6 - access("C"."CUST_NO"=:B1 AND "C"."START_DT"<=:B2)
8 - filter("D"."TXT">' ')
9 - access("D"."ACCT_NO"=:B1 AND "D"."START_DT"<=:B2)
11 - filter("B"."TXT">' ')
12 - access("B"."SVC_NO"=:B1 AND "B"."START_DT"<=:B2)
13 - filter(SYSDATE@!-1999<=SYSDATE@!-1799)
15 - access("A"."SVC_DATE">=SYSDATE@!-1999 AND "A"."SVC_DATE"<=SYSDATE@!-1799)
예전 데이터를 조회할 때에도 성능이 동일함을 알 수 있다. 이것은 매우 중요한 관점이다. 같은 SQL을 실행함에도 매우 느릴때가 있고, 매우 빠를 때가 있다면 그 SQL의 성능은 불안정하다고 할 수 있다. 이로써 이전 글에서 이슈가 되었던 여러 건을 조회할 때에도 종료일자는 필요 없음을 알 수 있다. 물론 SQL이 몇 줄 길어지긴 했지만 그 이유 때문에 역정규화의 단점인 데이터의 정합성을 해칠 수는 없는 일이다. SQL이 몇 줄 늘어나더라도 이렇게 사용해야 하는 이유는 또 있다.
역정규화할때 필요한 정합성을 체크하는 프로그램의 길이를 생각하면 몇 줄의 손해는 아무것도 아니다. 여기에 더하여 정합성을 보정하는 프로그램도 필요하므로 위의 몇 줄이 추가된 SQL이 손해라고 생각할 수는 없다. 또한 위의 SQL이 어려운 전문가용 SQL이라고 생각 하지는 않는다.
우리는 Type을 쓸 수 없다
만약 type을 이용할 수 없는 환경이라면 아래의 SQL을 이용해야 한다. 비슷한 유형이지만 이 경우는 변경이력 테이블을 두 번 Scan하는 비효율을 감안해야 한다. 물론 이때에도 rownum = 1은 사용할 수 있다. 먼저 최근의 데이터를 조회한다.
SELECT 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 b.start_dt <= a.svc_date
AND b.txt > ' '
AND c.cust_no = a.cust_no
AND c.start_dt <= a.svc_date
AND c.txt > ' '
AND d.acct_no = a.acct_no
AND d.start_dt <= a.svc_date
AND d.txt > ' '
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 | E-Rows | A-Rows | A-Time | Buffers |
------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 200 |00:00:00.02 | 2610 |
| 1 | NESTED LOOPS | | 1 | 8040G| 200 |00:00:00.02 | 2610 |
|* 2 | COUNT STOPKEY | | 200 | | 200 |00:00:00.01 | 2006 |
| 3 | NESTED LOOPS | | 200 | 1 | 200 |00:00:00.01 | 2006 |
| 4 | NESTED LOOPS | | 200 | 1 | 200 |00:00:00.01 | 1202 |
| 5 | VIEW | | 200 | 1 | 200 |00:00:00.01 | 600 |
|* 6 | TABLE ACCESS BY INDEX ROWID | CUST_HIST | 200 | 100 | 200 |00:00:00.01 | 600 |
|* 7 | INDEX RANGE SCAN DESCENDING| PK_CUST_HIST | 200 | 18 | 200 |00:00:00.01 | 400 |
| 8 | VIEW | | 200 | 1 | 200 |00:00:00.01 | 602 |
|* 9 | TABLE ACCESS BY INDEX ROWID | ACCT_HIST | 200 | 100 | 200 |00:00:00.01 | 602 |
|* 10 | INDEX RANGE SCAN DESCENDING| PK_ACCT_HIST | 200 | 18 | 200 |00:00:00.01 | 402 |
| 11 | VIEW | | 200 | 1 | 200 |00:00:00.01 | 804 |
|* 12 | TABLE ACCESS BY INDEX ROWID | SVC_HIST | 200 | 100 | 200 |00:00:00.01 | 804 |
|* 13 | INDEX RANGE SCAN DESCENDING | PK_SVC_HIST | 200 | 18 | 200 |00:00:00.01 | 604 |
| 14 | NESTED LOOPS | | 1 | 201M| 200 |00:00:00.02 | 2410 |
| 15 | NESTED LOOPS | | 1 | 201K| 200 |00:00:00.02 | 2212 |
| 16 | VIEW | | 1 | 201 | 200 |00:00:00.02 | 2013 |
|* 17 | FILTER | | 1 | | 200 |00:00:00.01 | 7 |
| 18 | TABLE ACCESS BY INDEX ROWID | SVC | 1 | 201 | 200 |00:00:00.01 | 7 |
|* 19 | INDEX RANGE SCAN | IX_SVC_01 | 1 | 201 | 200 |00:00:00.01 | 4 |
| 20 | TABLE ACCESS BY USER ROWID | CUST_HIST | 200 | 1000 | 200 |00:00:00.01 | 199 |
| 21 | TABLE ACCESS BY USER ROWID | ACCT_HIST | 200 | 1000 | 200 |00:00:00.01 | 198 |
| 22 | TABLE ACCESS BY USER ROWID | SVC_HIST | 200 | 40000 | 200 |00:00:00.01 | 200 |
------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter(ROWNUM=1)
6 - filter("C"."TXT">' ')
7 - access("C"."CUST_NO"=:B1 AND "C"."START_DT"<=:B2)
9 - filter("D"."TXT">' ')
10 - access("D"."ACCT_NO"=:B1 AND "D"."START_DT"<=:B2)
12 - filter("B"."TXT">' ')
13 - access("B"."SVC_NO"=:B1 AND "B"."START_DT"<=:B2)
17 - filter(SYSDATE@!-201<=SYSDATE@!-1)
19 - access("A"."SVC_DATE">=SYSDATE@!-201 AND "A"."SVC_DATE"<=SYSDATE@!-1)
변경이력에서 단 하나의 컬럼만 조회할 때는 위의 방법을 사용할 필요가 없다. rowid 대신에 값을 select 하면 된다. 따라서 비효율도 없다. 하지만 변경이력에서 여러 컬럼을 조회해야 한다면 위의 방법처럼 rowid를 사용해야 한다. 그렇게 되면 변경이력 테이블은 두 번씩 액세스 하는 셈이다. 하지만 이때에도 인덱스는 두 번 Scan 하지 않는다. 이제 성능의 안정성을 보기 위해 오래된 데이터를 조회해 보자.
SELECT 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 b.start_dt <= a.svc_date
AND b.txt > ' '
AND c.cust_no = a.cust_no
AND c.start_dt <= a.svc_date
AND c.txt > ' '
AND d.acct_no = a.acct_no
AND d.start_dt <= a.svc_date
AND d.txt > ' '
AND ROWNUM = 1) AS rid
FROM svc a
WHERE a.svc_date BETWEEN SYSDATE - 1999 AND SYSDATE - 1799 ) 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 | E-Rows | A-Rows | A-Time | Buffers |
------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 200 |00:00:00.02 | 2602 |
| 1 | NESTED LOOPS | | 1 | 8080G| 200 |00:00:00.02 | 2602 |
|* 2 | COUNT STOPKEY | | 200 | | 200 |00:00:00.02 | 2004 |
| 3 | NESTED LOOPS | | 200 | 1 | 200 |00:00:00.01 | 2004 |
| 4 | NESTED LOOPS | | 200 | 1 | 200 |00:00:00.01 | 1202 |
| 5 | VIEW | | 200 | 1 | 200 |00:00:00.01 | 602 |
|* 6 | TABLE ACCESS BY INDEX ROWID | CUST_HIST | 200 | 100 | 200 |00:00:00.01 | 602 |
|* 7 | INDEX RANGE SCAN DESCENDING| PK_CUST_HIST | 200 | 18 | 200 |00:00:00.01 | 402 |
| 8 | VIEW | | 200 | 1 | 200 |00:00:00.01 | 600 |
|* 9 | TABLE ACCESS BY INDEX ROWID | ACCT_HIST | 200 | 100 | 200 |00:00:00.01 | 600 |
|* 10 | INDEX RANGE SCAN DESCENDING| PK_ACCT_HIST | 200 | 18 | 200 |00:00:00.01 | 400 |
| 11 | VIEW | | 200 | 1 | 200 |00:00:00.01 | 802 |
|* 12 | TABLE ACCESS BY INDEX ROWID | SVC_HIST | 200 | 100 | 200 |00:00:00.01 | 802 |
|* 13 | INDEX RANGE SCAN DESCENDING | PK_SVC_HIST | 200 | 18 | 200 |00:00:00.01 | 602 |
| 14 | NESTED LOOPS | | 1 | 202M| 200 |00:00:00.03 | 2402 |
| 15 | NESTED LOOPS | | 1 | 202K| 200 |00:00:00.02 | 2205 |
| 16 | VIEW | | 1 | 202 | 200 |00:00:00.02 | 2011 |
|* 17 | FILTER | | 1 | | 200 |00:00:00.01 | 7 |
| 18 | TABLE ACCESS BY INDEX ROWID | SVC | 1 | 202 | 200 |00:00:00.01 | 7 |
|* 19 | INDEX RANGE SCAN | IX_SVC_01 | 1 | 202 | 200 |00:00:00.01 | 3 |
| 20 | TABLE ACCESS BY USER ROWID | CUST_HIST | 200 | 1000 | 200 |00:00:00.01 | 194 |
| 21 | TABLE ACCESS BY USER ROWID | ACCT_HIST | 200 | 1000 | 200 |00:00:00.01 | 197 |
| 22 | TABLE ACCESS BY USER ROWID | SVC_HIST | 200 | 40000 | 200 |00:00:00.01 | 200 |
------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter(ROWNUM=1)
6 - filter("C"."TXT">' ')
7 - access("C"."CUST_NO"=:B1 AND "C"."START_DT"<=:B2)
9 - filter("D"."TXT">' ')
10 - access("D"."ACCT_NO"=:B1 AND "D"."START_DT"<=:B2)
12 - filter("B"."TXT">' ')
13 - access("B"."SVC_NO"=:B1 AND "B"."START_DT"<=:B2)
17 - filter(SYSDATE@!-1999<=SYSDATE@!-1799)
19 - access("A"."SVC_DATE">=SYSDATE@!-1999 AND "A"."SVC_DATE"<=SYSDATE@!-1799)
이번에는 같은 방법으로 오래된 데이터를 조회 해보았지만 성능은 역시 안정적이다. 최근 데이터를 조회할 때는 종료일자 + 시작일자 인덱스가 1937 블럭을 Scan하였고 위의 SQL 에서는 2602블럭을 Scan 하였으므로 좋지 않다고 생각할 수 있다. 하지만 오래된 데이터를 구할 때 종료일자 + 시작일자 인덱스가 6498 블럭이나 Scan 하였다는 점을 생각하면 같은 테이블들을 두번씩이나 사용했음에도 성능에서 우열을 가리기 힘들다.
결론
변경이력을 Between 으로 조인해서 여러 건을 조회할 때에도 종료일자의 장점은 찾기 힘들므로 변경일자(시작일자)만 사용하면 된다. 어찌되었건 현재와 같은 무조건적인 종료일자의 사용은 자제해야 한다. 역정규화를 하거나 하지 않는 것은 당신의 자유다. 그리고 변경이력에 종료일자를 추가하여 역정규화 하는 것은 정합성 측면에서는 손실이지만 장점도 있다. 하지만 그것은 성능이 아니라 SQL이 간단해진다는 관점이 아닐까? 몇 배나 단순해질지가 문제이긴 하지만....
PS
이 글을 보고 성급하게 운영중인 테이블을 건드리면 안 된다. 제발 운영중인 테이블과 인덱스는 그냥 두기 바란다. 만약 역정규화를 제거하려면 다음 번 모델링 시에 반영하기 바란다. 그것이 아니라면 철저히 준비하고 실행하기 바란다.
현실세계의 모든 문제를 해결할 수 있는 글이란 없다. 그러므로 시작일자 컬럼만으로 해결하기 어려운 또 다른 패턴의 SQL이 있다면 나에게 알려주기 바란다. 그래야 더 많은 문제가 해결 될 터이니. 시작일자만으로 구현하기 어려운 SQL들도 결국은 몇 개의 패턴으로 정리되지 않을까 생각해본다.
댓글을 달아 주세요
FK에 대한 새로운 관점이군요. 찬찬히 생각을 해봐야 할 듯 합니다..
방문 감사드립니다.
어느모델러 2011.02.25 08:23 댓글주소 수정/삭제 댓글쓰기
부끄럽습니다. 관계선을 그었던 1인
동규님의 글은 공통점이 있네요.
서론, 본론, 결론에 따라 내 생각이 아래처럼 바뀝니다.
항상 이런 패턴으로 글을 쓰시는것 같습니다.
서론 : 말도 안되는 글인데 어떤 이야기를 하는지 들어보자.
본문 : 일리가 있는 글이군
결론 : 앞으로 그렇게 하면 안되겠네
감사합니다.
어쩌다 보니 글이 그런 패턴으로 되었네요.
읽는 사람이 지루하지는 않을 것 같습니다.
혈기린 2011.02.25 10:46 댓글주소 수정/삭제 댓글쓰기
김기창 수석님 블로그에서 댓글로 언급하셨던 내용에 대한 상세한 설명이시네요
그 댓글보고 엔티티에 대해서 아무 생각없이 당연한 관계라고했던 것에 대한 생각을 고치는 계기가 되었네요 좋은글 계속 감사 드립니다 ~~
모델링에 대한 글도 자주좀 올려주세요 눈이 번쩍 뜨이게 ㅎㅎ
네 김수석님 블로그에 제가 이와 관련된 댓글을 작성한 적이 있습니다. 제가 링크를 함부로 달지 않는데, 김수석님 블로그는 너무 좋은 내용이 많아서 안달수가 없었습니다.
그럼 수고하세요
엄청 어려운 일이네요. 쭈욱 두 번 읽어봤는데 사실 제 머리로는 아직 이해가 안가는 부분이 많아요^^;;; 다시 한번 더 읽어봐야겠어요^^
두번 읽으셨는데 이해가 안가신다면 그건 제 잘못입니다.^^
어느 부분이 이해가 안가시는지 설명해 주시면 설명을 추가하도록 하겠습니다.
이 글의 핵심은 부모의 식별자 값이 자식의 관계속성으로 들어오면(insert 되면) 전달관계이고 들어오지 않으면 조회관계라는 것입니다. 부서(부모) 사원(자식)의 경우는 데이터가 부모로 부터 오는것이 아나라 오히려 자식격인 인사시스템의 발령에서 오는 것입니다. 따라서 조회관계라고 할 수 있습니다.
혈기린 2011.02.25 13:36 댓글주소 수정/삭제 댓글쓰기
한가지 궁금한게 있는데요 조회관계 전달관계의 명칭이 관계형 이론에 있는 관계의 종류인가요 아니면 오수석님께서 임의로 만든 용어인가요?
몇권은 안되지만 제가 읽은 모델링 서적에는 없는 용어이기에 질문을 드립니다.
모델러 마다 엔티티의 분류도 조금 다르더군요
전달관계/조회관계는 제가 만든것 입니다. 용어를 만들었다기 보다는 개념을 만든것 입니다.
EF 코드 박사가 제시한 관계형이론에는 조회/전달 관계라는 용어는 없습니다. 이 용어뿐만 아니라 직접/간접 관계라는 용어도 없습니다. 관계형이론은 아주 짧은 글이므로 그 이론이 만들어진 후에 여러 개인(전문가)이 추가적인 용어를 만든것 입니다. 혈기린님의 말에 의하면 관계형이론에 없는 것은 모두 임의로 만든거라 할 수 있습니다.^^ 하지만 임의로 만든것이라도 그것이 옳은 개념이고, 그 개념으로 더 나은 모델을 만들수 있다면 더 이상 개인적이고 임의적이라고 볼 수는 없겠지요. E사의 이화식 사장님과 오픈메이드의 김기창 수석도 마찬가지로 이론에 없는 여러가지 용어를 사용하고 있습니다.
전달관계, 조회관계라는 용어는 제가 최초로 만든것이므로 그 어떤 모델링책에도 언급되지 않습니다. 부서-사원같은 잘못된 모델을 그대로 두고 볼 수 없기에 만든것 입니다. 하지만, 앞으로 나올 모델링 책에서는 이 개념을 사용하여 실수를 방지하는 것이 좋을 것입니다.
finecomp 2011.03.01 01:38 댓글주소 수정/삭제 댓글쓰기
항상 좋은 글 감사합니다...;
전달관계/조회관계...개념만큼이나 선택하신 용어도 맘에 딱 듭니다...^^;
저는 이전부터 잘못된 원인을 약간 다른 관점에서 보고 있었습니다.
부서-사원의 관계를 전달관계로 설정한 후, 필요에 의해 사원의 소속조직 이력관리의 개념으로 발령엔티티가 나타났고,
발령을 관계맺은 후에도 처음 맺어졌던 부서-사원관계를 그대로 두었고,
이 후 이런 모델을 만들었던 노련한 모델러 일수록 습관적으로 관계선을 그렸고 이것이 굳어져 현재는 오류로 보이는 듯 합니다.
이와 유사하게 아직은 담당관리센터에 대한 이력관리가 필요치 않은 고객-센터(관리센터)의 예와 비슷할 듯 합니다.
고객과 센터가 처음에는 전달관계로 설정되겠지요.
이 후 필요에 의해 고객의 관리센터이력을 추가해야 할 필요성이 생기는 순간, 부서-사원과 동일한 문제가 발생하게 되겠지요.
즉, 많은 경우의 전달관계에 대해서, 이런 유형의 이력이 생기는 순간 원래의 관계가 현행화 될 필요가 생길 수 있을 것 같습니다.
결론적으로, 최초설계/변경 시에 좀 더 신중히 관계를 검토하고 조회관계 시엔 코멘트라도 잘 달아야겠다는 반성이 생기는군요...;
저도 몇년 전 까지는 이런 오류를 범했습니다.
그런데 finecomp님은 이미 잘못된 원인을 다른 관점에서 보고 있었네요. 그리고 개념과 용어가 마음에 드신다니 다행입니다.
에너자이져 2011.03.10 19:31 댓글주소 수정/삭제 댓글쓰기
전달관계=직접관계, 조회관계=간접관계 의 등식이 성립되는거 같습니다.
직접/간접관계 보다는 전달/조회관계 라는 용어가 이해를 빠르게 하네요.
이해가 빠르다고 하시니 다행입니다.
전달관계=직접관계, 조회관계=간접관계가 성립하듯이
직접관계=전달관계, 간접관계=전달관계도 성립합니다.
감사합니다.
김상래 2011.06.12 11:42 댓글주소 수정/삭제 댓글쓰기
안녕하세요. 데이터 모델링을 하면서 항상 관계(Relationship)에 대해 많이 고민하게 되는데,
상당히 의미있고 중요한 부분을 집어내어서 명확히 풀어내신것 같습니다. 저도 개인적으로
머리속에 두서없이 비슷한 생각을 하고 있었는데 오늘 이 글을 통해 어느정도 자연스레 정리된 느낌이네요.
저는 저의 언어로 이렇게 정리를 했습니다. 확인차 몇가지 질문도 함께 드려봅니다
1. 저는 관계를 크게 엔터티간에 종속적인 관계와 참조적인 관계가 있다라고 보는데,
블로그에 쓰신 전달관계와 조회관계라는 용어의 맥락에서 볼때
종속적인 관계는 분명 부모 식별자의 데이터가 (관계속성으로) 자식으로 전달되니
부모 자식의 관계가 명확한 종속적인 관계 = 전달관계로, 당연히 관계선이 표현되어야 하고
RI 제약 생성의 대상이 된다고 볼수 있습니다.
즉, 종속관계는 전달관계로 관계가 표현되어야 한다는 것은 의심의 여지가 없습니다.
2. 문제는 조회관계와 참조관계인데.. 종속관계가 아닌 참조관계는 블로그에 언급된
전달관계가 있는 참조관계와 단순 조회관계인 참조관계를 포함하는 개념으로 보아야 할것 같습니다.
예를들어 고객의 직업에 있어서, 직업코드와 직업명을 관리하는 직업엔터티가 코드성으로 존재할 때
고객 엔터티에 직업코드가 일반 속성으로 존재한다면 이 경우는 전달관계가 있는 일반적인 참조관계로 보고
RI를 적용하여 FK Constraint를 생성하여 직업엔터티에 존재하는 직업코드만이
고객의 직업코드로 들어올수 있게 한다는 개념으로 정리해도 될까요?
3. 그렇다면 블로그에 언급하신 조회관계는 모두 역정규화에 의해 발생한 속성으로부터 발생된 것을
의미하는 것인가요? 이 글에서 언급하신 사원의 부서코드와 같이 외부시스템(외부테이블)과의 관계에 의해
나온, 즉 데이터 중복인 역정규화 속성으로 발생한 참조관계인 경우,
이를 조회관계라는 용어로 언급하신게 맞는지 궁금합니다.
정리하면 블로그에서 언급된 전달관계/조회관계라는 용어와 제가 생각하는
종속관계/참조관계 용어의 추상화정도와 관점을 맞춘다고 봤을 때, 아래와 같이 귀결시킬수 있을지 의견 부탁드려봅니다.
전달관계 : 종속관계 전체 및 역정규화된 속성에 의해 발생하는 단순 조회관계를 제외한
참조관계(일반적인 Non-identifying)까지를 포함
조회관계 : 외부테이블과의 관계에 의해 발생한 속성에 따른 참조관계로, Relationship을 관리할 필요가 없다.
다만 Join의 대상이 되므로 모델링 툴에서 표현을 할 필요는 있다.
아.. 그런데 이렇게 글로 정리하다보니 제가 생각하는 참조관계란 결국 애매하게 조회관계까지를 포함하는
개념이었듯 한데, 엄밀하게 정리를 하자면 전달관계가 있는 종속관계와 전달관계가 있는 참조관계, 그리고 전달관계가 없는
반정규화에 따른 조회관계가 남게 되는군요...
2년이 지났지만 답변드리겠습니다.
전달관계는 식별관계이든 아니든 상관없이 상위 엔티티의 식별자가
하위 엔티티의 속성으로 상속되는 것입니다.
위 쪽의 고객과 주문간의 관계는 일반 속성으로 상속되었으므로 비식별 관계이면서 전달 관계입니다. 이와는 반대로 상품과 주문상품의 관계는 식별자로 상속되었으므로 식별 관계이면서 전달 관계입니다.
조회관계는 외부테이블 이든 아니든 역정규화에 의한 관계입니다. 위의 예제에서는 max(마지막) 값을 가져다 놓은 것을 언급했습니다.
감사합니다.
김시준 2013.05.31 12:24 댓글주소 수정/삭제 댓글쓰기
좋은 글 감사드립니다.
도움이 되었으면 합니다.^^
주지영 2013.09.26 17:32 댓글주소 수정/삭제 댓글쓰기
모델링 접한지 얼마안된 DB관련경력자입니다. 책을 보면서도 속시원하게 뭔가 풀리지 않고 답답한 구석이 있었는데 ...
심지어 모델링 공부하는 사람에게 물어봐도 '이건 당연하게 사용했던거라...'라는 답변을 꼭 들어야만 했습니다.
앞으로 종종 들러 많은 가르침 배우고 싶네요.
감사합니다(__)~
잘 읽었습니다. 관계선에 대해 다시 한번 생각하고 정리 할 수 있는 기회였습니다.