부서-사원 모델에 대한 단상

조회관계란 무엇인가
전달관계와 조회관계의 차이점


거의 모든 프로젝트 현장에서 위와 같은 잘못된 모델이 등장한다. 위의 부서-사원 모델에서 틀린 곳을 발견할 수 있는가? 잘못된 점을 발견하지 못했다면, 여러분도 잘못된 모델링을 하고 있을 가능성이 높다. 이 글은 부서와 사원 사이에 존재하는 관계선이 잘못되었다는 것을 이야기 하고자 한다.

 

어느 설계자가 논리모델링을 하고 있었다. 그가 작업중인 위의 모델을 보는 순간 기대를 했지만 역시나 실망했다. 왜냐하면 부서엔티티에서 사원엔티티로 관계선을 긋는 것을 보았기 때문이다. 부서와 사원은 가장 기본적인 엔티티 아닌가? 핵심엔티티임에도 불구하고 관계선을 마음 내키는 대로 긋고 있는 것이다. 그래서 내가 그 설계자에게 다음과 같이 질문을 하였다.

 

필자: 사원엔티티와 부서엔티티의 관계는 사원이 현재 소속된 부서를 의미합니까?

설계자: 네 당연히 사원의 현 소속부서를 의미합니다.

필자: 그렇다면 부서와 사원 사이에 관계선을 긋지 마시고, 그냥 부서코드속성을 추가하세요.

설계자: 왜요? 부서와 사원은 1:N 관계이므로, 당연히 그어야 되는 것 아닌가요?

 

설계자의 잘못이 아니다

그렇다. 당연히 그렇게 생각할 것이다. 모델링공부를 많이 한 사람일수록 반사적으로 부서와 사원 사이에 관계선을 그어 버린다. 잘못된 관행이 온 세상을 덮고 있다. 왜냐하면, 영문원서를 포함하여 거의 모든 국내의 모델링 책에 부서-사원관계를 그어버리는 잘못을 범했기 때문이다. 교과서가 잘못되어 있는데 학생이 제대로 된 사고를 할 수 있을까? 부서와 사원간에는 이런 식으로 관계를 맺으면 안 된다.

 

위의 모델이 잘못되었다는 것을 증명하려면 모델링에 대한 약간의 배경지식이 필요하다. 지금부터 설명되는 내용은 모델링의 기본이므로 반드시 알고 있어야 하는 것들이다.

 

관계의 방향

엔티티간의 관계선은 Role, Cardinality, 관계속성의null 여부, 그리고 데이터의 전달방향을 나타낸다. 물론 관계선에는 이런 개념 이외에도 중요한 개념이 더 있지만, 오늘은 모델링 책에 자주 언급되고 있지 않는 데이터의 흐름(전달)과 그 방향에 관해 논의해보자.

 

물은 중력의 법칙 때문에 위에서 밑으로 흐른다. 물의 흐름과 데이터의 흐름도 비슷하다. 집합끼리의 관계에 의해서 최상위 부모로부터 최하위 자식까지 데이터는 순차적으로 물 흐르듯 흘러간다. 다시 말해, 데이터의 방향은 부모로부터 자식으로 흘러가는 것이다. 이런 일이 가능한 이유는 관계속성 때문이다.

 

관계속성의 정의

관계선을 긋는 순간 자식엔티티에 부모의 식별자가 상속된다. ER-WIN이나 파워디자이너 등의 모델링 툴을 써본 사람이라면 관계선을 긋는 순간 자식엔티티에 부모식별자에 해당하는 속성이 생성됨을 알 것이다. 이를 관계속성이라 한다. 관계선이 데이터의 이동 통로라고 비유한다면, 관계속성은 이동된 데이터의 도착장소이다. 부모집합은 관계속성이 존재함으로써 자식에 데이터를 전달(연결)할 수 있다. 쉬운 말로 표현하면, 관계속성은 부모식별자의 데이터를 자식의 공간에 집어넣어, 두 집합간의 관계를 완성한다.

 

고객, 상품, 주문, 주문상품을 통하여 데이터가 어떻게 흘러가는지 살펴보자. 아래의 그림에서 FK라고 표시된 것이 관계속성이다. 각 엔티티의 상세한 속성이 더 존재하지만 편의상 몇 개씩만 나타내었다.




우리의 예상대로 고객-->주문 , 주문-->주문상품 , 상품-->주문상품으로 데이터가 흘러감을 직관적으로 알 수 있다. 모델링을 해본 사람이라면 지극히 당연하다고 생각 할 것이다. 관계속성이 존재함으로 데이터가 흘러간다는 규칙은 업무에 무관하게 적용되는 모델링의 기본 법칙이다

 

관계선을 그으면 안 되는 경우

데이터가 전달되지 않을 때는 관계선을 그으면 안 된다. 관계속성이 생성되기 때문이다. 관계속성이 있다는 것은 데이터를 전달하겠다는 의미인데, 데이터가 전달되지 않는 경우에는 관계속성이 필요 없다.

 

 "부모 식별자의 데이터가 자식으로 전달(이행)되는 경우만 관계선을 그어라."

 

위에서 보는 것처럼 이 글에서 언급한 관계선의 규칙은 매우 간단하다. 하지만 아주 쉽고, 당연한 규칙이라도 실무에서는 지켜지지 않는 경우가 많다. 도대체, 언제쯤 대부분의 설계자들이 모델링의 기본규칙을 지키는 날이 올까? 만약 그랬다면 이 글의 서두에서 언급한 대화는 일어나지 않을 것이다.

 

여기까지가 배경지식이다. 지금부터 이미 언급된 부서-사원간의 관계가 왜 잘못된 것인지 알아보자. 결론부터 이야기 하자면 위에서 언급한 관계선의 규칙에서 벗어나기 때문이다.

 

현 소속부서의 함정

내가 본 많은 수의 부서-사원 모델은 관계명에현 소속부서로서가 명시되어 있다. 현 소속부서로서? 현 소속부서라는 것은 논리적으로 존재할 수 없다. 현 소속부서가 존재한다면 그것은 발령의 마지막 값을 물리적으로 역정규화 한 것이다.

 

관계선의 검증방법

관계선은 업무규칙을 나타내기도 하지만 데이터집합의 원천을 표시하는 용도가 있다. 즉 부서와 사원 사이에 관계선을 그렸다면, 사원엔티티의 부서코드 값들은 부서엔티티로부터 온 것인가? 라고 검증해보아야 한다. 실제로 부서와 사원의 관계선을 검증해보면, 사원의 부서코드는 부서집합으로부터 온 것이 아니다.. 인사시스템의 발령에서 사원의 부서코드로 데이터가 insert 혹은 update된다. 인사시스템의 발령이 데이터의 원천이므로 부서와 사원은 아무런 관계가 없다. 따라서 관계선을 삭제해야 하며 아래와 같이 나타내야 한다.

 



위의 그림은 인사시스템의 모델이다. 물론 발령에는 직무코드, 직위코드와 관련된 추가적인 관계가 존재한다. 하지만 부서와 사원에 집중하기 위해 생략하였다. 위의 모델에서는 당연하게도, 부서와 사원은 관계선이 없다. 즉 현 소속부서라는 집합은 인사발령에서 사원 별로 가장 최근 값만 추출한 것이다. 그럼 인사시스템이 아닌 다른 시스템이라면 어떻게 될까? 아래의 모델을 보자.

 

인사시스템이 아닌 타 시스템이라면 인사시스템의 부서와 사원테이블을 1:1로 중복시켜 놓을 것이다. 관계선에 D라고 표시한 것은 추출관계를 나타낸 것이다. 부서코드, 직위코드, 직무코드는 인사시스템의-발령엔티티에서 역정규화 된 것임을 알 수 있다. 가끔 인사시스템에서 부서코드, 직위코드, 직무코드를 미리 역정규화 해놓는 경우도 있다. 미리 역정규화 해놓은 것을 타시스템으로 1:1 Copy 해도 역정규화 되었다는 사실은 변하지 않는다위의 모델을 자세히 보아도 부서와 사원은 아무런 관계가 없음을 알 수 있다.

 

부서와 사원간의 FK는 필요 없다

부서와 사원간에 FK를 생성하려는 사람이 있다. 부서와 사원간에 FK에 의한 참조무결성은 쓸모가 없다. 데이터가 부모에서 자식으로 전달될 때, 올바른 값으로 전달(insert 혹은 update) 되었는지 체크하는 것이 참조무결성이다. 그런데 부서-사원간의 관계에서는 부모인 부서로부터 자식으로 데이터가 이행되지 않는다. 오히려 사원의 자식인 인사발령에서 데이터가 거꾸로 전달된다. 따라서 FK는 의미가 없다.

 

현소속부서라는 집합의 정합성을 검증하려면, 인사시스템의 발령에서 사원 별로 가장 최근 값만 추출하여 정합성을 검증해야 한다. 사원의 부서코드뿐만 아니라 직위코드, 직무코드도 마찬가지 방법으로 검증해야 한다. 실제로도 정합성 체크는 이렇게 하고 있다.

 

조회관계(Read Only Relationship)란 무엇인가?

조회관계란 데이터의 부모자식간의 데이터 전달이 목적이 아니라, 오직 두 집합을 연결하여 조회하려는 목적으로 탄생된 관계이다. 그래서 조회관계를 Read Only Relationship으로 부를 수 있다. 부서와 사원간의 관계 역시 역정규화에 의한 조회관계이다. 오직 사원의 자식인 인사-발령에서 현소속부서라는 데이터가 전달되므로, 부모인 부서집합에서의 전달관계는 없다. 하지만, 사원의 입장에서 현소속부서명과 부서의 위치를 알려고 하면, 부서와 사원을 연결할 수 있어야 조회가 가능하다. 특정 사원의 부서명을 조회하려면 부서와 조인하라.”는 정보가 필요하다. 따라서 조회관계라 하더라도 모델상에 어떤 식으로든 나타내어야 한다.

 
역정규화는 조회관계를 발생시킨다

조회관계는 역정규화를 하는 경우에 나타난다. 예를 들어 영국 프리미엄 리그를 보면, 리그와 팀을 배정해야 경기를 할 수 있다. 만약, 팀 엔티티의 속성에 리그코드가 있다면 그것 또한 역정규화에 의한 조회관계이다. 왜냐하면, 새로운 리그가 시작되기 전에, 지난 리그의 팀 성적에 따라서 각 팀들을 리그에 배정하는 작업이 존재할 것이기 때문이다. (예를 들면, 맨유는 1부 리그에 배정되었다.) 각 팀들을 리그에 배정하는 엔티티는 인사시스템의 발령에 해당한다. 물리모델단계에서 배정 엔티티의 리그코드는 역정규화 되어 팀 엔티티로 들어갈 수 있다. 하지만, 역정규화는 성능과 개발생산성을 위한 작업이므로 개념이나 논리모델에서 보다는 물리설계단계에서 나타내는 것이 적합하다. 

 

전달관계와 조회관계의 차이

일반적으로 우리가 알고 있는 관계는 전달관계이다. 전달관계는 부모식별자의 데이터를 자식에 전달하는 역할과, 두 집합을 조인하여 조회하는 역할을 모두 수행한다. 따라서 전달관계는 조회관계의 기능을 포함한다. 전달관계와 반대로 조회관계는 오직 두 집합을 연결하여 데이터를 조회하는 목적 밖에 없다.

 

조회관계를 어떻게 표현할 것인가

조회관계는 관계속성으로 데이터가 전달되지 않으므로 의미가 없다고 생각 할 수도 있다. 하지만 위에서 설명 한 것처럼 “특정 사원의 부서명을 조회하려면 부서와 조인하라.”는 정보를 인식할 수 있어야 한다. 따라서 아래와 같이 나타내는 것을 권장한다.

 

부서와 사원 사이의 관계선에 P를 표시하여 가상의 관계임을 나타내었다. 가상관계는 관계속성을 만들지 않는다. 또한 관계명에 조회관계라는 것을 명시해주어, 데이터가 부서에서 사원으로 전달되지 않음을 나타내었다. 그리고 부서코드와, 직위코드, 직무코드는 인사시스템의 발령테이블에서 역정규화된 된 속성이라는 것을 속성의 정의란에 나타내 주어야 한다. 회색부분은 외부(External) 엔티티를 나타낸 것이다. 외부엔티티를 사용하여 데이터의 원천을 나타내주면, 개발자가 데이터를 이행할 때 쉽게 참조할 수 있다. 즉 위의 모델을 그려놓으면인사시스템에서 데이터가 바뀌면 내 시스템의 부서와 사원 데이터를 동기화 해야 하겠군하고 명확히 판단할 수 있다.

 

위의 모델을 그림으로써 얻을 수 있는 정보는 세 가지이며, 다음과 같다.

1. 부서와 사원간의 관계는 전달관계가 아니라 조회관계이다.

2. 부서와 사원엔티티는 인사시스템이 원천이다.

3. 부서코드와, 직위코드, 직무코드에 해당하는 데이터는 인사시스템의 발령데이터가 원천이다.

 

또한 위의 세가지 정보로 다음과 같이 두 가지 장점을 얻을 수 있다.

1. 차세대 프로젝트의 데이터를 이행하는 사람은 데이터의 원천이 인사시스템의 부서, 사원, 발령 모델임을 인식하므로 매핑정의서를 쉽게 작성할 수 있다.

2. 개발자는 인사시스템의 부서나 사원, 발령의 데이터가 변경되면, 인사시스템이 아닌 타시스템의 부서와 사원도 동기화 해야 한다는 사실을 쉽게 알 수 있다. 물론, 동기화 프로그램의 작성도 모델을 참조할 수 있으므로 그만 큼 쉬워진다.

 

하지만 현실은……

실무에서는 거의 100% 아래와 같이 모델링 한다.



위의 모델에서는 어떤 정보와 어떤 장점을 얻을 수 있나? 거의 없다. 한가지 얻을 수 있는 것은 부서집합의 부서코드 데이터가 사원에 전달된다는 거짓정보이다. 이 정보에 의해서 사원의 부서코드는 부서로부터 상속되었다는 잘못된 생각을 하는 사람이 있다. 이에 따라 불필요한 FK를 생성하는 사람도 있다. 다시 한번 말하지만 사원의 부서코드는 부서 엔티티로부터 상속된 것이 아니라 인사시스템의 발령엔티티로부터 온 것이다. 

 

조회관계와 전달관계는 확실히 구분하기 바란다. 그렇게 하면, 모든 것이 드러나고 명확해진다.

 

반박의 논리

이 글을 몇몇 모델러에게 보여주었더니 반발이 있었다. 그런데 이상한 것은 대부분 반대입장만 표현하고, 반대의 적절한 이유가 없다는 것이다. 예를 들면, “내가 지금까지 이렇게 사용했어도 문제가 없었다혹은 “~책에 그렇게 하라고 되어있다가 대표적인 이유였다. 그런 것들은 이유가 될 수 없다. 반박을 하려면 이유가 있어야 한다. 이 글의 논리 중에 어느 부분이, 어떻게 잘못되었다고 지적 할 수 있는 능력이 필요하다. 그나마 이유를 댄 사람들은 아래와 같다.

 

전달관계로 표현해도 문제가 발생하지 않는다는 의견에 대해

그렇다. 전달관계로 표현해도 FK만 생성하지 않는다면 성능저하와 같은 문제는 발생하지 않는다. 하지만 프로젝트의 개발과정에서 여러 사람이 불편을 겪을 것이다. 조회관계의 개념을 모른다면 역정규화 되었다는 사실을 인식하기가 어렵다. 그러므로 인사시스템의 발령데이터가 변경되면, 트리거 성으로 타시스템에 동기화 해야 된다는 사실을 발견하는데 시간이 더 걸릴 것이다. 데이터를 이행하기 위한 매핑정의서를 작성할 때도 마찬가지로 어려움이 예상된다. 만약 모델러가 이런 모든 정보들을 안다고 해도 다른 사람들까지 모두 안다고 생각하면 안 된다. 모델은 정확히, 그리고 자세히 표현할수록 여러 사람이 얻는 이익이 많다.

 

우리회사는 발령이 없다는 의견에 대해

영세한 업체라면 발령이라는 엔티티가 없을 것이다. 인정한다. 그런 경우에는 부서-사원은 1:N로 직접적인 관계가 있으므로 관계선을 그어야 한다. 하지만 인사발령이 없는 영세한 업체라면 SI 프로젝트를 하지도 않을 것이며, 설계자나 모델러를 쓰지도 않을 것이다. 따라서 대부분의 경우 부서-사원의 현소속부서 전달관계가 존재한다면 잘못된 것이다.

 

우리 시스템에는 발령이 필요 없다는 의견에 대해

맞는 말이다. 인사시스템을 제외하면, 발령이라는 엔티티는 필요 없을 것이다. 하지만, 그 이유로 부서-사원간에 관계선을 긋는 것은 말이 안 된다. 발령이 필요 없다는 이유로, 존재하지도 않는 부서-사원간의 전달관계와 그에 따른 관계속성을 만든다는 것은 적절하지 않다. 사원의 부서코드는 전달관계속성이 아니라 역정규화된 추출속성이라는 엄연한 진실을 가리는 것이다.

 

모델링툴에서 조회관계를 표현하지 못한다는 의견에 대해

가장 그럴 듯한 반박논리다. 조회관계를 표현하는 기능이 없으므로 전달관계선을 그어야 한다는 것이다. 모델링툴 때문에 전달관계선을 긋는 것 보다는, 관계를 맺지 말고 누구나 볼 수 있도록 코멘트(Text Box)로 조회관계임을 기술하는 것이 더 나아 보인다. 어쩔 수 없이 전달관계선을 그을 수도 있다. 하지만, 개념을 알고 행동하는 것과 개념을 모르고 행동하는 것은 큰 차이가 있다. 다시 말해, 조회관계란 개념을 알고 있지만, 모델링툴에 기능이 없기 때문에 눈물을 머금고 관계선을 그은 것이라면 정당한 이유가 된다. 만약 그것이 아니라 부서-사원의 관계를 습관적으로 그은 것이거나 부서-사원의 관계가 1:N이라고 잘못 인식하고 관계를 맺었다면 옳지 않은 행동을 한 것이다.

 

현재 가장 대중적으로 사용하고 있는 ER-WIN이나 파워디자이너 같은 모델링툴은 조회관계를 나타낼 수 없다. 툴의 설계자가 조회관계라는 개념을 것을 모르니 당연한 것이다. 따라서 관계를 맺으면 무조건 전달관계와 전달관계속성을 만들어 버린다. 그에 따라 툴에서 제공하는 테이블 생성용 스크립트를 받아보면 예외 없이 FK를 생성해 버린다. 부모로부터 데이터가 전달되지 않으므로 참조무결성을 보장할 필요가 없는 데이터임에도 쓸데없이 FK를 생성하여 속도만 저하시킨다.

 

조회관계를 표현하는데 가장 유리한 모델링툴은 DA#이다. 필자가 가장 애용하는 툴이기도 하다. 이 툴에서 가상관계를 이용하면 관계속성을 만들지 않는다. 물론 가상관계가 아니라 조회관계를 표현할 수 있으면 좋겠지만, 그런 기능은 없으므로 현재로써는 가상관계로 처리하는 것이 최선이다. 위에서 그린 ERDDA#으로 표현한 것이다.

 

관계의 분류

관계는 여러 가지로 분류할 수 있다. 흔히 통용되는 관계분류의 예제는, 직접/간접 관계와 식별/비식별 관계이다. 직접관계란 1촌끼리의 관계를 의미한다. 즉 나와 아버지의 관계(1)이다. 관계형 데이터 모델에서는 직접관계만 표현하면 된다. 하지만 가끔 물리설계시에 SQL의 성능을 높이기 위해 할아버지가 직접 손자와 관계를 맺는 일도 있다. 이를 간접관계라고 한다. 식별관계는 부모의 식별자가 자식식별자의 일부로 상속되는 경우이다. 이와는 반대로 비식별관계는 부모의 식별자가 자식에 일반속성으로 상속된다. 이상으로 일반적인 관계의 분류방법을 알아보았다.

 

이 글에서 언급한 관계분류방법은 일반적으로 통용되는 분류방법과 다르다. 분류의 초점을 데이터의 전달유무에 맞추었다. 즉 전달/조회 관계로 새롭게 분류해 보았다. 직접/간접관계 분류법과 전달/조회관계 분류법은 많이 다르지만, 관계의 역정규화라는 점에서는 유사하다. 하지만, 데이터의 전달유무에서는 차이가 있다. 간접관계는 부모식별자의 데이터가 직접 자식으로 상속되는 전달관계이다. 이와는 반대로 조회관계는 부모로부터 데이터를 받지 않는다.  

개념이나 논리모델에서 조회관계 표현방법
될 수 있으면, 역정규화된 관계는 개념/논리모델단계에서 나타내지 말고, 물리설계단계에서 나타내기 바란다. 물리설계단계에서는 조회관계임을 명시하거나, 간접관계임을 나타내어 관계가 역정규화 되었음을 나타낼 수 있다. '그럼 개념이나 논리에서는 어떻게 나타내야 하는가?' 라고 질문할 수 있다. 개념모델단계에서는 부서와 사원 사이의 관계를 M:N으로 나타내면 되고, 논리모델단계라면 M:N 관계를 풀어서 외부엔티티인 인사발령을 표현해주면 된다. 논리모델에서 외부엔티티를 사용하는 것은, 물리모델링시에 조회관계(역정규화)로 나타낼 것임을 예고하는 것이다.  ( 2011.02.28 추가 )

 

결론

조회관계는 부모로부터 자식으로 데이터를 전달하지 못하고, 오직 조회를 목적으로 두 집합을 연결한다.

전달관계는 부모로부터 자식으로 데이터를 전달하고, 조회를 목적으로 두 집합을 연결도 한다.

부모로부터 자식으로 데이터가 이행되는 경우만 전달관계선을 그어야 한다.

부모로부터 자식으로 데이터의 전달이 끊기면 조회관계이다.

조회관계는 관계속성이나 FK를 생성하지 않는다.

역정규화에 의해서 조회관계가 발생된다.

부서--<사원은 조회관계이다.

저작자 표시 비영리 동일 조건 변경 허락
신고
Posted by extremedb

댓글을 달아 주세요

  1. Favicon of http://rainlethe.tistory.com BlogIcon 레인레테 2011.02.25 06:01 신고  댓글주소  수정/삭제  댓글쓰기

    FK에 대한 새로운 관점이군요. 찬찬히 생각을 해봐야 할 듯 합니다..

  2. 어느모델러 2011.02.25 08:23 신고  댓글주소  수정/삭제  댓글쓰기

    부끄럽습니다. 관계선을 그었던 1인
    동규님의 글은 공통점이 있네요.
    서론, 본론, 결론에 따라 내 생각이 아래처럼 바뀝니다.
    항상 이런 패턴으로 글을 쓰시는것 같습니다.

    서론 : 말도 안되는 글인데 어떤 이야기를 하는지 들어보자.
    본문 : 일리가 있는 글이군
    결론 : 앞으로 그렇게 하면 안되겠네

    감사합니다.

  3. 혈기린 2011.02.25 10:46 신고  댓글주소  수정/삭제  댓글쓰기

    김기창 수석님 블로그에서 댓글로 언급하셨던 내용에 대한 상세한 설명이시네요
    그 댓글보고 엔티티에 대해서 아무 생각없이 당연한 관계라고했던 것에 대한 생각을 고치는 계기가 되었네요 좋은글 계속 감사 드립니다 ~~
    모델링에 대한 글도 자주좀 올려주세요 눈이 번쩍 뜨이게 ㅎㅎ

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2011.02.25 11:06 신고  댓글주소  수정/삭제

      네 김수석님 블로그에 제가 이와 관련된 댓글을 작성한 적이 있습니다. 제가 링크를 함부로 달지 않는데, 김수석님 블로그는 너무 좋은 내용이 많아서 안달수가 없었습니다.
      그럼 수고하세요

  4. Favicon of http://xenerdo.com BlogIcon 제너시스템즈 2011.02.25 11:29 신고  댓글주소  수정/삭제  댓글쓰기

    엄청 어려운 일이네요. 쭈욱 두 번 읽어봤는데 사실 제 머리로는 아직 이해가 안가는 부분이 많아요^^;;; 다시 한번 더 읽어봐야겠어요^^

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2011.02.25 16:55 신고  댓글주소  수정/삭제

      두번 읽으셨는데 이해가 안가신다면 그건 제 잘못입니다.^^
      어느 부분이 이해가 안가시는지 설명해 주시면 설명을 추가하도록 하겠습니다.

      이 글의 핵심은 부모의 식별자 값이 자식의 관계속성으로 들어오면(insert 되면) 전달관계이고 들어오지 않으면 조회관계라는 것입니다. 부서(부모) 사원(자식)의 경우는 데이터가 부모로 부터 오는것이 아나라 오히려 자식격인 인사시스템의 발령에서 오는 것입니다. 따라서 조회관계라고 할 수 있습니다.

  5. 혈기린 2011.02.25 13:36 신고  댓글주소  수정/삭제  댓글쓰기

    한가지 궁금한게 있는데요 조회관계 전달관계의 명칭이 관계형 이론에 있는 관계의 종류인가요 아니면 오수석님께서 임의로 만든 용어인가요?
    몇권은 안되지만 제가 읽은 모델링 서적에는 없는 용어이기에 질문을 드립니다.
    모델러 마다 엔티티의 분류도 조금 다르더군요

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2011.02.25 13:58 신고  댓글주소  수정/삭제

      전달관계/조회관계는 제가 만든것 입니다. 용어를 만들었다기 보다는 개념을 만든것 입니다.

      EF 코드 박사가 제시한 관계형이론에는 조회/전달 관계라는 용어는 없습니다. 이 용어뿐만 아니라 직접/간접 관계라는 용어도 없습니다. 관계형이론은 아주 짧은 글이므로 그 이론이 만들어진 후에 여러 개인(전문가)이 추가적인 용어를 만든것 입니다. 혈기린님의 말에 의하면 관계형이론에 없는 것은 모두 임의로 만든거라 할 수 있습니다.^^ 하지만 임의로 만든것이라도 그것이 옳은 개념이고, 그 개념으로 더 나은 모델을 만들수 있다면 더 이상 개인적이고 임의적이라고 볼 수는 없겠지요. E사의 이화식 사장님과 오픈메이드의 김기창 수석도 마찬가지로 이론에 없는 여러가지 용어를 사용하고 있습니다.

      전달관계, 조회관계라는 용어는 제가 최초로 만든것이므로 그 어떤 모델링책에도 언급되지 않습니다. 부서-사원같은 잘못된 모델을 그대로 두고 볼 수 없기에 만든것 입니다. 하지만, 앞으로 나올 모델링 책에서는 이 개념을 사용하여 실수를 방지하는 것이 좋을 것입니다.

  6. finecomp 2011.03.01 01:38 신고  댓글주소  수정/삭제  댓글쓰기

    항상 좋은 글 감사합니다...;
    전달관계/조회관계...개념만큼이나 선택하신 용어도 맘에 딱 듭니다...^^;

    저는 이전부터 잘못된 원인을 약간 다른 관점에서 보고 있었습니다.
    부서-사원의 관계를 전달관계로 설정한 후, 필요에 의해 사원의 소속조직 이력관리의 개념으로 발령엔티티가 나타났고,
    발령을 관계맺은 후에도 처음 맺어졌던 부서-사원관계를 그대로 두었고,
    이 후 이런 모델을 만들었던 노련한 모델러 일수록 습관적으로 관계선을 그렸고 이것이 굳어져 현재는 오류로 보이는 듯 합니다.

    이와 유사하게 아직은 담당관리센터에 대한 이력관리가 필요치 않은 고객-센터(관리센터)의 예와 비슷할 듯 합니다.
    고객과 센터가 처음에는 전달관계로 설정되겠지요.
    이 후 필요에 의해 고객의 관리센터이력을 추가해야 할 필요성이 생기는 순간, 부서-사원과 동일한 문제가 발생하게 되겠지요.

    즉, 많은 경우의 전달관계에 대해서, 이런 유형의 이력이 생기는 순간 원래의 관계가 현행화 될 필요가 생길 수 있을 것 같습니다.

    결론적으로, 최초설계/변경 시에 좀 더 신중히 관계를 검토하고 조회관계 시엔 코멘트라도 잘 달아야겠다는 반성이 생기는군요...;

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2011.03.02 09:08 신고  댓글주소  수정/삭제

      저도 몇년 전 까지는 이런 오류를 범했습니다.
      그런데 finecomp님은 이미 잘못된 원인을 다른 관점에서 보고 있었네요. 그리고 개념과 용어가 마음에 드신다니 다행입니다.

  7. 에너자이져 2011.03.10 19:31 신고  댓글주소  수정/삭제  댓글쓰기

    전달관계=직접관계, 조회관계=간접관계 의 등식이 성립되는거 같습니다.
    직접/간접관계 보다는 전달/조회관계 라는 용어가 이해를 빠르게 하네요.

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2011.03.11 10:07 신고  댓글주소  수정/삭제

      이해가 빠르다고 하시니 다행입니다.
      전달관계=직접관계, 조회관계=간접관계가 성립하듯이
      직접관계=전달관계, 간접관계=전달관계도 성립합니다.
      감사합니다.

  8. 김상래 2011.06.12 11:42 신고  댓글주소  수정/삭제  댓글쓰기

    안녕하세요. 데이터 모델링을 하면서 항상 관계(Relationship)에 대해 많이 고민하게 되는데,
    상당히 의미있고 중요한 부분을 집어내어서 명확히 풀어내신것 같습니다. 저도 개인적으로
    머리속에 두서없이 비슷한 생각을 하고 있었는데 오늘 이 글을 통해 어느정도 자연스레 정리된 느낌이네요.

    저는 저의 언어로 이렇게 정리를 했습니다. 확인차 몇가지 질문도 함께 드려봅니다

    1. 저는 관계를 크게 엔터티간에 종속적인 관계와 참조적인 관계가 있다라고 보는데,
    블로그에 쓰신 전달관계와 조회관계라는 용어의 맥락에서 볼때
    종속적인 관계는 분명 부모 식별자의 데이터가 (관계속성으로) 자식으로 전달되니
    부모 자식의 관계가 명확한 종속적인 관계 = 전달관계로, 당연히 관계선이 표현되어야 하고
    RI 제약 생성의 대상이 된다고 볼수 있습니다.
    즉, 종속관계는 전달관계로 관계가 표현되어야 한다는 것은 의심의 여지가 없습니다.

    2. 문제는 조회관계와 참조관계인데.. 종속관계가 아닌 참조관계는 블로그에 언급된
    전달관계가 있는 참조관계와 단순 조회관계인 참조관계를 포함하는 개념으로 보아야 할것 같습니다.
    예를들어 고객의 직업에 있어서, 직업코드와 직업명을 관리하는 직업엔터티가 코드성으로 존재할 때
    고객 엔터티에 직업코드가 일반 속성으로 존재한다면 이 경우는 전달관계가 있는 일반적인 참조관계로 보고
    RI를 적용하여 FK Constraint를 생성하여 직업엔터티에 존재하는 직업코드만이
    고객의 직업코드로 들어올수 있게 한다는 개념으로 정리해도 될까요?

    3. 그렇다면 블로그에 언급하신 조회관계는 모두 역정규화에 의해 발생한 속성으로부터 발생된 것을
    의미하는 것인가요? 이 글에서 언급하신 사원의 부서코드와 같이 외부시스템(외부테이블)과의 관계에 의해
    나온, 즉 데이터 중복인 역정규화 속성으로 발생한 참조관계인 경우,
    이를 조회관계라는 용어로 언급하신게 맞는지 궁금합니다.

    정리하면 블로그에서 언급된 전달관계/조회관계라는 용어와 제가 생각하는
    종속관계/참조관계 용어의 추상화정도와 관점을 맞춘다고 봤을 때, 아래와 같이 귀결시킬수 있을지 의견 부탁드려봅니다.

    전달관계 : 종속관계 전체 및 역정규화된 속성에 의해 발생하는 단순 조회관계를 제외한
    참조관계(일반적인 Non-identifying)까지를 포함
    조회관계 : 외부테이블과의 관계에 의해 발생한 속성에 따른 참조관계로, Relationship을 관리할 필요가 없다.
    다만 Join의 대상이 되므로 모델링 툴에서 표현을 할 필요는 있다.

    아.. 그런데 이렇게 글로 정리하다보니 제가 생각하는 참조관계란 결국 애매하게 조회관계까지를 포함하는
    개념이었듯 한데, 엄밀하게 정리를 하자면 전달관계가 있는 종속관계와 전달관계가 있는 참조관계, 그리고 전달관계가 없는
    반정규화에 따른 조회관계가 남게 되는군요...

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2013.05.31 18:24 신고  댓글주소  수정/삭제

      2년이 지났지만 답변드리겠습니다.
      전달관계는 식별관계이든 아니든 상관없이 상위 엔티티의 식별자가
      하위 엔티티의 속성으로 상속되는 것입니다.
      위 쪽의 고객과 주문간의 관계는 일반 속성으로 상속되었으므로 비식별 관계이면서 전달 관계입니다. 이와는 반대로 상품과 주문상품의 관계는 식별자로 상속되었으므로 식별 관계이면서 전달 관계입니다.
      조회관계는 외부테이블 이든 아니든 역정규화에 의한 관계입니다. 위의 예제에서는 max(마지막) 값을 가져다 놓은 것을 언급했습니다.
      감사합니다.

  9. 김시준 2013.05.31 12:24 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 글 감사드립니다.

  10. 주지영 2013.09.26 17:32 신고  댓글주소  수정/삭제  댓글쓰기

    모델링 접한지 얼마안된 DB관련경력자입니다. 책을 보면서도 속시원하게 뭔가 풀리지 않고 답답한 구석이 있었는데 ...
    심지어 모델링 공부하는 사람에게 물어봐도 '이건 당연하게 사용했던거라...'라는 답변을 꼭 들어야만 했습니다.
    앞으로 종종 들러 많은 가르침 배우고 싶네요.
    감사합니다(__)~

  11. Favicon of http://oracledo.tistory.com BlogIcon #도상 2016.11.28 10:01 신고  댓글주소  수정/삭제  댓글쓰기

    잘 읽었습니다. 관계선에 대해 다시 한번 생각하고 정리 할 수 있는 기회였습니다.

-개발 방법론 VS 모델링 방법론

최근에 어느 모델러에게 “~개발 방법론 때문에 미치겠다.”는 말을 들었다. SI 프로젝트에 참여하면 여러 가지 방법론이 등장한다. 하지만 그것들은 개발 방법론 혹은 관리방법론이기 때문에 분석과 사고, 그리고 통찰이 중요한 모델링을 수행하는 데는 오히려 방해가 되는 경우가 있다. 왜냐하면 이런 방법론들은 복잡한 건물을 세우는 방법, 건물 내부를 채우는 방법, 그리고 최소의 인력으로 최소의 시간을 들여 프로젝트를 마치는 방법에 관심이 집중되어 있기 때문이다. 이런 방법들은 프로젝트의 일정이 진행되는 입장에서 보면 관리적이며 과학적인 방법이다. 하지만 데이터 모델링에는 그런 과학적 방법이 얼마나 보탬이 될지는 미지수 이다.

 

데이터의 품질은 어떻게 보장되나?

많은 이들이 데이터의 품질에 목을 매고 있지만 개발 방법론에 따라 데이터 품질이 좌지우지 되는 것은 아니다. 왜냐하면 많은 경우에 데이터의 품질이란 데이터 모델의 품질에 따라 좌우된다. 그러므로 각종 개발 방법론들이 데이터 모델의 품질을 향상시키는 쪽으로 방법론이 진화되었으면 한다. 진화가 그리 어려운 것은 아니다.

 

진화방법 1

모델러는 일이 어떻게 진행될지 명확히 설명해야 하며, 산출물을 제대로 작성하고, 일정을 지켜야 한다. 모델러들 또한 고집이 있다. 전문가이기 때문에 개발자에게 배우지 않으려는 특징이 있다. 개발자들에게 UML을 배울 수 있다면 배워야 한다. 항상 모델러가 선생님이 되어야 하는 것이 아니다. UML과 모델링 툴의 사용법과 표기법이 다르다고 해서 그 사상이나 엔티티의 내부 구조가 달라지는 것이 아니다. 그러므로 모델러들도 특정 모델링 Tool에만 집착하면 안되며 UML로 모델링 할 수 있는 능력을 키워야 한다. 다른 Tool이 있음에도 내가 사용법을 모른다고 해서 고객이 몇 백만 원 혹은 몇 천만 원 하는 특정 Tool을 사야만 할까?

 

진화방법 2

반면에 단위 일정 내에서는 모델러에게 특정 방법을 강요해서는 안 된다. 즉 인정 될 수 있는 범위 내에서는 자유를 주어야 한다는 이야기이다. 왜냐하면 그들만의 방법이 따로 있기 때문이다. 화가가 그림(ERD)을 그리는데 특정 방법론을 강요할 수 있을까? 또한 장인이 작품(모델)을 만드는데 대규모 공장의 논리를 강요할 수 있을까 


강요와 허용의 차이

많은 경우에 모델러를 PL(관리자)로 생각하기 때문에 강요가 발생한다. 모델러는 PM이나 PL이 아니다. 물론 한사람이 다하는 곳도 있지만 그것은 예외이다. 그림을 그리는 방법에 대해서는 완전한 자유를 주어야 한다. 이와 반대로 구조물의 단순성 및 명료성 자체를 논하는 과정(철학적 과정)이 아니라, 복잡한 구조물을 세울 때(개발진행과정)에는 과학적 개발 방법이 꼭 필요하다. 즉 모델러에게 프로젝트의 관리 및 개발방법을 무시해도 된다는 특권을 주라는 이야기가 아니다. 다만 그들에게 좋은 그림을 그릴 수 있도록 자유로운 사고와 통찰을 허하라. 그렇게 해야만 데이터 모델이 단순해지고 명료해진다. 그리고 무엇보다 중요한 데이터의 품질이 향상된다.

 

이런 생각을 하는 사람은 나뿐인가? 만약 그렇다면 100년 전에 생존했던 어느 철학자가 필자의 의견을 지지했던 것처럼 보이는 이유는 무엇일까?

우리 문명은 '진보'라는 단어를 특징으로 한다. 진보는 우리 문명이 가진 특징 가운데 하나이기보다는 그것의 형태이다. 전형적으로 볼 때 그것은 쌓아 올리는 것이다. 그것은 점점 더 복잡해지는 구조물을 세우는 데 매몰되어 있다. 또 명료성 조차도 목적 자체가 아니라 이 목적을 이루기 위한 수단으로서 추구된다. 내게는 그와 반대로 명료성, 명석성이 그 자체로서 귀중하다.

 

나는 건물을 쌓는 데는 관심이 없고, 지을 수 있는 건물의 기초를 명료하게 보는 데에 관심이 있다.

 

그러니 내가 겨누는 과녁은 과학자들의 것과 같지 않으며, 나의 사고방식은 그들의 것과 다르다.

 

-비트겐슈타인 <철학적 언급들>의 서문 초고

저작자 표시 비영리 동일 조건 변경 허락
신고
Posted by extremedb

댓글을 달아 주세요

  1. Favicon of http://blog.naver.com/bluepupils BlogIcon 김기창 2011.01.27 11:55 신고  댓글주소  수정/삭제  댓글쓰기

    SQL이나 플랜이 없는 글을 보니 신선하네요.. ㅎㅎ

    언젠가는 모델링 방법론이 녹아 있는 개발 방법론을 볼 수 있을 거라 생각해요. 언젠가는...
    더 바라기는 개발 방법론이 녹아 있는 모델링 방법론이 지배했으면 하는데 쉽지 않을 거 같고...

    오동규님의 주장대로 철학적 과정을 정형화한다는 건 모순이 있죠.

    사실 모델링 과정보다는 결과를 검증하는 활동에 초점을 맞춰야 되는데, 관리하는 입장에서는 답답하다 보니 방해가 되는 방법들을 강요하는 거 같아요. 좋은 의도에서 과정을 간섭하는데 그게 결과에 나쁜 영향을 미치는 게 문제죠.

    현재로서는 모델러가 내공을 쌓을 수 밖에 없는 거 같아요.
    그래야 간섭도 줄어들고, 간섭 받고도 일정 수준 이상의 결과를 낼 수 있을 거에요.

    그 내공 중에 커다란 부분은 진화입니다. 끊임없는 진화... 정체돼 있는 것이 아닌... 정지하고 있는 사람은 뭐든 시작해야 합니다... ㅎㅎ

    진화라는 단어도 다시 생각하게 하는 좋은 글이네요.

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2011.01.27 15:52 신고  댓글주소  수정/삭제

      김 수석님 오랜만 입니다.
      실행계획이 없는 글은 많지 않은데 용케 찾아 내셨네요.^^
      진화는 중요한 항목임에 동의합니다. 개발방법론도 언젠가는 진화 되겠죠?


-변경이력에서 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에서 테스트를 진행 하였다.


실습을 위한 object 생성.SQL

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


 

테이블의 건수

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

댓글을 달아 주세요

  1. 선분이력 2010.12.03 18:20 신고  댓글주소  수정/삭제  댓글쓰기

    "결과적으로 표의 결과는 간발의 차이로 ‘나쁨’이 2개인 시작일자만 관리하자는 측의 승리다"???

    < 의문점 1 >
    중간 시점의 값 조회시 성능 --> 중간
    오래된 시점의 값 조회시 성능 --> 나쁨

    시작일자, 종료일자를 둘 다 관리하더라도 시작일자만 관리할 때랑 똑같은 방식으로 이력을 조회할 수 있고, 그렇다면 중간 시점이나 오래된 시점을 조회할 때 성능이 나쁘지 않습니다. 앞서 말씀드린 사항이고, 오동규님도 그 점을 인정하셨는데 다시 나쁘다고 평가하셨군요.

    < 의문점 2 >
    정합성(데이터 품질)을 보장하는가? --> 나쁨
    추가적인 비용(노력)이 얼마나 드는가? --> 나쁨

    "추가적인 비용(노력)"이라고 한 부분이 "정합성을 보장"하기 위한 비용(노력)이므로 이 둘을 따로 떼어서 나쁨을 2개로 만드신 것은 공평하지 않게 느껴집니다.

    < 결론 >
    위 2가지 사항을 보정하고 나면 아래와 같이 역전되는 걸요.

    시작일자만 관리의 "나쁨"은 2개
    종료일자도 관리의 "나쁨"은 1개

    그러면 간발의 차이로 ‘나쁨’이 1개인 "종료일자도 관리하자"는 측의 승리인가요? ㅎㅎ
    암튼 수고하셨고, 잘 읽었습니다. 판단은 독자들의 몫으로...

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.12.05 04:31 신고  댓글주소  수정/삭제

      선분이력님 반갑습니다.

      <의문점1>
      제가 인정하지 않았으면 단점이 되고 인정했다면 단점이 안되는 것은 아무래도 이상한 것 같습니다. 제가 대안(튜닝방법)을 인정했다고 해서 종료일자+시작일자 인덱스를 이용하여 between으로 사용하는 방법의 단점이 없어지는것은 아닙니다.

      제가 이글의 마지막 부분에서도 말씀드렸지만 튜닝을 하면 미래에 성능을 향상시킬 수 있습니다만 그러면 단점을 가리는 격이 됩니다. 현실도 그게 아니라는 것을 증명합니다.
      1. 같은 화면에서 인덱스를 시점에 따라 동적으로 바꿔서 사용하는 튜너는 0%에 가까울 것입니다.
      2.현실에서는 중간시점과 오래된 시점을 조회할 목적으로 화면을 따로 만들지 않습니다. 종료일자가 pk 인덱스에 포함된 경우는 두번째 글에서 제가 제시한 rowid를 활용한 SQL을 사용하지 않고 그냥 종료일자와 시작일자로 between을 사용합니다. 이것이 현실입니다.

      제가 처음으로 제시한 방법을 많은 사람들이 사용하고 있을 가능성은 별로 없을것 입니다. 앞으로 많은 사람들이 적용하길 기대합니다.

      <의문점2>
      "데이터의 품질을 보장하는가?"는 선분이력님이 말씀하신 것 입니다. 즉 벤더사에 개발해 달라고 부탁 하셨던 기능(Constraint)입니다. 데이터의 정합성을 100% 보장할 수 있는 기능입니다. 종료일자를 key로 사용했다면 이런 기능은 없습니다.

      두번째는 추가적인 코딩량 증가(개발생산성 감소)입니다. "추가적인 노력이 얼마나 드는가?"는 일반적인 거래나 업무처리의 목적으로 만든프로그램 이외에 추가적으로 개발해야 하는 노력(코딩량)을 나타낸 것입니다. 즉 종료일자를 사용하면 추가적인 노력이 들므로 개발생산성이 떨어진다는 것입니다.

      Constraint 존재유무와 추가적인 코딩량 증가(개발생산성 감소)는 전혀 다른것 입니다. 공정하지 않다고 하셨지만 만약 추가적인 노력을 뺀다면 개발생산성을 나타내는 'SQL의 복잡성' 항목도 같이 빠져야 합니다. 그래야 공정할 것입니다. 공정하게 같이 뺀다면 2:1로 시작일자만 관리한다는 측의 승리 입니다.

      의문이 풀리셨으면 합니다.

      선분이력님이 공정하지 않다고 하셨지만 양측의 의견을 약 일주일간 조율 했습니다. 시작일자측에게 더 혹독하게 조율했습니다. 그래서 시작일자만 관리하자는 측의 불만이 더 많습니다. 제가 공정한지 아닌지 모든 것은 독자들이 판단하실 것 입니다.


      ----------------------------------------------------------------------------------------------------
      답변은 끝났습니다. 제가 궁금한 것이 있어 선분이력님 뿐만 아니라 모든 분들께 질문을 드려 봅니다.

      이상한 것은 1부와 2부에서 인덱스를 사용하는 경우에 대하여 논의했는데 그 결과를 종료일자를 사용하자는 측에서는 인정하려 들지 않는다는 것입니다. 왜그런지 모르겠습니다.

      "종료일자 + 시작일자 인덱스를 between으로 사용하는 것은 시작일자만 사용하는것 보다 빠르지 않고 오히려 특정 시점에서는 느리다."를 종료일자를 관리하자는 측에서 아무도 인정하지 않더군요. 그래서 내가 다시 물어 보았습니다. 증명이 되지 않았냐고 말입니다. 답변은 증명이 된게 아니라고 하더군요. 인정하지 않는 이유는 아래의 아래의 3개 였습니다.

      1."인덱스 힌트를 동적(Dynamic SQL)으로 바꾸어 종료일자 + 시작일자 대신에 시작일자 + 종료일자 인덱스를 사용하면 되지"
      2."오래된 시점은 화면을 따로 개발하면 되지"
      3."오동규님이 제시하신 SQL을 사용하면 되지"

      위의 세가지 이유로 인정하지 않는것은 논리적인 오류입니다. 즉 종료일자를 between으로 사용하는것이 느리다고 증명(인정)이 되었으므로 위의 3가지 방법을 사용하려는 것입니다.

      이 현상은 종료일자를 지지하는 측에서만 나타납니다. 시작일자만 관리하자는 측에서는 시작일자 인덱스의 단점 두가지를 인정합니다.
      "배치에서는 확실히 느려지는 군"
      "betweeb을 사용할 때보다 복잡해 보이는 군"

      블로그의 독자, 이메일로 의견을 주고 받은분들, 그리고 사내의 컨설턴트들에 이르기 까지 이런 현상은 광범위 하게 나타납니다. 혹시 왜 이런 현상이 나타나는지 이유를 아시는 분이 있으면 저에게 알려주시기 바랍니다. 심리학을 아시거나 전공하신 분이 있으면 좋겠습니다. 그래야 정확히 원인을 밝히고 다음번 글을 쓸때 더 공감을 받을 수 있겠죠.

      제가 이런 질문을 드리는 이유는 이 글을 쓰며 느꼈던 점 때문입니다. "종료일자의 장점이 없다는 것을 증명을 하면 할 수록 종료일자를 선호하던 사람들은 더욱 종료일자를 사랑하게 된다." 는 이상한 현상 때문입니다.

      감사합니다.

  2. 2010.12.10 11:38  댓글주소  수정/삭제  댓글쓰기

    비밀댓글입니다

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.12.10 14:26 신고  댓글주소  수정/삭제

      그런 것이 있었네요. 이 상황과 거의 흡사합니다.
      좋은 정보 감사합니다.

    • Ejql 2011.01.17 16:28 신고  댓글주소  수정/삭제

      심리는 모르겠지만. 종말론을 믿는 종교 얘기를 하나 하겠습니다. 예전에 1999년 종말론자들은. 1999년 12월 31일에 어느 빌등 옥상에 모두 모였습니다. 종말이 다가오니 신께서 우리를 데려가신다는 아주 확고한 믿음으로 인해서요.
      그러나. 1999년 12월말... 밤12시가.. 지나고. 아침이 되었습니다. 당연히 종말은 없었고 사회이슈가 되었던 종말예언은 가짜로 판명이 난것이 확실했습니다. 그러나. 누구하나 종말론자들의 교주한테 달려드는 사람이 없었습니다. 오히려.. 그때 상황을 본사람들의 얘기를 듣자면. 더 확고히 종말론을 믿고 내려왔다는 것입니다.

      나중에 심리학자가 내 놓은 답은 " 종말이 오지 않은 현실을 알았지만. 심리적으로 그것을 인정하게되면 자기의 자아(?)가 붕괴되고 사회적지위가 떨어지기 때문에 더욱 집합을하고 종말론의 끈을 놓지않기 위해서 이유를 찾고 심리적으로 그렇게 되었다"라고 합니다.

      물론 비교대상은 아니지만, 그러한 비슷한 심리가 아닌가 합니다.

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2011.01.17 17:55 신고  댓글주소  수정/삭제

      반갑습니다.
      결국 자기 만족을 위한 것이군요.
      결국 관성의 법칙과 비슷하구요.
      생각해보니 자연스러운 현상인듯 합니다.
      저도 그런적이 있습니다.
      좋은 말씀 감사드립니다.

  3. Favicon of http://blog.naver.com/bluepupils BlogIcon 김기창 2011.01.27 22:10 신고  댓글주소  수정/삭제  댓글쓰기

    논쟁이 어느 정도는 끝난 거 같네요. 열정이 부럽네요…

    모든 사람의 관점을 만족시키기는 힘들지만 선분이력에 대해 전문적으로 접근했다고 생각해요.
    위의 표가 판단의 중요한 가이드가 될 거 같아요.
    아마 무턱대고 종료일자를 사용하는 경향은 줄어들 거 같습니다.

    제 책에도 이력 엔터티의 성능을 간단하게 설명했는데요. 성능을 논할 위치는 아니라서 일반적인 내용으로요...
    하지만 또 다른 중요한 포인트는 이력 데이터에 대한 정의라서 제 블로그는 아니지만 간단하게 글을 쓰려고요.

    내역과 이력을 어느 정도 구분해야 되는데 이걸 혼동해서 엔터티의 성격이 애매해지는 경우가 많습니다.
    물론 진짜 애매한 경우가 있어서 이력 관리가 힘든데요. 오래 연구하고 있는 분야인데 어려운 거 같습니다.

    기본적으로 발생해서 그냥 쌓이는 데이터는 내역으로 정의해야 되고요.
    이미 쌓여 있는 데이터가 변경돼야 이력 데이터라고 생각합니다.

    예를 들어 주문내역 같이 그냥 발생한 데이터에 종료일자를 사용하면 안 됩니다.
    종료일자 없이 시작일자 대신 발생일자, 생성일자 등으로 사용하는 게 명확하고요.

    발생과 변경의 판단 기준은 해당 엔터티입니다. 해당 엔터티가 [주문]이면 주문한 게 변경돼야 이력 데이터고요. 고객이 상품을 바꿔서 주문한 것이라 확대하면 안 되고요. 이는 식별자가 기준이라는 얘기와 같습니다. 식별자에 종속된 데이터가 변경되느냐로 판단하면 됩니다.

    제 책에도 각종 종속성을 많이 설명했는데, 결국 모델링은 종속성을 따지는 작업이라 생각됩니다.

    이력은 어쨌든 글로 설명하기 힘든데요(말로도 잘 설명이 안 됨).
    성능 문제는 요건에 따라, 환경이나 전략에 따라 판단해야 하는 중요한 문제고요.
    이력 데이터를 정의하는 것은 그 자체로 중요한 문제라고 생각해서 간략하게 적었습니다.

  4. Favicon of http://www.yousungc.pe.kr BlogIcon 정유성 2012.01.02 13:22 신고  댓글주소  수정/삭제  댓글쓰기

    잘 보았습니다. 많은 도움이 되었어요.
    감사합니다 ^^


-
변경이력에서 여러 건을 조회 할 때의 해결방법

-변경이력 조회시 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들도 결국은 몇 개의 패턴으로 정리되지 않을까 생각해본다.  


신고
Posted by extremedb

댓글을 달아 주세요

  1. 선분이력 2010.11.24 11:45 신고  댓글주소  수정/삭제  댓글쓰기

    TYPE 까지 써 가며 새로운 해법을 찾으려고 노력하셨지만, 선분이력의 장점이 오히려 부각된 거 같습니다. 만약 종료일자(end_dt)를 시작일자(start_dt)보다 앞에 두었는데 오래된 과거 데이터를 조회해야 할 일이 생기면, 아래와 같이 쿼리하면 됩니다. ROWID를 활용하므로 변경이력을 두번씩 액세스하지 않아도 됩니다. (약간 복잡해지긴 했지만 오동규 님이 제시한 쿼리 정도의 복잡성입니다. 종료일자를 시작일자 앞에 두었다는 것은 대부분 최근 데이터 위주로 조회한다는 가정이므로 아래와 같이 써야 할 일은 흔지 않습니다. TYPE을 미리 선언할 필요도 없습니다.)

    SELECT a.svc_no, a.cust_no, a.acct_no, a.svc_date, a.txt, b.col1, b.col2, b.col_n, c.col1, c.col2, c.col_n, d.col1, d.col2, d.col_n
    FROM (SELECT x.svc_no, x.cust_no, x.acct_no, x.svc_date, x.txt
    ,(select /*+ index(b) */ rowid from svc_hist b where svc_no = x.svc_no and end_dt >= x.svc_date and rownum <= 1) b_rid
    ,(select /*+ index(c) */ rowid from cust_hist c where cust_no = x.cust_no and end_dt >= x.svc_date and rownum <= 1) c_rid
    ,(select /*+ index(d) */ rowid from acct_hist d where acct_no = x.acct_no and end_dt >= x.svc_date and rownum <= 1) d_rid
    FROM svc x
    WHERE x.svc_date BETWEEN SYSDATE - 1999 AND SYSDATE - 1799) ) a, svc_hist b, cust_hist c, acct_hist d
    WHERE b.rowid = b_rid
    AND c.rowid = c_rid
    AND d.rowid = d_rid;

    "index(또는 index_desc) 힌트 및 rownum <= 1 조건을 써선 안 된다"고 선언(이전 이전 글)하고서 선분이력의 성능 문제를 풀어 보려니 더 꼬이는 거 같습니다. 이력 테이블 PK 구조를 바꿀 일이 과연 있을까요? 0%는 아니겠지만, 정말 흔치 않은 일이므로 "약간의 관리적 부담이 생길 수 있다"로 코멘트하고서 그냥 index(또는 index_desc) 힌트 및 rownum <= 1 조건을 쓰는 게 낫지 않을까요? 단, first_rows 오퍼레이션이 정상적으로 작동하는 경우엔 가급적 그것을 활용하자!!
    판단은 독자들의 몫이겠죠.^^;

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.11.25 00:29 신고  댓글주소  수정/삭제

      반갑습니다.
      여기 예제에서 보듯이 여러건을 조회할 때에도 종료일자의 성능상 장점은 없습니다. 다른 유형의 SQL을 연구해보아야 합니다. type까지 쓰며라고 하셨지만 그 노력이 무척 힘들다면 사용을 포기 할 수도 있겠습니다. 하지만 이렇게 조금의 노력을 들인다면 한다면 정합성을 보장하고 성능도 보장합니다. 판단은 말씀하신대로 독자의 몫입니다.

      선분이력님이 이야기 하신 어조는 독자들이 '잘못될 확률이 낮으므로 index_desc + rownum을 사용해야 한다'라고 오해할 수 있습니다.
      그리고 '점이력이 선분이력보다 느리므로 점이력은 역정규화 해야한다. 그리고 역정규화를 해도 정합성이 틀어질 일은 거의 없다' 처럼 들릴 수가 있습니다.
      만약에 그러셨다면 위험한 발언이고 그런 말씀을 한거라고 보지는 않겠습니다.

      제가 지난 10여년간 느낀점이 몇가지 있습니다.
      1.인덱스의 사용유무에 따라 SQL의 답이 틀려지면 안된다.
      2.데이터의 정합성이 중요하므로 성능을 위한 역정규화는 차선책으로 생각하라

      그런데 1,2번에 대하여 다들 수긍하지만 예외적인 것이 3가지가 있는데 그중 2가지가 index_desc + rownum 조합과 변경이력의 역정규화입니다.
      1, 2번을 지킬 수 있다면 지켜야 겠지만 index_desc + rownum 조합과 변경이력 테이블의 역정규화는 마치 1,2번을 만족하지 않아도 되는 특권을 부여한것 처럼 보입니다. 그런데 그 특권이 정당한지 연구해 보자는 것이죠.

      즉 max를 구하기 위해 반사적으로 index_desc + rownum을 사용하는 것을 많이 보았을 것 입니다. 또한 습관적으로 점이력을 선분이력으로 역정규화 하는 것도 마찬가지입니다. 저 또한 예전에 index_desc + rownum 조합과 변경이력 테이블의 역정규화를 즐겨사용 하였습니다. 하지만 그것은 명백한 저의 잘못이었습니다. 저의 실수로 고객에게 위험성을 증가시킨 것 입니다. 원칙을 지킬 수 있다면 지켜야 하며 불가능 할 때만 사용하는 것이 위험을 최소화 할 수 있을 것 입니다.

      수고스럽더라도 다른 예제를 저에게 주시기 바랍니다. 현실적으로 시작일자 만으로 해결하기 어려운 예제들을 모아서 패턴화 시켜 정리하려고 합니다. 이런 패턴들이 모이면 현실세계의 문제들중 많은 부분이 해결 될 겁니다. 만약 10개의 문제중에 8~9개가 풀린다고 하면 역정규화는 1~2개만 해도 될 것입니다. 이런 노력에 보탬이 되주시기 바랍니다. 그러실거라고 믿습니다.

      감사합니다.

    • 선분이력 2010.11.25 07:57 신고  댓글주소  수정/삭제

      이력 테이블 PK 구조가 바뀔 가능성이 없는 경우 관리적 부담을 약간 지면서 그런 SQL 패턴을 허용할 수 있다는 것인데, 자꾸 원론적인 얘기를 하시니 그 부분은 더 이상 논쟁하지 않았으면 합니다. 인덱스 사용여부에 따라 SQL 답이 틀려져도 된다고 생각하는 사람은 아무도 없습니다. 관리적 부담을 진다는 것은 그런 일이 발생하지 않도록 잘 관리하겠단 뜻이지 방치하겠단 뜻이 아닙니다.
      과거 데이터 조회시 성능에 문제가 생긴다면 그런 식으로 풀 수 있다는 것입니다. 정 index 힌트와 rownum 조건을 쓰는 게 싫으면 선분이력도 점이력이라고 생각하고 서브쿼리로 필터링하거나 위에서 제시하신 방식으로 똑같이 성능 문제를 해결할 수 있습니다.
      index(또는 index_desc) 힌트와 rownum 조건을 쓰는 부분에 대해 거부반응을 보이시니 일단 그 패턴은 배제하겠습니다. 시스템이 처한 상황, Mission Critical 여부, 구성원 성향 등에 따라 생각이 다를 수 있으므로...

      여기부터가 오늘의 본론입니다.
      이력을 관리하면 과거 데이터를 조회할 수 있지만, 과거 데이터를 조회하는 일이 흔치는 않습니다. 특히, 오래 전 데이터를 조회하는 일은 거의 없습니다. 과거 데이터를 조회한다고 해 봐야 대개 전일 또는 전월 말일 시점 조회입니다. 오래 전 과거 데이터를 조회하는 일상적인 업무가 무엇인지 한번 떠올려 보십시오. 잘 생각나지 않을 것입니다. 그만큼 흔치 않습니다.

      < Rule of thumb >
      * 최종 이력 조회 = 90%
      * 전일 또는 전월말 시점 조회 = 9%
      * 오래된 과거 이력 조회 = 1%

      이력 테이블을 조회한다고 해도 실제 90%는 가장 마지막 이력 데이터를 조회하는 경우입니다. 점이력이든 선분이력이든 마찬가지입니다.
      점이력 모델에서 가장 마지막 이력을 찾으려면 매번 서브쿼리로 필터링해야 하는 불편함(코딩, 성능)이 있으니까 전통적으로 많이 쓰던 방식이 마지막_여부(LAST_YN) 컬럼을 두는 것입니다. 또는 순번 컬럼에 정해진 값(예를 들면, 9999)을 넣어 두기도 합니다.

      select * from 거래 a, 마스터 b, 마스터이력 c
      where join_condition ...
      and c.last_yn = 'Y'; -- 또는 and c.seq = 9999

      이렇게 할 때의 단점은 이력이 쌓일 때 Insert 뿐만 아니라 Update까지 발생한다는 사실입니다. 그럼에도 그런 속성을 둠으로써 가장 최근 데이터를 빠르게 조회할 수 있으니 그렇게 했던 것이고, 기왕에 Update 할 거면 종료일자를 세팅하자고 아이디어를 낸 것이 선분이력입니다. 가장 마지막 레코드에는 '99991231'를 넣기로 약속한 거구요. 시작일자만을 갖는 점이력에 종료일자 속성을 하나 더 둔 것에 불과하고, 최종 이력을 조회할 때 아래와 같이 하면 됩니다.

      select * from 거래 a, 마스터 b, 마스터이력 c
      where join_condition ...
      and c.end_dt = '99991231';

      이렇게 하니까 과거든 현재든 "쉽게" 찾을 수 있게 된 것입니다. 과거는 아래와 같이 between 조건으로 쿼리합니다.

      select * from 거래 a, 마스터 b, 마스터이력 c
      where join_condition ...
      and '20101031' between c.start_dt and c.end_dt;

      속도요? 최종 이력을 조회할 때는 매우 빠릅니다(90%가 여기에 속함). (종료일자를 선두에 두었을 때를 기준으로 볼 때) 전일 또는 전월 말 시점 조회시 아주 조금 불리하지만 무시할 만한 차이(인덱스에서 몇 개 레코드 더 스캔하는 정도)입니다(9%). 오래된 과거면 꽤 느릴 수 있는데, 다시 말씀드리지만 오래된 과거 데이터를 보는 빈도는 1%도 되지 않습니다.

      역정규화에 대해 굉장히 보수적이신데, 앞선 글에서 무대리님도 언급하신 것처럼 역정규화를 최소화하려면 그만큼 기준성 데이터에 대한 이력 관리가 매우 중요하고 이력 테이블에 대한 조회도 빈번해집니다. 이렇게 중요하고 빈번한 쿼리를 오동규 님께서 제시한 것처럼 복잡하게 쿼리해야 할 일인지 모르겠습니다. TYPE까지 쓰면서 말이죠.
      저 같으면 아래와 같이 하겠습니다.

      1. 우선, 자주 사용되는 "마스터성 정보"에 대한 이력은 선분이력으로 모델링하겠습니다.
      2. 최종 이력은 '=' 조건으로 '99991231'을 조회하고(90%),
      3. 전일 또는 전월말 시점은 between으로 조회하고(9%),
      4. 아주 오래된 과거 이력은 서브쿼리로 필터링하거나 위에 있는 오동규 님 방식으로 쿼리하겠습니다.(1%)
      (※ 개인적으로는, index 힌트와 rownum 조건을 사용하기도 합니다. 아주 오래된 과거 이력을 조회하는 일이 흔하지 않은 데다, 이력 테이블의 PK 구조를 바꾸는 일은 더더욱 없으므로)

      "만약 점이력으로 모델링하면" 100% 서브쿼리로 필터링하거나 위에 있는 방식으로 쿼리해야 합니다. 성능은 동일한데도 말이죠. 1%를 위한 99%의 희생!!

      거래성(주문, 배송, 접수, 상담 등) 데이터의 이력은?
      이 경우에는 선분이력을 써 본 기억이 없습니다. 앞으로도 쓰지 않을 생각이구요. (100% 장담은 못합니다.)

      입력 과정에서 선분의 중복이 생기는 문제는?
      앞 글 답변에서 얘기했듯이 저는 동시성 제어 패턴으로 해결하겠습니다.

      결론적으로, "성능" 때문에 선분이력 모델링 기법의 효용성 자체를 부정해야 할 정도로 과거 데이터를 조회하는 일은 흔지 않습니다. 가끔이지만 오래된 과거 데이터를 조회할 때, 선분이력의 성능이 좋지 않을 수 있으니 "튜닝 차원"에서 가능한 조치들이 무엇인지를 아는 것은 매우 중요합니다.

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.11.25 13:59 신고  댓글주소  수정/삭제

      공감합니다.
      데이터의 품질을 동시성 제어로 잘 관리하시니 아래의 1,2,3,4 번을 사용하면 성능도 문제가 없고 SQL도 간단해 지겠습니다. 효율적으로 관리될 것 같습니다. ^^

      선분이력님께서 "자꾸 원론적인 얘기를 하시니 ~" 라고 하셨습니다. 이와 관련해서 토론이 길어지는 이유를 생각해봤습니다. 그 이유는 선분이력님과 저의 생각의 방향이 달랐기 때문입니다.

      이 글과 이전 글의 의도는 "정합성이 틀어질 가능성이 있음에도 '성능' 때문에 변경이력을 역정규화 해야 하는지"를 연구하는 것입니다. 정합성, 정규형과 역정규형의 성능을 논하려면 원론적인 이야기를 피할 수 없습니다. 예컨데 제가 위의 댓글에서 이야기한 원칙이 없다면 이런 연구를 할 필요가 없고 그냥 역정규형을 사용하면 되는 것 입니다.
      '발전을 위해 성능을 낱낱이 비교해보자' VS '현재의 이력관리도 문제가 없으므로 그럴 필요없다' 처럼 생각의 방향이 틀린것 같습니다. 둘 다 맞는 말 이지만 방향만 다른 겁니다. 의견의 방향이 다르다고 해서 한쪽의 견해를 틀렸다고 할 수는 없을것 입니다.

      무대리님과 선분이력님의 의견을 들었으므로 제 생각의 방향에서 말씀드리겠습니다. 다른 유형을 더 접해야 하지만 현재까지의 연구결과는 '성능 때문이라면 역정규화가 필요치 않다'는 것 입니다. 만약 역정규형을 사용해야 한다면 SQL의 단순화 때문일 것 입니다. SQL의 간편함은 선분이력을 따라갈 수 없습니다. 전체 개발공정에 큰 영향을 주지는 않겠지만 적어도 이력데이터를 조회하는 프로그램의 생성성에는 영향을 줄것입니다. 이 의견은 사내/사외의 많은 전문가들이 공감을 하고 계십니다. 선분이력님도 공감할 수 있다고 생각합니다.

      이와 관련된 다양한 예제를 수집하고 싶습니다만 그다지 많은 패턴이 나올 것 같지 않을것 입니다. 왜냐하면 단건 조회는 이미 이전글에서 제가 언급하였고, 다른 테이블과 조인으로 여러건 조회하는 패턴도 이글에서 테스트 되었기 때문입니다. 없을것 같지만 또다른 패턴이 있다면 제게 알려주시기 바랍니다.

      그리고 다음 글에서 index_desc + rownum 과 관련해서 실제 발생한 사건을 소개해 드리겠습니다. 알만한 분들은 다알 고 계시는 일화입니다. 물론 index_desc + rownum 조합의 위험성을 알리는 일화 입니다.

      새벽시간에 댓글을 쓰시다니 열정에 감동했습니다.
      앞으로도 좋은 댓글 부탁드립니다.
      감사합니다.

      PS 점심시간을 이용하다 보니 답변시간이 넉넉치 않습니다. 이점 이해바랍니다.^^

    • 선분이력 2010.11.25 15:23 신고  댓글주소  수정/삭제

      저와의 견해차를 아래와 같이 정의하셨습니다.

      '발전을 위해 성능을 낱낱이 비교해보자' VS '현재의 이력관리도 문제가 없으므로 그럴 필요없다'

      좌(左)는 오동규 님이고, 우(右)는 저인 거 같은데, 이렇게 정의하니까 왠지 제가 문제의식이 없는 사람처럼 느껴지는군요. 자격지심인가요? ㅎㅎ

      앞선 글 답변에서도 말씀드렸지만 저도 선분이력의 데이터 정합성 문제(실제 데이터가 많이 깨지고 있다는 측면보다는 트랜잭션이 동시에 발생할 경우 그럴 가능성을 내재하고 있다는 문제) 때문에 고민을 많이 했었고, 성능 측면을 낱낱이 비교했던 사람입니다. 오동규 님이 느낀 문제의식을 저도 똑같이 느꼈고, 그래서 발전 방향을 모색하기 위해 오래 전부터 연구를 했던 겁니다.

      오동규 님도 저도 성능을 낱낱이 비교한 점은 똑같습니다. 다만, 거기서 도출한 최종 결론이 달랐던 거죠.

      < 저의 결론 >
      "발전을 위해 성능을 낱낱이 비교해 본 결과, 동시성 제어를 통해 정합성 부분을 보완하기만 하면 선분방식의 이력관리가 여전히 장점이 많다."입니다.

      < 오동규 님의 결론>
      "발전을 위해 성능을 낱낱이 비교해 본 결과, 정합성이 좋지 않을 뿐만 아니라 성능도 나쁘므로 앞으론 점이력을 써야 한다."입니다.

      논의가 복잡하고 길어질 것 같아 지금까지 언급 안 했는데, 추가로 연구해야 할 패턴이 있냐고 물으시니 하나만 더 말씀드리겠습니다. 지금까지는 소량의 범위 데이터만 읽는 경우를 가정했습니다. 즉, 인덱스 손익분기점 이내의 데이터량을 가정하고 인덱스를 통해 이력 데이터를 액세스하는 경우만 논의했습니다.
      만약 이력 데이터를 참조해야 할 기준 집합이 1,000만 건쯤 된다면, 점이력에서 어떻게 쿼리해야 할까요? 물론 Full Table Scan과 분석함수(Analytic Function)을 써야겠죠. 해 보면 아시겠지만, 이 역시 쿼리가 매우 복잡해집니다. 게다가, 아래 수천 만건의 이력 테이블을 모두 읽어 Sort 해야 하는 성능 부하가 생깁니다.

      * svc : 1,000만건(active_yn = 'Y' 기준)
      * svc_hist : 10,000만건
      * acct_hist : 3,000만건
      * cust_hist : 5,000만건

      아래는 현재 살아있는(activt_yn = 'Y') 모든 서비스의 전월 말 시점 이력을 조회하는 쿼리입니다. svc_hist, acct_hist, cust_hist 셋 다 선분이력이고, Full Table Scan은 불가피하지만 분석함수를 쓰지 않으므로 Sort 부하는 없습니다

      SELECT a.svc_no, a.cust_no, a.acct_no, a.svc_date, a.txt, b.col1, b.col2, b.col_n, c.col1, c.col2, c.col_n, d.col1, d.col2, d.col_n
      FROM svc a, svc_hist b, acct_hist c, cust_hist d
      WHERE a.active_yn = 'Y'
      AND b.svc_no = a.svc_no
      AND d.cust_no = a.cust_no
      AND c.acct_no = a.acct_no
      AND '20101031' between b.start_dt and b.end_dt
      AND '20101031' between c.start_dt and c.end_dt
      AND '20101031' between d.start_dt and d.end_dt
      AND ...


      저는 여기까지만 하겠습니다. 힘드네요.

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.11.25 15:51 신고  댓글주소  수정/삭제

      문제의식이 없는 사람처럼 느껴졌다면 제 실수 입니다. '발전을 위해' 라는 문구가 좋지 않았던 것 같습니다. 사과드립니다. '성능을 낱낱이 비교해보자' VS '현재의 이력관리도 문제가 없으므로 그럴 필요없다'


      제 결론에 "~성능상 특별한 장점이 없으므로 성능문제로 역정규화 할 필요는 없다." 로 수정되어야 합니다. 선분이력님이 말씀하신것 처럼 점이력의 성능이 우월한 것은 아니죠. 말씀하신대로 우월한 것은 1%에 불과합니다. 그 1% 마저도 제 방법대로 다른 프로그램을 하나 더 만든다면 차이는 없어집니다.

      예제 SQL 감사드립니다. 분석함수를 써야 한다고 하셨으므로 종료일자를 출력하는 경우를 가정하고 테스트 하겠습니다.

    • 선분이력 2010.11.25 17:01 신고  댓글주소  수정/삭제

      종료일자는 출력 안 해도 됩니다. 서비스(svc)와 관련된 각종 변경이력에서 전월말('20101031') 시점의 데이터를 뽑는 것이 핵심입니다. 살아있는 모든 서비스에 대해서 말이죠.
      이력 테이블을 2번씩 Full Scan한다면 굳이 분석함수를 쓰지 않아도 되구요.

  2. 2010.11.24 20:26  댓글주소  수정/삭제  댓글쓰기

    비밀댓글입니다

  3. 김영석 2013.05.04 10:00 신고  댓글주소  수정/삭제  댓글쓰기

    두 분의 논의에 감사드립니다.
    오히려 두 분의 논의로 인해 많은 정보를 얻고, 도전을 받습니다.
    연구성과를 이렇게 공유해 주시는 오동규님께 감사드립니다.


테스트에 사용할 이력테이블을 현실에 가깝에 만들어서 테스트 해달라는 요청이 있었습니다. 그래서 첨부파일을 따로 올립니다. 고객번호가 테이블과 인덱스에 추가된것을 제외하고 내용은 대동소이합니다. 2010.11.18



-점이력과 선분이력의 차이는 무엇인가?
-선분이력은 언제 필요한가?
 


주문상품 변경이력 테이블에 종료일시가 보인다. 설계자에게 종료일시가 왜 필요한지 물어보았다. “선분이력을 만들기 위함입니다.” 왠 선분이력? 주문상품 변경이력에 선분이력이 왜 필요한지 다시 물어 보았다. “최근에 변경된 주문 데이터를 가져오려면 주문번호 + 상품번호 + 종료일시 + 시작일시로 PK 인덱스를 구성해야 빠르게 가져올 수 있기 때문입니다. 알만 하신 분이 이런것을 왜 물어봅니까?” 어이쿠 한방 맞았다. 이럴때는 물러나야 한다. 설계자가 필자를 감사나 감리로 느낄 수 있기 때문이다.

각종 변경이력 테이블에 인덱스(종료일자 + 시작일자) 열풍이 지난 몇 년간 불고 있는 듯하다. 인덱스를 떠나서 종료일자를 아예 PK 컬럼으로 설정해 놓고 있다. 하지만 유용한 물건도 용도를 모르고 사용하면 다칠 수 있다. 변경이력에 성능을 위한 종료일자는 필요 없다. 종료일자는 성능관점에서 고려할 것이 아니라 업무적으로 필요할 때만 사용해야 한다.

 

환경: Oracle 11g R2

 

(시작일시 + 종료일시) 인덱스와 (종료일시 + 시작일시) 인덱스의 성능비교를 위해 100만 건을 생성한다. 

CREATE TABLE TEST1 AS  

SELECT SYSDATE - level AS start_dt,                  --시작일시

       SYSDATE - level + 1 - 1/24/60/60  AS end_dt,  --종료일시

       '종료일자의 필요성 테스트' as txt

  FROM DUAL

CONNECT BY LEVEL <= 1000000;

 

ALTER TABLE TEST1 MODIFY(START_DT  NOT NULL, END_DT  NOT NULL);

 

CREATE INDEX idx_test1_01 ON TEST1 (START_DT, END_DT) ;

CREATE INDEX idx_test1_02 ON TEST1 (END_DT, START_DT) ;

 

begin

    dbms_stats.gather_table_stats(user, 'TEST1', cascade => true);

end;

 

시작일시, 종료일시의 min, max 값을 구해보자.

select TO_CHAR(min(START_DT), 'SYYYYMMDD HH24MISS') min_st_dt,

       TO_CHAR(max(START_DT), 'SYYYYMMDD HH24MISS') max_st_dt,

       TO_CHAR(min(END_DT),   'SYYYYMMDD HH24MISS') min_ed_dt,

       TO_CHAR(max(END_DT),   'SYYYYMMDD HH24MISS') max_ed_dt

  from test1;

 

MIN_ST_DT        MAX_ST_DT        MIN_ED_DT        MAX_ED_DT      

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

-07281227 105223  20101115 105223 -07281228 105222  20101116 105222

1 row selected.

 

BC 728년부터 시작하여 가장 최근의 시작일자는 2010 11 15 10 52 23초이다. 참고로 BC를 나타내려면 ‘SYYYYMMDD’를 포맷으로 사용해야 한다. 이제 테스트를 시작해보자.

 

최근 정보를 구할 때 시작일자 + 종료일자 인덱스는 느리다?

최근 데이터를 구할 때는 시작일시 + 종료일시 인덱스를 사용하면 비효율적이라고 하였다. 정말 그런지 (시작일시 + 종료일시) 인덱스를 이용하여 가장 최근의 데이터를 구해보자.

 

SELECT /*+ gather_plan_statistics INDEX(T idx_test1_01) */ *

  FROM TEST1 T

 WHERE TO_DATE('20101116', 'SYYYYMMDD') BETWEEN start_dt AND end_dt

   AND ROWNUM = 1;

 

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

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

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

|   0 | SELECT STATEMENT             |              |      1 |      1 |00:00:00.21 |    3773 |

|*  1 |  COUNT STOPKEY               |              |      1 |      1 |00:00:00.21 |    3773 |

|   2 |   TABLE ACCESS BY INDEX ROWID| TEST1        |      1 |      1 |00:00:00.21 |    3773 |

|*  3 |    INDEX RANGE SCAN          | IDX_TEST1_01 |      1 |      1 |00:00:00.21 |    3772 |

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

 

Predicate Information (identified by operation id):

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

   1 - filter(ROWNUM=1)

   3 - access("END_DT">=TO_DATE(' 2010-11-16 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND

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

       filter("END_DT">=TO_DATE(' 2010-11-16 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))

 

시작일자 + 종료일자 인덱스는 최근의 데이터를 구할 때 불리하다. 한 건을 구하기 위해 3773 블럭이나 Scan 하였기 때문이다. 시작일자 + 종료일자 인덱스는 최근 데이터를 구할 때 비효율적임을 알 수 있다.

 

최근 정보를 구할 때 종료일자 + 시작일자 인덱스는 빠르다?
최근 정보(2010년 11월 16일 데이터)를 구하기 위해 종료일자 + 시작일자 인덱스를 이용한다.
 

SELECT /*+ gather_plan_statistics INDEX(T idx_test1_02) */ *

  FROM TEST1 T

 WHERE TO_DATE('20101116', 'SYYYYMMDD') BETWEEN start_dt AND end_dt

   AND ROWNUM = 1;

 

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

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

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

|   0 | SELECT STATEMENT             |              |      1 |      1 |00:00:00.01 |       4 |

|*  1 |  COUNT STOPKEY               |              |      1 |      1 |00:00:00.01 |       4 |

|   2 |   TABLE ACCESS BY INDEX ROWID| TEST1        |      1 |      1 |00:00:00.01 |       4 |

|*  3 |    INDEX RANGE SCAN          | IDX_TEST1_02 |      1 |      1 |00:00:00.01 |       3 |

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

 

Predicate Information (identified by operation id):

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

   1 - filter(ROWNUM=1)

   3 - access("END_DT">=TO_DATE(' 2010-11-16 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND

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

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

 

종료일자 + 시작일자 인덱스는 불과 4블록만 Scan 하였으므로 매우 효율적이다. 설계자들이 종료일자를 선호하는 이유는 이것뿐이 아니다. 최종 변경건의 종료일자에 ‘99991231’을 입력하면 쉽고 빠르게 max 값을 찾을 수 있다는 사실이 모델러를 기쁘게 한다. 아래의 SQL을 보자.

 

Max 일자를 구하기 위해 최종 종료일자를 4000 12 31일로 update 한다.

 

UPDATE TEST1

   SET END_DT = TO_DATE('40001231','YYYYMMDD')

 WHERE start_dt = TO_DATE('20101115 105223', 'SYYYYMMDD HH24MISS') ;

1 row updated.

 

COMMIT;

Commit complete.

 

이제 max 값을 구해보자.

SELECT /*+ gather_plan_statistics INDEX(T idx_test1_02) */ *

  FROM TEST1 T

 WHERE end_dt = TO_DATE('40001231','YYYYMMDD')

   AND ROWNUM = 1;

 

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

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

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

|   0 | SELECT STATEMENT             |              |      1 |      1 |00:00:00.01 |       4 |

|*  1 |  COUNT STOPKEY               |              |      1 |      1 |00:00:00.01 |       4 |

|   2 |   TABLE ACCESS BY INDEX ROWID| TEST1        |      1 |      1 |00:00:00.01 |       4 |

|*  3 |    INDEX RANGE SCAN          | IDX_TEST1_02 |      1 |      1 |00:00:00.01 |       3 |

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

 

Predicate Information (identified by operation id):

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

   1 - filter(ROWNUM=1)

   3 - access("END_DT"=TO_DATE(' 4000-12-31 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))

 

좋은 것이 아니라 선입견이다. 틀을 깨라

종료일자에 4000 12 31일 조건만 주면 쉽고 빠르게 max 값을 구할 수 있다. 정리하면, 종료일시 + 시작일시 인덱스는 빠르고, between을 사용할 수 있고, 종료일자에 = 조건을 주면 max값을 찾을 수 있다.  이 세 가지 사실만으로 종료일자는 충분히 매력적이라고 생각 할 수 있다.

하지만 역설적이게도 이런 사실들은 여러 사람을 함정에 빠트린다. 종료일시라는 컬럼을 사용하는 것이 왜 함정인지 지금부터 논의해보자.

 

인덱스를 동적으로 바꿔서 사용할 것인가?

위의 예제들을 보면 인덱스(종료일자 + 시작일자)를 사용해야 하는 것처럼 보인다. 하지만 최근 데이터가 아닌 오래된 데이터를 보려면 어떻게 할 것인가? 최근 데이터를 구할 때는 종료일자 + 시작일자 인덱스를 사용하고 예전 데이터를 구할 때는 시작일자 + 종료일자 인덱스를 사용해야 하는가?  그렇게 하려면 기준이 필요하다. 언제부터가 오래된 데이터 인가? 1달 전 데이터? 아니면 1년 전 데이터? 이력테이블마다 기준을 만든다는 것은 불가능한 일이므로 그렇게 할 수도 없고, 그렇게 할 필요도 없다.

 

PK의 정합성을 어떻게 확보할 것인가?
서론에서 언급했던 PK에 대해서 논의해보자. 성능관점에서 주문번호 + 상품번호 + 종료일시 + 시작일시를 PK로 지정했다고 가정하고, 이때까지 PK의 정합성에 별 문제가 없다고 좋아해서는 안 된다. 주문상품 변경이력 테이블에서 본래의 식별자는 주문번호 + 상품번호 + 변경일시(시작일시)이다. 3개의 컬럼으로 Unique를 만족해야 한다. 하지만 종료일시가 PK에 끼어듦으로 해서 정합성이 깨질 수 있다. 예를 들면 종료일시를 PK에 추가하는 순간 같은 주문번호로 같은 상품을 같은 변경일시에 2건이상 insert 하는 것(Dup)을 허용하는 꼴이 된다. 종료일시를 PK에 추가하였으므로 세개의 값이 같아도 종료일시만 다르다면 insert가 가능하다. 변경이력 테이블의 데이터에 Dup이 많은 이유는 대부분 종료일시 때문이다.  

 

인덱스에서 종료일자를 빼면 성능이 저하된다?

왜 인덱스(종료일시 + 시작일시) VS 인덱스(시작일시 + 종료일시)만 고려해야 하는가? 틀을 깨보자. 그냥 시작일시만 인덱스로 잡아보자. 기존 인덱스를 모조리 삭제하고 시작일시만으로 구성된 인덱스 만들어 본다. 그리고 이제부터 종료일시 컬럼도 없다고 가정한다.

DROP INDEX idx_test1_01;

DROP INDEX idx_test1_02;

CREATE INDEX idx_test1_03 ON TEST1 (START_DT) ; 

                                                                                                  

이제 최종 데이터를 구해보자.

SELECT /*+ gather_plan_statistics  */ *

  FROM test1 t

 WHERE start_dt = (SELECT MAX (start_dt) FROM test1);

 

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

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

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

|   0 | SELECT STATEMENT             |              |      1 |      1 |00:00:00.01 |       7 |

|   1 |  TABLE ACCESS BY INDEX ROWID | TEST1        |      1 |      1 |00:00:00.01 |       7 |

|*  2 |   INDEX RANGE SCAN           | IDX_TEST1_03 |      1 |      1 |00:00:00.01 |       6 |

|   3 |    SORT AGGREGATE            |              |      1 |      1 |00:00:00.01 |       3 |

|   4 |     INDEX FULL SCAN (MIN/MAX)| IDX_TEST1_03 |      1 |      1 |00:00:00.01 |       3 |

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

 

Predicate Information (identified by operation id):

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

   2 - access("START_DT"=)

 

시작일시가 인덱스의 선두컬럼이므로 성능이 좋지 않을것으로 판단하면 오산이다. 3773 블록이 아니라 고작 7블록을 scan 했다. 이 정도면 충분한 성능이다. 서브쿼리를 사용했으므로 SQL이 복잡해진다는 의견은 받아 들일 수 없다. 위의 쿼리가 그렇게 복잡하다면 SQL을 다시 공부해야 한다.

또 다른 반박의견으로는 종료일시 + 시작일시 인덱스를 사용하면 4블록만 Scan하지만 시작일시만으로 구성된 인덱스는 7블록을 Scan한다는 의견이 있다. 맞는 말이다. 하지만 초당 수백 번 혹은 수천 번 사용되는 것이 아니라면 이대로 사용해도 무리가 없다. 만약 자주 사용되어 부하가 심한 SQL이라면 다음처럼 사용하면 된다.

 

SELECT /*+ gather_plan_statistics */ *

  FROM (SELECT *

          FROM test1 a

         ORDER BY a.start_dt DESC)

 WHERE ROWNUM = 1 ;  

 

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

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

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

|   0 | SELECT STATEMENT              |              |      1 |      1 |00:00:00.01 |       4 |

|*  1 |  COUNT STOPKEY                |              |      1 |      1 |00:00:00.01 |       4 |

|   2 |   VIEW                        |              |      1 |      1 |00:00:00.01 |       4 |

|   3 |    TABLE ACCESS BY INDEX ROWID| TEST1        |      1 |      1 |00:00:00.01 |       4 |

|   4 |     INDEX FULL SCAN DESCENDING| IDX_TEST1_03 |      1 |      1 |00:00:00.01 |       3 |

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

 

Predicate Information (identified by operation id):

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

   1 - filter(ROWNUM=1)

 

정확히 4블록만 Scan 하였으며, 위의 SQL도 복잡하지 않음을 알 수 있다. max값을 구하기 위해 종료일자 컬럼은 필요치 않음을 알 수 있다.

종료일시 + 시작일시로 구성된 인덱스의 장점은 특정 시점의 테이터를 between으로 구할 수 있다는 것이다. 종료일시 컬럼이 없고 시작일시만으로 구성된 인덱스는 특정시점의 데이터를 조회해야 할때 between을 사용할 수는 없다. 과연 SQL은 얼마나 복잡해질까? 또한 최근 데이터를 구할 때 얼마나 느려질까?

 

SELECT /*+ gather_plan_statistics */ *                   

  FROM (SELECT *

          FROM test1

         WHERE start_dt <= TO_DATE('20101115 105223', 'SYYYYMMDD HH24MISS')

         ORDER BY start_dt DESC )

  WHERE ROWNUM = 1;    

 

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

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

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

|   0 | SELECT STATEMENT               |              |      1 |      1 |00:00:00.01 |       4 |

|*  1 |  COUNT STOPKEY                 |              |      1 |      1 |00:00:00.01 |       4 |

|   2 |   VIEW                         |              |      1 |      1 |00:00:00.01 |       4 |

|   3 |    TABLE ACCESS BY INDEX ROWID | TEST1        |      1 |      1 |00:00:00.01 |       4 |

|*  4 |     INDEX RANGE SCAN DESCENDING| IDX_TEST1_03 |      1 |      1 |00:00:00.01 |       3 |

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

 

Predicate Information (identified by operation id):

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

   1 - filter(ROWNUM=1)

   4 - access("START_DT"<=TO_DATE(' 2010-11-15 10:52:23', 'syyyy-mm-dd hh24:mi:ss'))

 

장점이 없는데 자리만 차지한다

특정 시점의 데이터를 구하려면 시작일시 <= 특정시점을 만족하는 데이터 중에 max(시작일시)에 해당하는 값을 취하면 된다. 위에서 보듯이 SQL이 어렵지 않으며 성능 또한 최적이다. 이로써 특정시점의 데이터를 구하는 것도 종료일시가 필요치 않으며, 시작일시 인덱스만으로도 충분한 성능을 낸다는 것을 알 수 있다. 또한 최근 데이터(2010 11 15)를 구할 때에도 시작일시 인덱스를 사용하면 최적이다. 이제 종료일시 + 시작일시 인덱스는 성능이 우월 하지 않음을 알게 되었다. 또한 종료일시 컬럼은 어떠한 장점도 없으면서 테이블과 인덱스의 덩치(Disk Size)만 크게한다는 것도 알게 되었다.

 

변경이력에서 종료일자는 추출속성이며 성능을 위해 역정규화 한 것

사실은 ~변경이력에서 시작일자 혹은 종료일자란 없는 것이다. 시작일자는 변경일자로 사용해야 하며 종료일자는 존재하지 않는 것이다. 종료일자 혹은 종료일시는 추출속성이다. 이 추출 속성을 유지하기 위해 원본 테이블의 데이터가 변경될 때마다 트리거성으로 이력테이블의 종료일자에 update가 발생한다. update가 없다면 변경이력 테이블의 모든 종료일자의 값은 ‘99991231’일 것이다.

 

만약 종료일자 없이 시작일자만 관리했다면 이러한 update는 발생하지 않는다. 원본 테이블에 변경이 자주 발생할 수록 update의 부하는 심해질 것이다. 또한 가능성은 많지 않지만 이런 역정규화에 의해 데이터의 정합성이 깨질 수도 있다.

 

그럼 종료일자는 언제 사용해야 하나?

가장 중요한 부분이다. 종료일자는 성능관점이 아니라 업무(Business)적으로 필요할 때만 써야 한다. 예를 들어보자. 당신이 프로젝트에 투입되는 계약을 했다고 가정해보자. 그 계약서에는 '며칠부터 며칠까지 프로젝트에 투입된다'고 명시되어 있어야 한다. 이럴 때는 시작일자 종료일자를 사용해야 한다. 또한 자동차를 빌릴 때(Rent)에도 언제부터 언제까지 사용할 것인지 명시되어야 한다. ~변경이력과 변경이력이 아닌 것과의 차이점을 알았다면 종료일자를 언제 써야 하는지도 이해한 것이다. 바로 이것이 점이력과 선분이력의 차이이다. 원래 점이력이었던 것을 선분이력으로 바꾸어선 안된다.

 

결론

1 ~변경이력 테이블에서 종료일자는 성능관점이나 개발생산성 관점에서 장점이 없다.

2 ~변경이력 테이블의 PK에 종료일자를 추가하면 정합성을 해칠 수 있다. 이렇게 되면 DB 차원에서 정합성을 보장하지 못한다. 따라서 종료일자가 추가된 PK가 존재함에도 불구하고 주문번호 + 상품번호 + 시작일자로 Unique 인덱스를 추가로 생성하거나 아니면 프로그램에서 정합성 체크를 해야 한다.
3 ~변경이력 테이블에서 종료일자는 테이블과 인덱스의 사이즈를 각각 크게한다. Disk 공간이 더 필요할 것이다.

4 인덱스가 A + B 일 때 insert 하는 속도는 인덱스가 B 컬럼으로만 되어있을 때 insert 하는 속도보다 느리다. 이 원리는 종료일자 + 시작일자 인덱스와 시작일자만으로 구성된 인덱스에도 적용된다.

5 ~변경이력 테이블에서 종료일자는 추출속성이며 역정규화가 필요하다. 역정규화는 원본데이터가 변경될 때마다 update가 추가로 발생된다.
6 역정규화는 데이터가 틀어질 위험성을 가지고 있다.

 

1, 2, 3, 4, 5, 6에 의해서 종료일자 혹은 종료일시는 변경이력에서 성능관점으로 사용해서는 안되며 업무적으로 필요할 때에만 사용해야 한다. 이렇게 된다면 성능이 보장되는 것은 물론이고, 인덱스와 테이블의 사이즈도 줄어들 뿐만 아니라 역정규화의 단점인 update의 부하와 데이터의 정합성이 틀어지는 것을 막을 수 있다.

필자는 선입견이 많아서 자주 되뇌이는 말이 있다.
"진실이라고 생각되는 것들은 때때로 변한다. 하지만 선입견은 좀처럼 변하지 않는다." 만약 이말을 따른다면 우리가 이미 알고 있다고 생각하는 것들을 의도적으로 의심해 보아야 한다. 나는 이런 사람들을 린치핀이라 부르고 싶다.

저작자 표시 비영리 동일 조건 변경 허락
신고
Posted by extremedb

댓글을 달아 주세요

  1. Favicon of http://david2kim.tistory.com/ BlogIcon 리베 2010.11.17 09:28 신고  댓글주소  수정/삭제  댓글쓰기

    소개하신 선분이력에 종료일자가 들어가는 이슈는 아무래도 E사의 영향이 큰듯 합니다.
    Modeling 교육 때에도 이 문제로 강사와 수강생이 의견충돌을 보였었는데요. 강사쪽에서 워낙 강경하게 나오는 상황에,
    다른 수강생들의 눈총(?) 때문에 결론을 맺지 못한 주입식(?) 교육이 되었던 기억이 있습니다.
    말씀하신 바와 같이 실제로 시작일자와 종료일자를 포함한 PK를 생성하였을 경우 관리상의 어려움(?)이 조금씩 발생하더군요.
    뭐, 이 부분에 대해서 본인이 Modeling을 잘못해서, 또는 Query를 잘 활용을 못하기 때문에라고 말씀하신다면야 할 말이 없습니다만...
    최소한 누가 좋다더라... 이렇게 해야된다고 하던데... 라는 식의 말로 인해서 마치 "성능 향상 불변의 법칙"처럼 되는 형태는 지양되어야 하지 않을까 싶습니다. 예를들어 5년전에 최고였던 기술이 현시점에도.. 앞으로 미래에도 최고의 기술이라는 법은 없으니 말이죠...
    항상 고정관념(?)을 깨주시는 오동규님의 게시물은 저에게 자극을 주는듯 합니다. 감사합니다. ^^

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.11.17 13:21 신고  댓글주소  수정/삭제

      반갑습니다. 리베님
      그런일이 있었군요.^^
      리베님처럼 변경이력의 pk에 종료일자 때문에 관리에 어려움을 호소하는 분들이 많습니다.

      그리고 고정관념에 대해서는 저도 피해갈 수 없었습니다. 수많은 토론과정과 증명과정을 통하는 방법이나 의도적으로 의심을 하는 방법으로 고정관념을 극복하기 위해 노력하는 중 입니다.^^

  2. 라튜니 2010.11.17 13:45 신고  댓글주소  수정/삭제  댓글쓰기

    항상 좋은 정보 감사합니다. 공감되는 내용입니다.

    저 역시 종료일자는 큰 의미가 없다고 생각됩니다.

    다만 이력으로 종료일자를 사용할 경우 PK로 ID + 시작일자 + 종료일자 는 의미가 없다고 생각합니다만
    PK를 ID + 종료일자로 구성하는 것은 나름 장점이 있다고 생각합니다. 물론 마찬가지로
    이 경우에도 DB에서 실체무결성으로 정합성을 보장해 주지는 못합니다.

    그러나 종료일자를 사용하고 종료일자에 '99991231'과 같은 고정한 값을 지정함으로써의 장점은
    종료일자 이후에 다른 컬럼들이 있는 결합인덱스에서 일꺼 같습니다.

    ID + 종료일자 + 조건1 + 조건 2 .... 결합인덱스의 경우
    WHERE 조건에 종료일자 = '99991231' 를 줌으로써 최신 데이타를 바로 찾을 수 있고
    결합인덱스의 종료일자 이후 조건(조건1, 조건2이 '=' 조건으로 사용될 경우)을 드라이빙(Access) 조건으로
    사용할 수 있기 때문이죠.

    말씀하신 것 처럼 WHERE start_dt <= TO_DATE('20101115 105223', 'SYYYYMMDD HH24MISS')
    조건을 사용하다면 결합인덱스의 경우 시작일자 이후의 다른 컬럼들은 Access 조건으로 사용될 수 없을 꺼 같은데요.

    물론 마찬가지로 시작일자를 '99991231' 과 같은 고정된 값으로 지정한다면 가능하겠지만 시작일자를 고정한 값으로
    하는 것은 이치에 맞지 않는 거 같네요.

    이 점은 어떻게 생각하시는지 궁금합니다.

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.11.17 15:45 신고  댓글주소  수정/삭제

      안녕하세요. 라튜니님. 자주 뵙네요.

      말씀하신대로 시작일자 이후의 다른 컬럼들은 Access 조건으로 사용될 수 없습니다. 또한 시작일자를 고정한 값으로 사용하는 것도 불가능 합니다.

      이야기 하신 pk 인덱스가 ID + 종료일자 + 조건1 + 조건 라고 한다면 종료일자 대신에 ID + 조건1 + 조건 + 시작일자 를 PK로 지정 하시면 됩니다.

      만약 PK 인덱스가 ID + 종료일자 로 구성되었고 ID + 종료일자 + 조건1 + 조건 인덱스를 추가하셨다면 시작일자로 대체가 가능합니다. PK 인덱스를 ID + 시작일자 로 두고 ID + 조건1 + 조건 + 시작일자 로 추가적인 인덱스를 구성하시면 됩니다.

      잘 아시겠지만, 종료일자 컬럼을 무조건 쓰지 말라는 이야기가 압니다. 앞서 예를 들었던 자동차 RENT를 예를 들면 종료일자를 써야 합니다. 변경이력이 아니기 때문입니다. 즉 자동차의 정보가 바뀐게 아니라는 겁니다. 자동차의 데이터는 그대로 이지만 시간이 흐름에 따라 빌려간 사람들이 달라지겠죠.

      다만 컬럼의 값이 변경되면 트리거성으로 INSERT 되는것은 변경이력 입니다. 이때는 역정규화는 위험하기 때문에 사용을 자제하자는 것 입니다. 자제라는 말은 역정규화를 하여 장점이 단점보다 큰 경우에는 종료일자를 사용할 수 있다는 뜻입니다.

      이 POST에서 말하고자 하는 것은 ~변경이력 엔티티를 설계할 때 장단점을 따져보지 않고 습관적으로 종료일자를 추가하는 경우에 대해 경종을 울린것 입니다. 장/단점을 비교하시고 설계하셨다면 문제가 없습니다.^^

  3. 혈기린 2010.11.17 15:39 신고  댓글주소  수정/삭제  댓글쓰기

    말씀하신걸 종합하면 점이력 선분이력을 정할때 성능관점이 아닌 업무관점으로 구분해서 모델링을 해야한다는 말씀이신가요?
    모르는 사람이 보면 선분이력은 점이력에 비해서 좋은점이 없다 처럼들릴거 같아서요
    실제 업무에서 보면 선분이력으로 관리해야 하는데 점이력으로 관리하여서 많은 문제점이 있었던것을 보아서요~~

    인덱스를 종료일자+시작일자 이냐 아니면 시작일자+종료일자로 할것이냐만 고민했었는데 많은것을 느끼게 해주는 글이었습니다
    감사드립니다 ^^

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.11.17 16:43 신고  댓글주소  수정/삭제

      기린님 반갑습니다.
      제가 쓴 글의 요지는 변경이력이냐 아니냐에 있습니다. 값이 바뀔때 트리거성으로 INSERT 되는 테이블은 변경이력입니다. 하지만 자동차 RENT의 경우는 변경이력이 아닙니다. 자동차 정보가 바뀌는것이 아니기 때문입니다. 자동차 RENT의 경우는 선분이력이 맞습니다.종료일자가 원래 존재합니다. 이럴때는 종료일자를 사용해야 합니다.

      반대로 변경이력인 경우는 종료일자가 존재하지 않는것입니다. 만약에 종료일자가 있다면 이것은 역정규화 한 것며 종료일자가 장점이 없다는 것을 말씀 드린겁니다. 원래는 없던것을 만들어 낸 것이죠. 하지만 이때에도 예외적으로 종료일자가 있으면 좋은 경우는 있습니다.

      예를 들면 사원변경이력 테이블의 경우입니다. 현재 부서번호 10번인 직원들에 대하여 최종 이력을 보려고 할때 입니다. 종료일자가 존재한다면 아래처럼 간단히 구현할 수 있습니다. 이 예제는 회사에 같이 근무하는 컨설턴트에게 들은 것 입니다.
      WHERE 부서번호 = 10
      AND 종료일자 = '99991231'

      하지만 시작일자만 존재한다면 위의 경우처럼 간단히 구할 수 없습니다. WHERE 부서번호 = 10 조건만 줄수 있으므로 과거에 부서번호가 10번이 이었던 직원과 현재 부서번호가 10번인 직원이 혼재되어 나오므로 추가적으로 직원테이블과 조인을 하거나 추가적인 작업이 필요합니다. 또한 프로그램 화면에 종료일자를 항상 표시해야 하는 경우는 분석함수를 써야 할 수도 있습니다. 이럴때에는 종료일자를 추가할 지 말지 장단점을 따져보아야 합니다.

      감사합니다.

  4. salvationism 2010.11.18 14:33 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 글 잘보고 갑니다.
    글로 쓰기는 좀 그런데 놓쳤던 부분을 하나 얻고 갑니다.

  5. Favicon of http://ukja.tistory.com BlogIcon 욱짜 2010.11.18 14:42 신고  댓글주소  수정/삭제  댓글쓰기

    히스토리 데이터 관리를 위해 "종료일자"라는 추가하는 것이 "역정규화(또는 비정규화)"의 일종이라는 것을 간과하기 쉬운데 아주 명쾌하게 설명하셨네요.

    히스토리 데이터 관리를 위해 종종 사용되는 또 다른 방법 중 하나가 "현재(최신) 데이터"와 "과거(이력) 데이터"를 별도의 테이블 분리하는 것인데, 이 방법 또한 철저한 검증을 할 기회가 있어야겠군요.

  6. Favicon of http://blog.naver.com/bluepupils BlogIcon 김기창 2010.11.18 14:46 신고  댓글주소  수정/삭제  댓글쓰기

    바쁘다는 이유로 이제서야 처음으로 글을 올리는데... 처음부터 쉬운 소재의 글이 아니네요. ㅎㅎ

    이 글을 읽는데 도움이 될 수 있어... 이력이 어떤 데이터를 의미하는지부터 잠깐 언급할게요.
    저는 간단히 '발생내역'과 '변경이력'으로 구분합니다.

    일반적인 트랜잭션이 발생시킨 데이터(발생내역)와
    이미 존재하는 발생된 데이터가 변경될 때 생기는 데이터(변경이력)는 다르죠.
    제 책(이론과 실무를 겸비한 전략서-관계형 데이터 모델링 프리미엄 가이드)을 참고하시면 차이점은 쉽게 이해하실 것입니다. 물론 완전히 구분하기 쉽지 않아 더 연구해야 할 분야라고 생각합니다.

    발생내역 데이터에 종료일자가 들어가야 하는 경우(업무)는 기간을 의미하는 것 이외는 없을 거 같고요.
    윗글에서 언급된 렌트기간을 관리할 때 종료일자를 사용하겠죠.
    이건 생략해도 되는 추출속성이 아니라 필수적으로 필요한 업무 속성입니다.

    문제는 윗글에서 언급한 변경이력인데요.
    기존 데이터가 변경된 것이므로 뭐(PK)에 대한 변경을 관리하게 됩니다. 즉 PK+종료일자, PK+시작일자(+종료일자), PK+변경일자와 같이 관리하게 됩니다.

    개인적으로 더 상세한 검토가 필요하지만 일정 부분 성능과 관련이 있다고 생각했는데요. 차이가 없다고 하니 새로운 발견이네요.
    엄청나게 조회되는 요건에 해당하거나 과거 특정일에 해당하는 대량 조회일 경우에도 성능이 완전히 같은지 검토해야 될 거 같습니다.

    제 책에서도 언급했지만 실무에서 종료일자가 남발되는 경향이 있어요.
    관리가 안 되는 경우를 많이 봤기 때문에 보수적으로 사용해야 될 거 같습니다.

    데이터 정합성은 무엇보다 중요합니다. 약간의 성능 향상보다 정합성이 더 중요하다고 생각합니다.
    계정계에서의 대부분의 변경이력 데이터는 'PK+변경일자'처럼 관리해도 충분한 거 같습니다.

    p.s.
    그동안 개인적인 일 때문에 별 도움이 못 돼 죄송해요. ㅎㅎ
    모델링 영역에서도 참신한 내용들 많이 부탁드리겠습니다.

    • 혈기린 2010.11.18 16:22 신고  댓글주소  수정/삭제

      오동규님 페이스북에서 모델링 서적을 같은회사 컨설턴트분이 집필중이라는 이야기를 들었는데
      드뎌 출간이 되시나 보네요 ~~
      기대 됩니다 ㅎㅎ

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.11.18 17:41 신고  댓글주소  수정/삭제

      종료일자에 대해서는 저와 생각이 같네요. 보수적으로 사용해야 한다는...

      아 참! 축하드립니다. 모델링책이 출간되었군요. 아마 이번달 말에는 서점에서 구할 수 있겠죠? 기대 됩니다.

      그리고 혈기린님 제가 페이스 북에서 그런말을 했었나요? 대외비인데 큰일 날뻔 했네요.^^

    • 혈기린 2010.11.18 18:10 신고  댓글주소  수정/삭제

      대외비 였나요 ㅎㅎ
      이렇게 고수분들의 열정을 볼때마다 많이 반성하게 됩니다 나태한 자신을 ~~

    • 최상운 2010.11.18 19:42 신고  댓글주소  수정/삭제

      흥미로운 글들입니다.

      개인적인 의견으로는 오동규님도 언급 했듯이, <종료일자>는 성능적인 문제 보다는 업무적인(자동차 RENT 회사의 예처럼) 관점에서 접근해야 할 것 같습니다.

      변경의 성격이 연속적이라면 <시작일자(변경일자)>만 관리 해도 될것 같습니다.
      (하지만 연속적인 경우라도 지구가 멸망할때가지 연속적일지는 잘 모르겠네요)

      변경의 성격이 비 연속적이라면 <시작일자> 와 <종료일자> 모두를 관리해야 할 것입니다.
      (이 경우에는 지구가 멸망하기 하루 전에 종료처리를 하면 되겠네요)

      데이터의 정합성 문제는 크게 이슈 될 필요가 없을것 같네요. 일반적인 변경이력은 자식 엔티티를 파생하지 않고,선분이력의 경우 DBMS 차원에서 선분의 중복을 막을 수 있는 방법은 없으니깐요.(아래 글에서 Constraint를 걸자고 하는데 현실적으로는 불가능할 것 같네요.)
      변경이력에서는 정합성 보다는 데이터의 무오(無誤)성이 중요 하다고 생각 되네요.

      성능적인 관점에서는 오동님의 논거도 틀리지는 않았다고 봅니다. 다만, 다양한 경우에서 보편적으로 적용 할 수 있을 지는 좀더 검증과 논의가 있어야 할 것으로 생각 됩니다.

      앞으로 어떻게 진행될지 기대가 됩니다.

    • 선분이력 2010.11.18 21:53 신고  댓글주소  수정/삭제

      아래 글은 Constraint를 걸자는 얘기가 아니고, 혹시 "그런 기능이 DBMS에 추가된다면"이라고 말하고 있습니다. 정합성 측면은 일단 논외로 하고 성능측면으로 좁혀서 생각해 보자는 의도입니다.

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.11.19 08:51 신고  댓글주소  수정/삭제

      최상운 수석님 반갑습니다.
      말씀하신대로 다양한 경우에 대하여 검증이 필요합니다.

  7. 선분이력 2010.11.18 18:17 신고  댓글주소  수정/삭제  댓글쓰기

    상품번호, 시작일자, 종료일자, 가격
    =======================================
    상품A, 2010/10/10, 2010/10/18, 1000
    상품A, 2010/10/12, 2010/10/20, 2000

    PK를 [상품번호+시작일자]로 구성하거나 [상품번호+종료일자]로 구성하면 레코드의 유일성은 보장됩니다. 다만, 선분의 중복을 막지 못합니다. 위 사례를 예로 들면, 2010/10/12부터 2010/10/18 구간에 속하는 이력 레코드가 2건이 되기 때문이죠. 이것이 선분이력의 가장 취약한 단점이고, 저도 이 때문에 선분이력으로 모델링할 때 데이터 상황과 여러가지 애플리케이션 특성을 고려해서 결정합니다.

    "변경이력 테이블에서 종료일자는 성능관점이나 개발생산성 관점에서 장점이 없다"고 단정하셨는데, 질문 한 가지 드리겠습니다.

    [질문] 만약 위와 같은 선분의 중복 현상을 원천적으로 막아주는 Constraint 기능이 DBMS에 추가된다면, 그래도 선분이력을 쓰지 않으시겠습니까?
    제시하신 예제는 너무 단편적이고 실제 업무에서 있을 법한 형태가 아니어서 아래와 같은 상황을 가정하겠습니다.

    -- 아래 ----------------------

    [ 서비스 ]
    # 서비스번호(PK)
    * 고객번호
    * 계정번호
    * 가입일자
    * 기타 속성들

    위 '서비스' 테이블과 자주 조인되는 '서비스변경이력', '고객변경이력', '계정변경이력' 테이블이 있고, 각각 PK 구성은 다음과 같습니다. (시작일자, 종료일자 순서는 논외)

    >> 서비스변경이력 : 서비스번호 + 시작일자 + 종료일자
    >> 고객변경이력 : 고객번호 + 시작일자 + 종료일자
    >> 계정변경이력 : 계정번호 + 시작일자 + 종료일자

    이런 상황에서 2010년 10월 1일부터 10월 31일 사이에 가입한 서비스와 관련한 각종 변경이력을 조회하고자 할 때, 모두 선분이력이므로 아래와 같이 간단하게 쿼리할 수 있습니다.

    select *
    from 서비스 a, 서비스변경이력 b, 계정변경이력 c, 고객변경이력 d
    where a.가입일자 between '20101001' and '20101031'
    and b.서비스번호 = a.서비스번호
    and c.고객번호 = a.고객번호
    and d.계정번호 = a.계정번호
    and a.가입일자 between b.시작일자 and b.종료일자
    and a.가입일자 between c.시작일자 and c.종료일자
    and a.가입일자 between d.시작일자 and d.종료일자
    and b.filter_condition = ...
    and c.filter_condition = ...
    and d.filter_condition = ...

    서비스 테이블에 대한 가입일자 조회구간은 가변적이고, 위 쿼리가 가장 자주 수행되는 쿼리라고 가정하겠습니다. 이런 업무를 선분이력으로 설계하고 위와 같이 간단하게 쿼리하는 것이 정말 성능상 잇점이 없다고 생각하시는지 궁금합니다.
    (다시 말씀드리지만, 선분의 중복 현상을 원천적으로 막아주는 Constraint 기능이 DBMS에 추가되었다고 가정하겠습니다. 데이터 정합성 문제는 따로 논하려는 것이므로 성능 관점에서만 답변해 주시면 고맙겠습니다.)

  8. 선분이력 2010.11.18 21:58 신고  댓글주소  수정/삭제  댓글쓰기

    SELECT * FROM
    (SELECT * FROM test1
    WHERE cust_no = 5
    AND start_dt <= TO_DATE('20101116', 'YYYYMMDD')
    ORDER BY start_dt DESC )
    WHERE ROWNUM = 1;

    새로 추가한 첨부파일에 위와 같이 cust_no를 추가하셨지만, 그렇더라도 현실 세계에서 위처럼 간단한 SQL은 많지 않습니다. 대부분 다른 테이블과 많은 조인을 수반합니다. 그리고 위처럼 특정 cust_no만 조회하는 것이 아니라 여러 고객을 조회해야 한다면 rownum 조건을 사용할 수도 없겠죠.
    위 쿼리만 보더라도 아래와 같이 작성하면 훨씬 간결합니다. 정렬 작업을 위해 Sort Area를 할당하지 않아도 되고요.

    SELECT * FROM test1
    WHERE cust_no = 5
    AND TO_DATE('20101116', 'YYYYMMDD') between start_dt and end_dt

    처음 답글에서 제가 제시한 모델을, 점이력으로 설계하면서도 더 간단하고 빠른 SQL문을 작성해 내지 못한다면 결국 성능 측면에선 선분이력이 잇점을 갖는 건 사실입니다.

    첨부 파일 PS에 덧붙이신 아래 의견이 제 질문에 대한 오동규님의 답변이라고 생각하고 계속 말씀드리겠습니다.

    -- 아래 ------------
    "역정규화의 장/단점을 비교해서 역정규화의 장점이 더 크고, 데이터의 정합성을 위해 별도의 검증 로직이 졲재하고, 정합성이 깨졌을 때 보정해주는 프로그램이 있다면 종료일자를 적용해도 문제가 없다. 이 글은 변경이력 엔티티를 설계할 때 장단점을 따져보지 않고 습관적으로 종료일자를 역정규화 하는 경우에 대해 경종을 울린 것이다. 또한 이 글을 읽고 업무적으로 점이력과 선분이력이 어떻게 다른지 판단할 수 있었으면 한다."
    -- 끝 --------------

    장단점을 따져보지 않고 "습관적으로" 종료일자를 두는 것에 경종을 울린다고 말씀하시지만 오동규님 글은 결국, "선분이력은 성능상 장점도 없는데다 데이터 정합성까지 해칠 위험이 있으니 좋지 않다"는 것이 결론처럼 느껴집니다. 데이터 정합성 부분은 많이 알려진 사실이어선지 주로 성능상 잇점이 없음을 설파하셨구요.
    데이터 정합성에 다소 문제가 생길 수 있음에도 불구하고 선분이력을 사용하는 데는 성능적인 이유가 가장 큽니다. 그럼에도 성능 문제가 거의 드러나지 않는 아주 간단한 샘플 쿼리를 보이며 "성능상 별차이 없지 않느냐, 도대체 장점이 뭐냐"고 얘기하신다면 또다른 편견을 양산할 수 있다는 생각이 듭니다.
    선분이력을 처음 접하고 무조건 선분이력으로 모델링하려는 분들이 계셨던 것처럼, 이 글을 접한 분 중에선 앞으로 선분이력을 무조건 배타적으로 바라보는 분들도 생겼을 것입니다. 오동규님이 추가로 올린 파일 말미에도 강조했듯이 차이를 알고 상황에 맞게 판단하는 능력이 중요한데도 말이죠.
    그런 측면에서 보면, 애초에 각각의 장단점만을 비교해 보였으면 좋지 않았을까란 생각을 했습니다. 의도와 다르게 선분이력에 대해 너무 부정적인 측면만을 강조했다는 생각에 주제넘게 끼어들게 되었습니다. 죄송합니다.

    • 선분이력 2010.11.18 23:48 신고  댓글주소  수정/삭제

      저도 선분이력에 대해 나름 연구를 많이 했고, 특히 정합성 측면에서 많은 고민을 하면서 한때 매우 비판적이었던 사람입니다. 성능이 아무리 빨라도 데이터를 지켜내지 못하면 허당이죠. 그것을 주장할 논리와 근거는 사라지고, 비판과 공격의 대상이 될 뿐입니다.
      하지만 현실 업무를 모델링하다 보면 선분이력의 매력에서 헤어날 수가 없습니다. 마약 같다는 느낌을 받죠. 왜냐면, 단점을 뻔히 알면서도 대안 모델을 찾기가 쉽지 않기 때문입니다. 여기서 점이력 모델과의 장단점 비교 논쟁을 계속하진 않겠습니다.
      하여튼...
      그래서 저는 방향을 바꿨습니다. 분명 장점은 있으므로 비판하기보다 보완하자는 쪽으로 말이죠. 가장 취약한 정합성 문제를 해결할 코딩 패턴을 개발하자는 생각입니다. 동시성 제어 측면에서 잘 연구해 보면, 몇 가지 아이디어가 나옵니다. 그 중 일부는 약간의 모델 변경을 겸해야 하구요. 고민은 각자에게 맡기고, 저는 여기까지... (선분이력의 모든 문제를 100% 해결할 솔루션이 있다면 당장 소개하겠지만, 그렇지가 못해 생략하는 겁니다. 예를 들어, Appl을 통하지 않은 데이터 변경엔 속수무책이죠. 혹시 영어가 되시는 분 있으면, 앞서 제가 말씀드린 Constraint 기능을 추가해 달라고 각 DBMS 벤더에 연락 좀 해 주세요.)

      PS. 점이력으로 충분한 업무라면 그렇게 하는 것이 최선이라는 점엔 동감입니다.

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.11.19 02:02 신고  댓글주소  수정/삭제

      제가 이글을 쓴 배경에 대해서 말씀 드리겠습니다.

      종료일자의 장점을 나타낸 책과 글은 충분히 있습니다. 반면에 종료일자가 역정규화라는 생각을 가진 분은 별로 없었습니다.(물론 집단에 따라 다를 수 있습니다.) 더 심각한것은 DB쪽 튜닝을 아는 사람일 수록 종료일자를 비판없이 수용하는 것을 목격하였습니다. 흔하지는 않지만 튜닝을 모르는 모델러는 종료일자를 사용하지 않았습니다. 즉 종료일자가 대세라는 이야기 입니다. 이런 상황에서 제가 할 수 있는 일은 별로 없었습니다. 이 글에서 제가 주장하는 것은 대세가 아닙니다. 그래서 이 글도 쓸지 말지 고민을 하였습니다. 반발을 예상했기 때문입니다.

      이제 본문 내용에 대해 말씀드리겠습니다.
      업무적으로 점이력임에도 종료일자를 추가하여 선분이력으로 만들고자 하시는 분들의 대표적인 논리가 세가지 였습니다.
      1. 비교적 최근데이터를 구할 때는 종료일자 + 시작일자가 빠르다. 그래서 종료일자를 추가해야 한다.
      2. 특정 시점의 데이터를 보기위해서는 종료일자를 이용하여 BETWEEN을 쓰면 빠르고 쉽게 구할 수 있다.
      3. max값을 구할때 종료일자에 = '99991231' 만 주면 쉽고 빠르게 구할 수 있다.
      이 세가지가 종료일자를 추가해야 한다는 분들의 대표적인 주장들 입니다. 그래서 그게 아니라는 것을 조목 조목 나타냈습니다.

      성함을 알지 못하기에 선분이력님이라고 부르겠습니다. 물론 세가지 케이스 이외에도 선분이력님이 올려주신 SQL처럼 현실에서는 훨씬 더 어려운 케이스가 있다는것을 인정 합니다. 하지만 종료일자는 대세이고 그것의 장점이 이미 널리 알려져 있는 상황에서 종료일자의 장점을 차마 입에 담을 수는 없었습니다.(종료일자의 장단점을 비교하는 것이 더 좋았을 것이라고 말씀하셔서 답변을 드리는 것 입니다.)

      예전에 제가 근무하던 곳에서 원래 업무가 점이력임에도 불구하고 강제로 선분이력을 만듦으로 해서 두가지의 문제점이 발생했습니다. 레코드의 유일성 문제와 선분의 중복문제 입니다. 이 두가지 문제는 종료일자 때문에 생기는 겁니다. 애초에 고객번호 + 시작일시로 PK를 만들면 두가지 문제가 없어 집니다. 선분중복이란 종료일자가 생김으로 해서 문제가 발생하는 것입니다.

      제 글투가 너무 강했다는 것을 인정합니다. 제 글을 다시 읽어보니 너무 딱딱한 원칙론자 같이 느껴지네요. 다음 부터는 글의 어투를 신경써야 겠습니다. 너무 딱딱 하면 부러지게 마련이니까요.^^ 그런면에서 보면 "종료일자를 사용하면 안된다"가 아니라 다음처럼 수정하면 좋을것 같습니다.

      1.모델링단계에서는 역정규화를 안하는것이 원칙이며
      2.개발단계에서 SQL이 완성되고 테스트 데이터가 들어와서 성능이 떨어질때에도 무조건 종료일자를 추가하지 말고, 튜닝으로 해결 해야하며 그것이 안되면, 종료일자 추가 전후의 장단점을 비교한다.
      3. 종료일자를 사용하지 않을 때의 장점보다 사용할때의 장점이 더 크다면 데이터의 정합성을 위해 별도의 정합성을 체크하는 프로그램을 만들고, 정합성이 깨졌을 때 보정해주는 프로그램을 만든다면 종료일자를 적용해도 문제가 없다.
      4. 그렇지 않다면 종료일자를 사용하면 안된다.
      이렇게 정리하는게 옿을 것 같습니다. 종료일자에 대해 부정적인 입장은 어쩔 수 없을것 같습니다. 역정규화가 필요악(necessary evil)이라고 하더라도 악은 악이기 때문입니다. 물론 종료일자가 구세주가 될 수 있지만 현재상황에서 구세주임을 강조 할 필요는 없을 것 같습니다. 이미 알고 있을 테니까요.

      참 그리고 올려주신 SQL은 좋은 연구대상입니다. 언제 시간이 나면 빠르고 쉽게 처리 할 방법이 없는지 연구해 보겠습니다. 늦은시간 댓글 감사드립니다.

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.11.19 08:53 신고  댓글주소  수정/삭제

      선분이력님 장문의 댓글 감사합니다.
      정반합에 이른것 같습니다.^^

    • 무대리 2010.11.19 10:14 신고  댓글주소  수정/삭제

      두 분의 글을 읽고 여러가지 생각을 하게 됩니다. 방금 정리하신 오동규님 의견에 저도 한마디 덧붙이면...
      일반적인 역정규화는 말씀하신 프로세스로 진행해도 무리가 없고, 일부 속성만 중복 설계하는 것이므로 Appl에 미치는 영향도 크지 않습니다. 성능 문제가 나타난 프로그램만 새로 추가된 속성을 사용하도록 변경하면 되니까요. 하지만 이력모델을 바꾸는 문제는 차원이 180도 다릅니다.

      1. PK를 어떻게 구성할지에 관한 문제임
      2. 이력모델은 대부분 Key나 Main에 속하는 "기준성 테이블"에 적용함
      3. 기준성 테이블엔 대부분 이력관리가 필요함

      이런 기준성 테이블을 두고, "일단 개발해 보고 나중에 성능이 안 나오면 그때 바꾸자"는 주장이 과연 현실적인지요?
      어떤 이력 모델이 적합할지는 데이터와 시스템의 전반적인 특성을 분석해 가장 먼저 결정해야 할 사항이라고 생각합니다. 점이력을 기본으로 하고서 부분적으로 선분이력을 채택하거나, 선분이력을 기본으로 하고서 부분적으로 점이력을 채택하거나. 이것이 데이터 아키텍트의 역할 아닐까요? 일단 10인승 엘리베이터로 설계해서 건물을 짓고 나중에 20인승으로 바꾸는 건 불가능합니다. 어린이나 장애인을 위해 조작 버튼을 낮게 추가 설치하는 건 가능하겠지만...

  9. 무대리님에게 2010.11.19 20:35 신고  댓글주소  수정/삭제  댓글쓰기

    일반적인 역정규화는 무리가 없지만 변경이력은 180도 다르다는 말은 찬성할 수 없는데요.
    왜냐하면 key 나 main은 기준성 테이블인데 반해 이들의 자식인 변경이력은 기준성이 아닙니다. 기준성은 거의 모든 화면에서 참조하거나 많은 화면에서 참조됩니다. 일반적으로 변경이력은 참조하는 화면이 한정 됩니다.

    두번째로 변경이력에서 pk를 어떻게 할 것인지의 문제는 변경이력 자체의 정합성만 따지면 됩니다. 변경의력의 자식테이블이 없기 때문입니다. 일반적으로 pk 가 변경되면 key를 상속하는 자식테이블에 모두 반영을 해줘야 하기 때문에 대단히 critical 합니다. 하지만 변경이력은 아닙니다. 자식이 없습니다.

    • 무대리 2010.11.20 12:27 신고  댓글주소  수정/삭제

      변경이력은 거의 참조하지 않는다는 걸 보니 모델링할 때 역정규화를 아주 많이 하는 분이시군요.
      거래성 데이터에 기준성 정보를 역정규화해 두지 않았다면, 기준성 테이블보다 그 변경이력 테이블에 대한 참조가 더 많아지게 마련입니다. 아래 SQL들은 매우 일반적인 형태고 모두 변경이력을 참조하고 있습니다.
      이렇게 조회가 많은 변경이력 테이블의 PK 구조를 일단 개발해 보고 나중에 바꾸자고 하시니 현실적이지 않다고 이의를 제기한 것입니다.
      만약 아래 사용된 종목변경이력, 고객변경이력, 상품변경이력을 선분이력으로 설계했다면 SQL이 얼마나 간단해지겠습니다. SQL이 간단한만큼 성능향상에도 도움이 됩니다. 참고로 이들 기준성 정보는 자주 변경되지 않기 때문에 update & insert 방식으로 이력을 쌓더라도 시스템 전반에 미치는 성능 부하는 적습니다.

      -- 11월 20일자 주식체결내역 조회
      select a.계좌번호, b.종목명, a.호가, a.체결가, a.체결시각, a.체결수량, b.시가, b.고가, b.저가, b.종가
      from 체결 a, 종목변경이력 b
      where a.거래일자 = '20101120'
      and b.종목코드 = a.종목코드
      and b.변경일시 = (select max(변경일시) from 종목변경이력 where 종목코드 = a.종목코드 and 변경일시 <= a.거래일자 || a.체결시각)
      ;

      -- 11월 20일에 접수된 불만성 상담건 조회
      select b.고객명, b.고객상태코드, b.고객등급코드, c.상품명, c.상품가격, c.상품유형코드, c.공급처, a.상담사유코드, a.상담내용, a.처리상태
      from 상담 a, 고객변경이력 b, 상품변경이력 c
      where a.상담분류코드 = 'VC' -- 불만성상담
      and a.상담일시 like '20101120%'
      and b.고객번호 = a.고객번호
      and b.변경일시 = (select max(변경일시) from 고객변경이력 where 고객번호 = a.고객번호 and 변경일시 <= a.상담일시)
      and c.상품번호 = a.상담상품번호
      and b.변경일시 = (select max(변경일시) from 상품변경이력where 상품번호 = a.상담상품번호 and 변경일시 <= a.상담일시)
      ;

      -- 11월 20일 주문내역 조회
      select o.고객번호, o.주문일시, o.배송지, d.상품코드, d.주문수량, p.상품가격, (d.주문수량*p.상품가격) 주문금액
      from 주문 o, 주문상세 d, 상품변경이력 p
      where o.주문일시 like '20101120%'
      and d.주문번호 = o.주문번호
      and p.상품번호 = d.상품번호
      and p.변경일시 = (select max(변경일시) from 상품변경이력 where 상품번호 = d.상품번호 and 변경일시 <= o.주문일시)
      ;

      PS) 재밌군요. 종료일자를 둔 선분이력이 역정규화이기 때문에 필요악(necessary evil)이라며 공격을 받았는데, 그것을 안 쓰고 기준성 테이블 위주로 쿼리하도록 하려면 거래성 테이블에 많은 역정규화가 필요해지네요.

    • 무대리 2010.11.20 13:54 신고  댓글주소  수정/삭제

      "DB쪽 튜닝을 아는 사람일수록 종료일자를 비판없이 수용하는 것을 목격했다. 튜닝을 모르는 모델러는 종료일자를 잘 사용하지 않는다."는 오동규님 말씀에 한 말씀 덧붙이겠습니다.

      DB쪽 튜닝을 아는 사람일수록 종료일자를 비판없이 수용하는 것이 아니라 점이력을 사용하면 쿼리가 위와 같이 복잡해지고 성능에도 별로 좋지 않다는 사실을 잘 알기 때문에 선분이력을 선호하는 것이 아닐까요? (물론 튜닝실력이 어설프기 때문에 무비판적으로 선분이력을 따르는 분들도 계십니다.)
      튜닝을 모르는 모델러는 그런 사실을 잘 모르니까 간단하게 점이력으로 설계하는 것이 아닐까요? (물론 튜닝을 잘 알면서 점이력을 선호하는 분들도 계십니다.)

      실제 프로젝트를 해 보면 현업 담당자의 요구사항은 매우 복잡하고 다채롭습니다. 그런 요구사항을 잘 수용하고 확장성 있는 시스템을 구축하려면 기준성 데이터에 대한 이력관리가 매우 중요하고, 그것을 잘 활용할 수 있도록 설계해야 합니다. 어떻게 하는게 효과적일까 고민하다가 누군가 선분이력을 고안했고, 많은 분들이 그것을 받아들여 잘 활용하고 있습니다.
      물론 선분이력이 어울리지 않는 부분에까지 그것을 적용해 역효과를 내는 분들도 계시겠지만, 그런 경우를 보셨다고 해서 선분이력 무용론을 펼치시는 것은 좀 지나치다는 생각이 듭니다.
      몇 줄짜리 간단한 테스트 결과만으로 기존 방식이 잘못됐다고 자꾸 주장하지 않으셨으면 좋겠습니다. 스크린 골프 실력이 그린에서도 잘 먹힐까요?

      앞선 글에서 주장하신 index_desc 힌트와 rownum = 1 조건 불가론도 그렇습니다. 옵티마이저는 매우 보수적으로 의사결정을 하기 때문에 first_row(min/max) 오퍼레이션이 작동하지 않는 경우가 있습니다. 그런데 성능 요건이 충족돼야 고객이 만족한다면 어쩌시겠습니까? 그러면 "불가피하게" index_desc 힌트와 rownum 조건을 써야 할 수도 있습니다. 그럼에도 "어떤 이유로도 받아 들여서는 안 된다"시니 참 답답한 마음이 듭니다. 제시하신 쿼리 패턴이 모든 업무 유형을 커버할 수 있다고 생각하셨는지요? 이 부분은 제가 시간날 때 그쪽 코너에 다시 올릴테니 거기서 의견 나누시죠.

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.11.21 19:55 신고  댓글주소  수정/삭제

      안녕하세요. 오동규 입니다.
      두분이서 치열한 토론을 하셨네요. 그리고 저에게도 한마디 하셨네요.
      "스크린 골프실력이 그린에서 잘 먹힐 까요?"라고 하셨군요. 제가 실력이 별로 없다는 것은 인정합니다. 그리고 저에게 그러셔서 다행입니다. 다른분들에게 그러시면 곤란 합니다.^^ 토론이 자칫 감정적이거나 특정인을 비하하는 것으로 흐를 수 있는것 같습니다. 될수있으면 감정을 배제하시고 논리적으로 대응해 주시길 바랍니다.

    • 무대리 2010.11.21 23:10 신고  댓글주소  수정/삭제

      오동규 님이 실력 없다고 얘기한 것이 아니니 오해 마시기 바랍니다. 간단한 테스트 결과만으로 설명하기에는 시스템과 업무적 특성이 매우 복잡하다는 점을 얘기하고 싶었습니다. 저도 골프는 쳐 본 적이 없지만, 필드에 나가면 바람과 지형, 잔디 상태 등에 따라 돌발 변수가 많다고 들었습니다. 시스템 개발도 마찬가지라고 생각합니다.
      약간의 관리적 비용이 생기는 것을 알면서도 그것을 감수하고 차선(?)을 선택하거나, 전략적인 판단에 따라 그렇게 해야 하는 경우가 생깁니다. 그런 점을 감안하면서 기존 방식을 비판하셨으면 좋겠습니다. 물론 본래 의도가 copy & paster를 비판하려 한 것임은 저도 잘 압니다.
      기분 나쁘셨다면 사과드리죠.

  10. 나그네 2010.12.22 00:00 신고  댓글주소  수정/삭제  댓글쓰기

    우선 좋은 글 공유해 주시는 오동규님께 감사를 드리고 있습니다.
    마지막으로 선분이력님과 오동규님의 토론 때문에 점이력, 선분이력을 좀 더 상세하게 이해할 수 있게 되어서 감사드립니다.

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2010.12.22 11:22 신고  댓글주소  수정/삭제

      나그네님 반갑습니다.
      점이력과 선분이력을 상세히 이해하셨다니 글의 의도대로 된것 같습니다. 앞으로도 다양한 소통을 하겠습니다.

      즐거운 크리스마스 되시기 바랍니다.
      감사합니다.

최근에 몇몇 지인들과 모델링에 관한 즐거운 토론이 있었다.
화두는 두가지 였다.

1."논리 데이터 모델과 물리데이터 모델의 차이가 심하면 모델링이 잘못된 것인가? "

2."논리모델 단계에서의 성능을 고려한 논리모델링이 있으냐? 없느냐?"


여러가지 의견이 있었다.
1번에 대한 의견중에 한가지는 논리 데이터 모델과 물리 데이터 모델의 차이가 심하면 물리모델이 잘못되었다는
것이다.
왜냐하면 "물리모델링시에 역정규화및 목정성 테이블들이 많이 나오면 정합성, 확장성에 역행된다"는 것이다.
2번에 대한 의견중에 한가지는 "God of Modeling"(모델링의 신)형이다.
즉 "어짜피 물리모델링에서 역정규화 될테니 논리모델링의 마지막 단계에서 미리 하자"는 것이다.    


하지만 필자의 생각은 조금 다르다.
튜닝과는 달리 모델링의 세계에서는 정답이 하나일수는 없다.
다시말하면 모범답안은 있지만 누구의 의견도 틀렸다고 볼수는 없는것이다.
따라서 아래에서 제시한 필자의 의견 또한 관점에 따라서는 답이 아닐수 있다.

1번 ,2번에 대하여 필자의 항상 필자는 아래와 같은 말을 해왔다.

"논리모델은 정합성 확장성을 보장하는 위주로 극한까지 가보는것"

"물리모델은 정합성 확장성을 해치는것을 최소화 하면서 성능위주로 극한까지 가보는것"

필자는 위와 같은 생각을 논리모드,물리모드라고 부른다.


즉 논리단계에서는 성능생각을 머리속에서 지우고 물리단계에서는 논리모드를 머리속에서 지우는것이다.
하지만 훈련이 되지않으면 논리모드,물리모드의 변환이 상당히 어렵다.
예를 들면 9월 까지가 논리모델링 단계이고 10월 부터는 물리모델링 단계라고 가정하면
9월 말일까지는 논리모드만 생각하다가 갑자기 10월 1일 부터 물리모드로 전환하기가 어렵다는 것이다.
또하나의 어려움은 DBA 출신이 모델링을 하면 논리모델링시 성능생각을 자연스럽게 하게되고
DBA 출신이 아닌 이론적 모델러의 경우 물리모델링시에 정합성,확장성을 저해하지만 성능이 좋아지는
역정규화 및 집계속성, 계산속성등을 과감히 선택해야할 때가 있지만 이를 두려워 한다는 것이다.
이래서는 모델링이 잘될수가 없다.

어찌되었건 위에서 설명한 논리모드 및 물리모드를 적용하여 1,2번에 대하여 필자가 답을 한다면 아래와 같다.

1번 :
논리데이터 모델과 물리데이터 모델의 차이가 심하면 모델링이 잘된것이다.

왜냐하면 논리모델링시에  논리모드로만 생각했다면 물리모델링시 성능관점에서 손볼것이 꽤 있을것이고
그렇다면 논리와 물리는 차이점이 꽤 있을것이기 때문이다.
반대로 논리단계에서 논리모드 + 물리모드 둘다 생각했다면 논리와 물리의 차이는 거의 없다.
필자는 이경우를 논리모드 <--> 물리모드 를 변환하는 훈련이 부족 하다고 보는것이다.

2번:
논리 모델링 단계에서는 논리모드로만 생각하면 된다.
즉 논리모델링 단계에서는 성능 생각을 지워야 한다.

편집 후기 :
한 지인은 필자가 논리모드/물리모드라고 이야기 했더니 E 모사의 사장님이 이야기한 "단위삼각형의 법칙"
이라고 이야기 하는데 그것과는 전혀 다른 이야기 이다.
이는 필자의 독창적인 용어임을 밝혀둔다.
사실 필자의 ID 의 앞자리인 Extreme 도  "~극한 까지 가보는것" 의 극한 에서 나온것이다.

여러분들이 1번 2번에 대한 답을 한다면?
필자는 여러분들의 의견을 듣고싶다.
신고
Posted by extremedb

댓글을 달아 주세요

  1. 금땡이 2008.10.20 16:47 신고  댓글주소  수정/삭제  댓글쓰기

    공감하는 내용입니다.
    현실적으로 논리모델과 물리모델의 GAP차이를 설계단계에서 얼마나 극복해야 하느냐가 프로젝트 성공의 관건 중 하나가 되고 있는데요 모델러와 DBA의 협업이 무엇보다도 중요하다고 생각이 드는군요.
    DBA의 역할이 무엇보다 중요해지고 있고 이 중요성이 고객사와 프로젝트 PM이 인지하고 있었으면 합니다.
    심지어 데이터 모델러에게 물리모델과 Index Design 까지 요구하는 곳도 있으니 말입니다.

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2008.10.22 12:59 신고  댓글주소  수정/삭제

      금책임님 이야기 대로 이론은 제가 써놓은 글대로 이지만
      현실에서(실제 프로젝트시) 얼마나 잘 지켜지는지가 관건이라 할수 있습니다.
      또하나의 문제점은 정합성 확장성 위주로 논리모델을 확정하고 물리단계에서 여러가지 성능향상 기법을
      통하여 논리모델과 물리모델의 모습이 많이 달라지면 개발자들이 논리모델은 사용하지 않는다는 것 입니다.
      심지어 DBA 들도 보지를 않더군요.
      오직 컨설턴트 만을 위한 논리모델이라....
      음...이문제를 어떻게 할지...

  2. 백면서생 2008.10.20 17:16 신고  댓글주소  수정/삭제  댓글쓰기

    먼저 좋은 내용 잘 읽었습니다.
    결론 부터 말씀드리면 저도 extremedb님과 같은 생각입니다. 논리모델과 물리모델은 엄연히 구분된 것이니까요.설명을 잘해 놓으셔서 달리 덧붙힐 말이 없네요.^^
    논리모드/물리모드란 말도 마음에 드는군요.(상당히 용어고르시는 센스가 있으신듯..10그램^^)
    삶의 행복이 인생의 목적이라면 20대,30대,40대,50대 의 삶을 바라보는 태도(attitude)가 다 다르듯이 모델링도
    공학적으로 그 태도를 mode라고 볼수 있지 않나 생각이 드네요.
    참고로 E사의 대표이사께서 말씀하신 '단위 삼각형의 법칙'은 개인적으로 생각해 봤을때,
    Devide & Conquer 전략의 하나인거 같습니다. 전체적인 목표나 업무량을 일정단위로 나눠서
    그것을 하나씩 정복해 나감으로써 일의 효율을 높히는 방식이라 생각됩니다.
    따라서 앞의 엔터티 정의가 잘못 되어있으면 뒤에 수행되는 릴레이션처리(conquer)를 아무리 잘 하려고해도
    밑에 깔린 삼각형이 부실히 정복되었다면 무너지게 되니까요.
    맥킨지에서 말하는 MECE나 자기무당착도 이 이론(?)에 기초한걸로 알고 있읍니다.
    extremedb님의 말씀하신 논리모드/물리모드는 각단계를 바라보는 태도이고,
    '단위 삼각형의 법칙'은 지극히 물리적인 일의 양을 기초로한 PSA(Problem Solving Approach)방식이라 생각이 드네요.^^

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2008.10.20 12:41 신고  댓글주소  수정/삭제

      백면서생님이 차이점을 정확하게 지적해주셨습니다.
      오랜만에 모델링 고수분들과 대화를 하니 아주 즐겁네요.^^
      블로그에 모델링 이슈도 간간이 올리도록 하겠습니다.
      참 그리고 저는 DBA 는 아닙니다.
      DB관련 컨설턴트라 DBA 들과 일은 같이 합니다.
      아이디 때문에 많이 오해들 하시네요.
      욱짜님도 그렇구요.^^
      그런데 아이디도 알고보면 extremeDBA 가 아니라
      extremedb 입니다.

    • 백면서생 2008.10.20 17:16 신고  댓글주소  수정/삭제

      헛.. 이런 실수를...^^; 수정해야 겠네요..

      그리고 저 고수 아닙니다. 그냥 비슷한 업종의 장삼이사 일뿐~^^

  3. 금땡이 2008.10.22 10:13 신고  댓글주소  수정/삭제  댓글쓰기

    현업 담당자, DBA, 개발자 모두 마인드의 전환이 필요하다고 생각 합니다.
    삼풍 백화점과 성수대교를 다시 무너뜨릴 어마어마한 일을 지금도 하고 있는 것이지요.
    저 또한 손바닥으로 하늘을 가리는 형국이지만요.
    하지만 진보를 위한 과정이라고 믿어 의심하지 않습니다.

  4. 김대훈 2009.06.23 11:03 신고  댓글주소  수정/삭제  댓글쓰기

    안녕하세요. 모델링에 대한 좋은 글을 보게 돼서
    제 생각 공유 또는 의견을 듣고 싶습니다.

    저는 운영을 중심적으로 해왔습니다.
    그러다보니, 제 관점은

    물리모델에서 논리모델에 대한 갭을 없애기 위한 노력이 중요하다고 생각합니다.
    목표를 물리모델에서 논리를 최대한 많이 표현해야 된다는 관점입니다.

    개발 완료후에 비지니스는 계속 변경되어지는데, 별도로 논리모델에 반영하지 않고,
    물리모델만 변경하고 운영이 되는(게으른) 현실 때문에
    물리모델에 많은 논리 표현으로 도움 될 때가 있는 것 같습니다.
    (이미 extremedb님께서 지적하신 것처럼 잘 보지도 않는 논리모델을 위해서
    전체 모델을 2개씩 유지보수한다는 것은 효과성과 현실적인 어려움이 있는 것 같습니다)

    이러다보니, 논리모델과 물리모델의 갭을 인정한다라는 공식적인 공표를 해야 되는 뼈아픈 현실과
    논리 표현기법들이 물리모델에 가능한 많이 표현하다보니,
    어느 물리Notation(사실, 물리Notation이란 것은 없지만..)에도 없는 표현들이 난무해서,
    몇몇표현들은 개발자가 익숙해지는데까지는 시간이 소요되는 보완해야 되는 단점이 있는 것 같습니다.

    이것은 운영적인 관점에서 시작된 제(우리) 방식이였는데,
    재개발 프로젝트가 진행될 때, 여기에 익숙해져 있다보니,
    신규에서도 논리모델 관점보다는 물리모델에 논리적인 표현을 많이 추가하는
    방식으로 개발되는 비정상적인 방식이 진행되어서,
    논리와 물리는 다른 것이라는 것을 인식시키는데 고생한 기억이 있습니다.
    즉, 논리모델은 개발에만 사용되는 임시적인 산출물로 치부되어 버린 것이죠.
    이런 것에 대한 고민이 있습니다.
    현재 차기에 개발이 될 때에는 논리모델을 만들고, 그 위에 물리 표현을 하면 어떨까 라고
    생각하고 있습니다.

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2009.06.23 13:10 신고  댓글주소  수정/삭제

      대훈님 반갑습니다.
      비지니스 변경에 대한 논리및 물리 모델관리는 말씀 하신것 처럼 어려움이 많이 있습니다.
      하지만 3가지 관점에서 접근하는것이 어떨까요?
      첫번째로는 비지니스가 변경되면 논리모델과 물리모델에 반영 해야 한다는 것입니다. 이것은 성능개선의 방법으로 인한 역정규화나 추출속성 및 계산속성 추가 혹은 집계테이블의 추가를 이야기하는것이 아닙니다. 이런것들은 비지니스가 변한것이 아니지요. 비지니스 자체의 변경을 이야기 하는것 입니다.
      두번째로는 데이터의 정합성 관점인데요. 예를 들어 추출속성값이 틀려질수 있습니다. 그때에는 원본속성을 찾아서 다시생성 시켜주어야 하는일이 발생합니다.
      이때 논리모델이 없으면 원본이 되는 엔티티를 찾기가 어렵다는 겁니다. 여기저기에 역정규화 되어 있기 때문에 어느것이 원본인지 알기 힘듭니다. 그래서 논리모델에서는 원본만을 관리하고 물리모델에서는 컬럼의 description에 추출속성의 생성및 수정규칙을 관리한다면 정합성이 어그러 졌을때 원본에서 데이터를 추출하기가 쉬울 겁니다.
      논리모델을 관리해야 하는 3번째 이유는 논리모델에서 DBMS 의 종속성을 최소화 해야 한다는 겁니다.
      예를들어 만약 DBMS 가 MS-SQL 서버 2000 이고 조직 테이블이 3단계의 계층구조로 되어 있다면 물리모델에서 자기참조모델로 (Recursive) 만들기 힘듭니다. 왜냐하면 connect by 기능이 없기 때문입니다. 따라서 물리모델은 3개로 가져가는것이 맞습니다. 왜냐하면 물리모델은 DBMS 에 완전종속이기 때문입니다. 하지만 논리모델은 그래서는 안되겠지요.
      제가 3가지를 언급한 이유는 저를 비롯한 많은 컨설턴트들이 이런생각을 하고 있기 때문입니다.

      하지만 모범답안이 꼭정답일수 없듯이 이런 3가지 관점을 고민하시고 응용하시고 그중에서 버릴것이 있다면 과감히 버리시기 바랍니다.

  5. Favicon of http://www.modelingworld.co.kr BlogIcon 2009.06.30 11:29 신고  댓글주소  수정/삭제  댓글쓰기

    예전에 이글을 읽었는데 아직도 대화가 오고가고 있군요...
    저도 손이 근질근질 하여, 짧은 의견을 얘기해 보겠습니다.
    우선 각 모델의 개념을 우선 잡고 시작하도록 하겠습니다.

    --------------------------------------------------------------------------------------------------------
    - 개념모델 : 핵심Entity들간의 관계를 나타낸 모델
    - 논리모델 : 실제 모델링이 완성된 단계로서 모든 Entity, Relationship, Attribute가 도출되고, 모델검증이 완료된 상태
    - 물리모델 : 논리모델을 특정 DBMS에 맞게 최적화한 모델
    --------------------------------------------------------------------------------------------------------

    본문의 1,2번에 대해 얘기를 먼저 꺼내보도록 하겠습니다.
    1번 :
    저는 차이는 크게 중요하지 않다고 생각합니다. 이유는 차이가 클수도 있고, 아닐수도 있기때문입니다.
    즉 상황에 따라 틀리다는 말이죠. 각 모델링 단계에 충실하는게 중요하지 차이는 중요하지 않거든요.(이 말은 모델간 Alignment가 중요하지 않다는 말은 절대아닙니다)
    2번 :
    당연히 논리모델에서 성능이 고려되어야 합니다.
    좀더 정확히 얘기를 하면 순서가 바뀌는데요. 좋은 논리모델은 결국 좋은 성능을 가져올 수 밖에 없다는 말이죠.
    물론 물리모델링 단계에서 좀더 성능이 향상될 수 있지만 중요한것은 대부분 논리모델링 단계에서 결정되기때문입니다.
    => 써놓고 보니 extremedb님과 사상은 같지만 표현 방법만 틀린것 같군요 ^^;

    금땡이님과 김대훈님이 말씀하신 갭에 대해 잠깐 얘기하면 모델간 갭이 존재한다면 이미 두 모델 중 하나는 사용할 수 없는 모델이라고 생각합니다. 당연히 없는것 보다는 낫겠지만요...
    저는 바라보는 관점이 조금 틀린데, "갭을 없애기 위해 노력한다..."라는 시점은 벌써 모델링이 잘못되어가고 있는것같고, 갭이 없이 모델링이 되어야 한다고 생각합니다. 말장난 같지만 굉장히 중요하다라고 생각하거든요.
    모델간 Alignment유지가 모델링에서 가장 중요시 되어야 되는것 중 하나라고 생각합니다.
    개념모델이 강조되는 시점에서 2개씩 유지보수되어야 하는게 어렵다면...현실이 안타까울 따름이지요...
    그리고 논리와 물리모델을 따로 생각하여 "2개씩"이라 표현한것은 어떻게 보면 조금 부적절하지 않나 생각합니다.
    개념,논리,물리모델을 하나의 모델로 생각해야 맞지않을까요?

    좀더 전반적인 이야기를 해보면 지금까지 논리모델과 물리모델의 차이는...
    "한글명을 영문으로 바꾸어 놓은게 물리모델 아니야?" 아직 이런 생각을 가지고 있는 사람이 있어? 라고 할지모르지만
    아직도 정말 많은 개발자가 이런생각을 가지고 있다는게 문제입니다.
    중요한건 개발에 참여하는 모든인력(PM, Architect, coder등)의 Data를 바라보는 마인드 뿐만이 아닌,
    교육환경 자체를 바꾸어야 한다고 생각합니다. 모델러가 식별/비식별관계와 1~3정규화를 말하지 못 하는게 작금의
    현실이니까요...이런 모델러가 설계한 모델은 안봐도 비디오 아니겠습니까?
    많이 나아지고는 있지만 여전히 Data를 천대시 하고 있는게 가장 큰 문제이지요.
    개발자까지 단순히 좀더 오래 이쪽에 있을 수 있고, 돈을 좀더 많이 벌 수 있기에 Data쪽으로 가고싶다는 얘기하는 실정이니 다른사람들은 어떨까요?
    두서없이 적어봤습니다. 부적절한 표현이 있었다면 넓은 아량으로 이해해 주세요~^^

    • Favicon of http://scidb.tistory.com BlogIcon extremedb 2009.06.30 16:28 신고  댓글주소  수정/삭제

      개념모델을 설명하신걸보니 오픈메이드 컨설팅 분이 아니신가요?
      엉님 말씀에 답변드립니다.
      먼저 제 생각이 정답이 아닐수 있음을 밝혀둡니다.
      1번 :
      1번은 차이가 큰 경우를 이야기한 것입니다. 따라서 1번의 경우는 차이가 적은 경우를 생각 안하셔도 됩니다.

      2번 :
      논리모드 와 물리모드는 반드시 분리되어야 합니다.
      논리모델에서는 성능생각을 지워야 합니다.
      또한 성능을 고려하기 위해 정규화를 하는것이 아닙니다. 논리모델의 원래 원칙인 정합성 확장성을 하다보니 정규화가 되고 부수적으로 성능이 향상되는 경우가 있는것 입니다. 물론 엉님도 뒷부분에 이러한 말을 언급하셨지만 처음에 "성능을 고려하여 논리모델링을 해야한다" 라는 말을 다른사람이 보면 오해할 소지가 다분하기 때문입니다. 그리고 정규화를 하면 성능이 향상된다는 말도 조심스럽군요. 이유는 성능이 저하되는 경우가 많이 있기 때문입니다. 실제 튜닝시 50% 의 경우는 성능이 느려집니다. 물론 향상되는 경우도 마찬가지 이지요. 따라서 "정규화를 하면 성능이 향상된다" 라는 말 보다는 "과도한 역정규화는 오히려 성능을 저해하며 꼭 필요한곳(적재적소) 에 역정규화를 한다면 성능향상을 이룰수 있다." 라고 하는것이 좀더 적절한것 같습니다.

      물론 이것은 제생각 입니다.