#title 속성에 대하여.. [[TableOfContents]] 이제부터는 물리적인 구현에 대해서도 약간은 생각을 해주어야 한다. 어떤 것이 가장 유리한 것이지 판단하는 기준을 잘 세워야 한다. ==== 속성의 개념 ==== 속성은 개체집합의 관리하고자 하는 항목들(2개 이상)을 의미한다. 즉, 사물의 본질을 이루는 성질을 말하는데, 그것이 없이는 그 사물을 생각할 수 없는 조건을 의미하기도 한다. 보통 이러한 것은 물리적으로 Not Null로 선언한다. Null 허용 컬럼들의 경우는 옵션에 해당한다. 속성은 가공되지 않은 소스이다. 즉, 데이터이다. 데이터를 가공하면 사용자가 원하는 정보가 될 수 있음을 말한다. 간혹 가공된 소스가 속성이 될 수도 있으나 이것은 본질을 이루는 것은 아니다. 속성도 하나의 집합이다. 앞서 언급했듯이 데이터베이스는 집합으로 시작해서 집합으로 끝난다. 그러므로 속성도 집합임에는 틀림없다. 예를 들어 “사람”의 속성으로 “성별”이 있을 수 있다. 성별은 다음과 같이 나타낼 수 있다. 성별 = {남, 여} 이와 같이 속성은 하나의 집합이다. 보통 도메인을 이야기하면 해당 속성에 들어갈 수 있는 값(데이터)의 범위를 이야기한다. 역시 집합을 어떻게 정의하느냐에 따라서 속성의 형태가 틀려진다. attachment:property01.jpg 중요한 것은 어떤 것을 속성의 집합으로 보아야 하는가에 대한 물음이다. 위 그림의 “년 + 월 + 일” 이 하나의 속성으로 “날짜” 라고 볼 것인가 아니면 “년”, “월”, “일” 이렇게 모두 떨어진 것이 속성인가? 어떤 것을 속성으로 보느냐는 것은 철저히 업무에 따라서 틀려진다. 이제부터 그것에 대한 답을 찾아보자. ==== 속성의 표현 ==== attachment:property02.jpg 역시 옛날의 표현방식이다. 대학에서는 아직도 이러한 표기법을 사용하기도 한다. 실무에서는 좀처럼 사용하기 힘든 방식이다. 표현공간이 너무 넓을 뿐더러 모델의 복잡성이 증가하기 때문이다. ==== 관계와 속성 ==== 주의해야 할 것이 있다. 그것은 관계의 논리적인 또는 물리적인 표현이 속성이 아니라는 것이다. 아래의 DDL 문장을 보자. Report_no 와 같은 경우는 Foreign Key 이다. 즉, 이것은 관계의 물리적인 표현이지 속성이 아니라는 것이다. {{{ create table abbreviation( abbr_no char(6) , report_no integer not null unique , primary key (abbr_no) , foreign key (report_no) reference report ); }}} 단지 표현의 문제가 있을 뿐이다. 키개체집합의 경우는 많은 관계를 가진다. 관계가 매우 많은 경우는 묵시적인 관계를 가지도록 할 수도 있다. 모델러의 역량이다. 해당 개체집합을 도출하고, 속성을 정의할 때 관계를 표현해 줌으로써 의미가 더 명확해 질 수 있으므로 속성으로 표현할 수도 있다. 하지만 관계라는 것을 확실히 표시해 두는 것이 좋다. 많은 경우 개체인지 속성인지 관계인지 혼돈하는 경우가 많이 있다. 이는 각각의 개념이 아직도 미흡하기 때문이다. 또한 정의가 제대로 이루어지지 않았기 때문이기도 하다. ==== 독립성 ==== 속성은 독립성을 가져야 한다. 속성이 독립적이라는 것은 속성의 자체만으로도 어떤 의미가 있어야 한다는 말이다. 고객의 생일에 맞추어 “고객님의 00번째 생일을 축하합니다.” 라는 생일 축하 문자메시지를 고객의 핸드폰에 보낸다고 가정하자. 그렇다면 생년월일의 속성이 “년” , “월”, “일” 이 속성이 되는 것이 맞을까? “년월”, “일” 이 맞을까? 아니면 “년” , “월일” 이 맞을까? 이것은 해당 조직에서의 “원자값”이 무엇인지 찾으면 답이 나온다. 원자값은 “년”, “월”, “일” 과 같은 것이 아니라 “의미”의 원자값을 말한다. 생년월일의 경우는 해당 고객의 “생년월일”이 모두 모여 있어야 의미가 있다. 그러나 이것은 고객의 관점이다. 해당 기업의 관점에서는 해당 “월일”에 해당하는 고객의 “나이(현재년도 – 생년)”가 필요하다. 그러므로 “년” , “월일” 이 적당한 원자값이라는 것이다. 즉, 독립적으로 의미를 가질 수 있는 원자값 단위가 속성이라는 것이다. 아래의 그림을 보자. 아래의 그림은 속성의 또 다른 개념을 설명하고 있다. 속성은 식별자에 종속적이야 하며, 모든 속성은 같은 추상화레벨을 가져야 한다는 것이다. attachment:property03.jpg 관계 이론에서는 속성들이 식별자에 종속되어 같은 레벨(형제)로 어떤 주제로 묶여져 있으므로 ‘릴레이션’이라고 부른다.(용어주의) ==== 속성명칭 ==== 속성의 명칭을 결정하는 것은 매우 중요하다. 개체집합과 마찬가지로 속성(집합)의 정의와 명칭을 정의하는 것은 매우 중요한 작업이다. 아래의 그림을 보자. 아래의 그림은 속성의 명칭을 나타낸 것이다. attachment:property04.jpg 학점의 경우를 살펴보자. 학점하면 생각나는 것이 무엇이냐를 여러 사람들에게 물어보면 4.5와 같은 숫자를 생각하는 사람도 있고, A, B, C와 같은 학점(등급)을 생각하는 사람도 있다. 중요한 것은 모델러가 이러한 것을 결정하는 것이다. 4,5와 같은 점수를 나타내는 것인지 A, B와 같은 등급의 점수를 나타내는 것인지를 사용자에 의해서 결정을 하도록 유도해야 한다. 상태의 경우도 살펴보자. 뜻하는 바를 정확히 알 수 없다. 무엇의 어떤 상태인지를 명확히 파악해야 한다. 특기1, 특기2의 경우는 각각이 속성이라기 보다는 특기가 다중값 속성이다. 이렇게 단일명사로 표현하다 보면 그 뜻을 알 수 없다. 그러므로 복합명사로써 속성의 명칭을 선택하는 것이 중요하다. 구분과 같은 경우도 “남녀구분”과 같이 복합명사로 명칭을 부여하면 훨씬 더 명확해 짐을 알 수 있다. 속성의 명칭은 가능하명 속성의 명칭만 보고 알 수 있도록 하는 것이 좋다. 작업의 양은 많아지겠지만 속성의 정의도 개체집합의 정의와 같이 명확히 정의해주어야 한다. 속성길이, 타입만이 중요한 것은 아니다. 속성의 도메인을 명확히 정의하는 것이 중요하다. ==== 속성의 정의 방법 ==== 속성을 정의하는 방법은 개체집합을 정의하는 방법과 유사하다. 우선 속성의 후보를 선정 후 해당 속성에 대해서 하나씩 집고 넘어가는 방식이다. 그러나 속성을 정의할 때는 개체집합과 같이 분류하지는 않는다. 대신에 개체집합의 단위로 속성을 그룹화한다. 그 다음 속성의 자격검증에 들어간다. 속성의 자격검증은 다음의 질문으로 한다. * 원자값을 가지는가? (의미있는 정보 단위) * 단일값만을 가지는가? * 유도속성인가? * 관리상의 문제는 없는가? ==== 속성 후보의 선정 ==== 속성의 후보 선정 역시 개체집합의 후보를 선정할 때의 소스와 같다. 중요한 것은 구시스템의 속성을 그대로 가져가지 말라는 것이다. 일단은 속성이라고 판단하지 말고 후보만 수집한다는 생각을 해야 한다. 업무문서의 경우는 가공된 값(계산된 값)을 많이 가진다. 그러므로 어떤 데이터 소스에서 도출된 값인지를 파악하는 것이 중요하다. 현업 사용자의 경우는 업무를 많이 알고 있으므로 관리해야 할 속성을 많이 알고 있다. 그러나 현업 사용자들도 확실하지는 않으므로 역시 후보수집에만 초점을 맞추어야 한다. attachment:property05.jpg 용어 사전 또는 레포지토리 이용하는 것도 좋은 방법이다. 만약 속성에 대한 지식이 부족하다면 전문서적 또는 관련 서적을 참고하는 것도 도움이 된다. 또한 타시스템 밴치마킹 하여 미래에 관리해야 할 속성이 있는지에 대한 데이터를 수집하는 것도 좋다. ==== 개체집합 단위로 그룹화 ==== 속성을 개체집합 단위로 그룹화 할 때는 엔터티 집합의 도메인 내에 있는 관리 속성인지 파악하는 것이 주 업무이다. 현 단계는 어떤 속성이 어떤 엔터티 집합에 관리 속성인가만 파악하면 되는 단계이므로 현 단계를 충실히 수행하면 된다. ==== 원자값 ==== 원자값이란 의미 있는 정보의 단위이다. “의미 있는 정보의 단위” 란 무엇인가 알아보도록 하자. 아래의 그림은 “성명”에 대한 내용이 나온다. 그림처럼 성과 명을 분리해야 하는가? 아니면 성과 명을 하나로 두어야 하는가? attachment:property06.jpg 이에 대한 답은 사용자가 인식하는 의미 있는 단위가 무엇인가에 따라서 판가름된다. 즉, 업무에 따라서 성과 명을 분리하느냐 합체하느냐가 결정된다는 것이다. 만약 성이 무엇인지 분명히 구분해야 한다면 성을 분리해야 하는 것이 당연하다. 이름의 경우는 아래의 그림과 같이 같은 경우가 많이 있다. attachment:property07.jpg 이름의 경우는 짓다 보니 같은 경우가 많이다. 우리나라의 경우는 “성명”이 붙어 있어야 의미가 있다. 그러나 외국의 경우는 First Name, Middle Name, Last Name 과 같이 분리해 놓은 사례를 많이 볼 수 있다. 이것은 성과 중간이름, 이름이 모두 의미가 있기 때문이다. 외국의 경우는 남성과 여성이 결혼을 하게 되면 여성의 성이 남편을 따라가게 되므로 분리해야 의미가 있을 수도 있다. 역시 업무에 따라서 틀려진다. 다음의 예를 보자. attachment:property08.jpg 날짜의 속성을 년도, 월, 일과 같이 나누었다. 그리고 인덱스는 yyyy + mm + dd와 같이 결합인덱스로 구성하였다. 만약 다음의 쿼리와 같다면 오른쪽의 쿼리는 원하는 결과를 얻을 수 없을 것이다. attachment:property09.jpg 물론 인덱스를 사용할지 인덱스를 사용하지 못할지는 통계정보등의 상황과 옵티마이저가 결정할 일이지만 인덱스를 사용하지 못할 가능성도 많이 있다. 결합인덱스를 구성하는 두 개의 선행컬럼의 선택도에 달려 있는 문제이다. 이 경우는 성능보다도 어떻게 하면 원하는 결과를 나타낼 수 있는가가 문제이다. 개발자는 다음과 같은 생각을 할 수 있을 것이다. yyyy || mm between ‘200412’ and ‘200503’ --인덱스 사용 불가 그러나 이와 같이 where절의 조건을 사용한다면 인덱스를 사용하지 못한다. 왜냐하면 인덱스는 yyyy + mm + dd로 생성된 것이지 yyyymm으로 생성된 것이 아니기 때문이다. 또 다른 예를 보자. attachment:property10.jpg 사원번호 ‘1234’ 인 사원이 오늘이나 특정 일에 처리한 건수를 알고 싶다면 10만 건을 액세스 할 수 밖에 없다. 왜냐하면 발생일시 = {발생일 + 발생시간}로 복합속성으로 구성되고 있기 때문이다. 이러한 경우 하루 10만 건 발생하고 결합인덱스를 구성하는 첫 번째 컬럼이 ‘=’로 비교되지 않으므로 하루 발생 건수를 모두 뒤져야만 원하는 결과를 얻을 수 있다. 이를 해결하기 위해서는 발생일시를 발생일자와 발생시간으로 나누어야 한다. 그러면 발생일자와 사원번호는 ‘=’ 로 비교 할 수 있다. 그러면 한 명의 사원에 대해 처리한 건수인 100건만을 읽어 처리 할 수 있으므로 최적화 된다. 한 가지 예를 더 보도록 하자. attachment:property11.jpg 위 쿼리는 년도 보다는 mm의 선택도가 그나마 좋기 때문에 code와 mm 에 생성된 인덱스를 이용하여 Index Merge를 하게 된다. Merge의 방식은 Hash Match 방식이다. 문제는 이러한 단일 쿼리만을 봤을 때는 큰 영향이 없다. 그러나 이러한 것들이 쌓여 시스템에 전체적인 영향을 미치는 것이다. '''테이블 수정이 불가능할 경우''' 테이블 수정이 불가능 할 경우는 년월일에 대한 각각의 단일 인덱스를 삭제하고 yyyy + mm + dd로 구성된 결합인덱스를 생성하는 것이 좋다. 어차피 년, 월, 일은 각각 선택도가 좋지 않기 때문에 쓰여지지 않는 불필요한 인덱스가 된다. 만약 따로 각각 나누어야 할 이유가 있었다면 이야기는 틀려지지만 대부분의 경우는 년, 월, 일의 각각이 가지는 의미가 없기 때문에 년월일이 합쳐진 속성을 하나의 속성이라고 보아야 옳다. 다음은 yyyy + mm + dd의 결합인덱스를 이용할 경우의 결과 이다. (873 row(s) affected) Table 'date_test'. Scan count 2, logical reads 754, physical reads 0, read-ahead reads 0. '''테이블 수정이 가능한 경우''' 역시 필자가 의도한 바는 설계의 잘못으로 인해 성능에 미치는 영향이다. 테이블이 dt(yyyymmdd), code와 같이 만들어졌다면 대부분의 문제는 해결된다. index는 dt + code로 했다. {{{ select dt, code from date_test2 where dt like '200406%' and code like '10208%' (873 row(s) affected) Table 'date_test2'. Scan count 1, logical reads 1531, physical reads 0, read-ahead reads 0. }}} 논리적 읽기 수를 보면 더 많은 페이지를 읽은 것이 보인다. 이것은 적절한 솔루션이 될 수 없다. 그래서 필자는 날짜와 1부터 시작하는 dumy 테이블을 이용하여 조그만 희생을 치르고 생성된 인덱스를 최적화하여 사용하도록 유도하였다. {{{ select dt, code from date_test2 where dt in (select dt from dumy where dt like '200406%') and code like '10208%' (873 row(s) affected) Table 'date_test2'. Scan count 30, logical reads 98, physical reads 0, read-ahead reads 0. Table 'dumy'. Scan count 1, logical reads 3, physical reads 0, read-ahead reads 0. }}} 결과를 보면 알 수 있겠지만 하나의 테이블에 더 접근하였음에도 불구하고 15배 이상의 논리적 읽기 수 차이가 나는 것을 볼 수 있다. 이것은 결합인덱스의 첫 번째 컬럼의 비교조건을 부등호가 아닌 '=' 비교하게끔 한 것이 성능 향상의 중요한 원인이 된다. (결합인덱스의 개념을 알아야 이해가 갈 것임) 이때 또 문제가 생기게 된다. 만약 code만 접근할 경우라면 날짜(dt)에 대한 조건을 주어주지 못하고 결국 전체 테이블을 뒤져봐야만 원하는 결과집합을 얻을 수 있다는 것이다. 그러나 역시 dumy 테이블을 이용하면 이 문제는 해결된다. 다음의 쿼리를 보자. 이는 “올해만” 이라는 조건이 들어갔다. 대부분의 OLTP 시스템에서는 최근일에 대한 내용만이 필요하다. 그러므로 대부분 날짜에 대한 입력을 받아 이를 매개변수로 사용한다. 만약 Code에 대한 접근이 모든 날짜라면 사용자들과 합의하여 최근은 데이터만을 읽어오도록 하던가 아니면 몇 페이지에 걸쳐서 원하는 결과를 얻도록 유도해야 한다. (BPR) {{{ select a.dt, a.code from date_test2 a inner join (select dt from dumy where dt like cast(datepart(yyyy, getdate()) as varchar) + '%') b on a.dt = b.dt and a.code like '10208%' (5742 row(s) affected) Table 'Worktable'. Scan count 5759, logical reads 5796, physical reads 0, read-ahead reads 0. Table 'dumy'. Scan count 1, logical reads 3, physical reads 0, read-ahead reads 0. Table 'date_test2'. Scan count 1, logical reads 9343, physical reads 0, read-ahead reads 0. }}} 결과를 보면 알겠지만 우리의 예상과는 많이 빗나가는 결과가 나왔다. 우리는 dumy 테이블부터 읽어 들여 Nested Loop Join을 유도하였으나 Scan Count를 보아 이는 분명 Merge Join 또는 Hash Join으로 풀린 것이다. 이것은 date_test2 테이블을 풀스캔 했다는 의미가 된다. 필자는 여러 가지 노력을 해보았으나 우리가 의도하는 대로 실행계획이 움직이지 않았다. 할 수 없이 조인 힌트를 쓰고, Option절을 써가면서 우리가 의도하던 바를 얻을 수 있었다. {{{ select a.dt, a.code from (select dt from dumy where dt like cast(datepart(yyyy, getdate()) as varchar) + '%') b inner loop join date_test2 a on a.dt = b.dt and a.code like '10208%' option (force order) (5742 row(s) affected) Table 'date_test2'. Scan count 366, logical reads 1146, physical reads 0, read-ahead reads 0. Table 'dumy'. Scan count 1, logical reads 4, physical reads 0, read-ahead reads 0. }}} 중요한 것은 code의 선택도다. 테이블 변경 전에는 code에 단일 인덱스가 생성되어 있었다. 만약 다음과 같이 code 단일 인덱스를 이용한다면 어떨까? {{{ select * from date_test where code like '10208%' (5742 row(s) affected) Table 'date_test'. Scan count 1, logical reads 10199, physical reads 0, read-ahead reads 0. }}} 위에서 mm 인덱스와 code 인덱스가 Merge 했다고 했다. 이것은 분명히 Code 인덱스의 선택도가 좋지 못하다는 것임을 알 수 있다. 그러므로 결과는 풀스캔이다. 즉, 선택도 0.1 이상이라는 것이다. 그러나 yyyymmdd + code의 선택도는 code가 단독으로 존재할 때 보다 선택도가 훨씬 좋다. 선택도가 좋은 단일 컬럼을 조건으로 할 때는 code가 단독으로 존재하는 것이 좋지만 선택도가 검색조건에 따라 틀려진다면 결합하는 것이 유리하다. 이러한 경우는 선두 컬럼이 날짜, 일련번호와 같이 우리가 미리 알 수 있는 수치이어야 한다는 것이다. 또한 dumy 테이블의 크기도 고려사항에 포함해야 한다. ==== 설계속성과 다중값 속성 ==== 설계속성은 실제 개체집합이 가지고 있는 속성이 아니라 모델러가 의도적(성능 등의 이유)으로 개체집합에 포함한 속성이다. 다중값 속성은 해당되는 속성이 단일값을 가지는 것이 아니라 다중값을 가지는 것을 의미한다. 많은 경우 순차적으로 DBMS에서 부여해주는 순번과 같은 속성을 추가해 주는 경우가 있다. 모델링에서는 이러한 설계속성은 식별자를 선정 할 때 고려하는 것이 바람직하다. 위의 예제 중 발생일자, 사원번호를 다룬 적이 있다. 여기서 한 가지 더 고려해야 할 사항은 ‘발생일시’에 대한 정의이다. 만약 발생일자가 입고일자, 출고일자, 선적일자 등과 같이 다중값 속성이라면 “발생구분” 이란 설계속성이 필요하다. 이러한 경우는 속성의 통합을 이루는 것이므로 필요에 따라 식별자를 선정하는 단계 이전에 해결하고 넘어가야 하는 것이 맞다. 이러한 경우는 업무가 입고, 출고, 선적과 같은 다양한 경우에 나타나므로 인덱스와 물리적인 상황을 고려한다면 통합하는 것이 대부분 유리하다. 그러나 업무의 변화가 없고, 입고일자, 출고일자, 선적일자에서 더 추가할 일자가 없다면 가로로 펼쳐도 그리 큰 문제가 안 된다. 이는 추상화 레벨의 차이이다. 발생일자를 세분화 시킨다면 입고일자, 출고일자, 선적일자와 같이 되지만 이들을 일반화시킨다면 발생일자로 될 수 있다. 이에 대한 내용은 다음 단원에 더욱 자세히 살펴보도록 하겠다. ==== 정규화와 물리적인 요소 ==== 정규화에 대한 정확한 내용은 다시 다루도록 하고, 여기에서는 개념만을 다루도록 하겠다. 데이터베이스를 다룬다면 “정규화”라는 말을 들어보았을 것이다. 정규화는 성능 튜닝의 도구이자 무결성을 지켜주는 데이터베이스 최고의 도구이다. 이 모든 것은 “데이터의 중복”을 없애는데 그 목적이 있다고 할 수 있겠다. (결점이 생기는 대부분의 원인도 데이터의 중복에 있다.) 데이터의 중복을 없애는 것은 데이터베이스에서는 매우 중요한 일이다. 데이터의 중복은 통합된 정보의 취득을 어렵게 만든다. 또한 데이터베이스 시스템의 성능 저하의 원인이 되기도 한다. 물론 인덱스를 제대로 활용하지 못한다거나 H/W등의 병목 등의 여러 가지 상황적인 요소가 있을 수 있다. 그러나 이것은 충분히 쉽게 개선할 수 있다. 그러나 설계상의 문제로 데이터가 중복되는 것은 SQL이 복잡해지고, 성능 저하의 문제를 가져온다. 왜 그럴까? 이쯤에서 우리는 기본적인 컴퓨터의 구조를 생각해야 한다. 기초 전산학에는 “폰 노이만” 의 “프로그램 내장방식” 이라는 말이 나온다. 기본적으로 CPU는 Main Memory(RAM)에만 접근할 수 있다는 것이다. 그러므로 CPU가 처리하기 위해서는 데이터를 HDD에서 RAM으로 끌어 올려야만 한다. 이 비용은 컴퓨터에서는 매우 큰 비용이다. HDD가 매우 느린 하드웨어 자원이기 때문이다. 그렇다면 데이터의 중복이 있다고 생각을 해보자. HDD에서 RAM으로 데이터를 올려야 한다. 그러나 데이터가 중복되어 있다면 중복된 만큼 HDD는 읽기를 수행해야 하며, 저장공간도 더 필요한 것은 당연하다. 또한 RAM의 크기도 상대적으로 많이 필요하게 된다. 또한 CPU는 데이터가 중복된 만큼 더 처리를 해야 하는 것이다. 데이터의 중복으로 인해 이러한 결과는 성능 저하를 가져온다. 이것은 결국 정보의 질을 떨어뜨리는 일이고, 궁극적으로는 사용자의 정보 욕구를 만족시키지 못하는 결과를 초래하는 것이다. 데이터베이스에서 논리적으로 생각한다는 것은 물리적인 요소를 고려를 하지 않는다는 것이다. (물론 “논리” 라는 것의 개념의 범위가 이 문서에서는 이와 같다는 것이다.) 다음의 두 쿼리가 있다. TAB이라는 테이블은 1천 만 건이다. ID 컬럼에는 1 ~ 10000000까지의 번호가 순서대로 붙어 있다. ID 컬럼에 대하여 인덱스가 생성되어 있다. 물론 ID 컬럼 이외에 다른 여러 컬럼들이 있다. {{{ --1번 쿼리 SELECT * FROM TAB WHERE ID BETWEEN 1 AND 1000 --2번 쿼리 SELECT * FROM TAB WHERE ID BETWEEN 1001 AND 5000 }}} 대부분의 사람은 당연히 1번이 빠를 것이라고 대답할 것이다. 그러나 이것은 물리적은 상황을 고려하지 않은 상태의 얘기이다. 최악의 경우에는 응답시간이 1번보다 2번이 훨씬 빠를 수도 있다. 다음의 그림을 보자. attachment:property12.jpg 1 ~ 5000 은 1개의 페이지에 들어가 있고, 1, 2, 3 … 1000 은 각각의 페이지에 저장되어 있다면 1000개의 페이지에 접근해야 원하는 결과를 얻을 수 있다는 것이다. 위와 같은 물리적은 상황이라면 1001 ~ 5000의 로우는 MSSQL SERVER 2000의 경우 8KB만 읽으면 되고, 1 ~ 1000개의 로우는 모두 8KB * 1000인 약 8MB를 읽어야 처리를 할 수 있다는 것이다. 이 경우 데이터의 양이 작으므로 응답시간의 차는 눈에 띄게 차이가 나지 않을 것이다. 그러나 이러한 상황을 더 많은 데이터를 가지고 있는 테이블에 적용시킨다면 문제는 커진다. 위와 같이 “이상과 현실”이 존재한다. 이상적으로는 데이터 행이 조각화되어 있지 않고, 각각의 페이지에 꽉꽉 채워져 있다면 당연히 거의 이상적으로 처리할 수 있을 것이다. 아래의 예를 보자. TEST_NAME 컬럼이 CHAR(5000)이므로 당연히 MSSQL SERVER 2000에서는 1개의 페이지에 1개의 행만 삽입될 수 있다. 결과를 보면 알겠지만 페이지당 3083바이트가 낭비되고 있으며, 1만 행에 접근하기 위해서는 1만개의 페이지에 접근해야 한다. 엄청난 비효율이다. 이런 경우라면 당연히 CHAR대신에 VARCHAR을 써야 한다. (예에서는 입력되는 컬럼이 가변길이이다.) attachment:property13.jpg 비정규화 테이블은 일반적으로 컬럼의 개수가 많다. 컬럼의 개수가 많다는 이야기는 하나의 데이터 행의 크기가 크다는 의미이다. 위에서 살펴본 바와 같이 하나의 데이터 행의 크기가 크다면 당연히 최소 입출력 단위에는 데이터 행이 많이 들어가지 못한다. 최악의 경우 입출력은 매우 많이 읽어나고, 데이터 행이 각각의 페이지에 존재할 수 있다. 다음과 같이 테이블이 만들었다고 가정하자. {{{ CREATE TABLE 차량관리비내역( 차량번호 VARCHAR(10) , 관리부서 NUMBER , 연료 NUMBER , 주차비 NUMBER , 세차비 NUMBER , 윤활유 NUMBER , 부품대 NUMBER , 수리비 NUMBER , 타이어교환비 NUMBER , 자동차세 NUMBER , 환경세 NUMBER , 보험료 NUMBER ) }}} 잘 보면 이와 같은 경우는 1차 정규화 위배나 똑같다. 연료, 주차비, 세차비, 윤활유 등은 “차량관리비” 정도로 만들고, “구분” 컬럼을 만들면 된다. 즉, “차량관리비”는 다중값 속성이되어 다른 테이블로 떨어져 나가게 된다. 그래서 다음과 같은 논리 ERD가 그려질 것이다. attachment:property14.gif 각각의 테이블에 할당 받은 저장 공간을 효율적으로 사용하기 위해서는 테이블이 좁게 설계되어야 한다. 즉, 데이터 행을 하나의 최소 입출력 단위에 꽉꽉 채울 수 있다는 것이다. 뿐만 아니라 여러 가지 이상현상도 막을 수 있다. 예를 들어 “검사비”가 추가된다고 가정하자. 당연히 옆으로 컬럼이 나열되어 있는 경우는 테이블의 변경을 가져온다. 그러나 위와 같은 경우 비용과 구분만 넣어주면 그대로 끝난다. 최소한의 변경만으로 유연성이 확보될 수 있다. 또한 인덱스와 같은 추가적인 물리적인 객체의 추가적인 생성도 막을 수 있다. 이것은 우리가 관리해야 할 객체의 수가 줄어든다는 것도 의미한다. 실제로 가로로 놓느냐 세로로 놓느냐는 업무에 따라 틀려질 수 있다. 성능이 매우 중요한 경우에는 가로로 놓여질 수도 있다. 세로로 놓는 다는 것은 통합을 의미하며, 대부분은 통합이 유리하다. 가로로 펼쳐 놓다 보면 인덱스를 유지하는 비용이 더 클 가능성이 매우 높기 때문이다. 정규화를 함으로써 얻는 이점은 매우 많이 있다. 물론 정규화는 논리적인 것이지만 물리적으로 끼치는 영향은 매우 크다. 정규화를 하면 “이상”에 더욱 가까워 질 수 있다. 데이터베이스에서 정규화를 왜 강조하는지 알아야 한다. 정규화는 최고의 성능 향상 도구인 동시에 최고의 무결성 보장과 변화에 대처할 수 있는 최고의 도구이다. ==== 식별자와 설계속성 그리고 대리키 ==== 개체집합을 대표하고 각각의 개체를 구별지을 수 있는 속성(들)이 식별자이다. 이러한 식별자는 설계속성으로 가려져 있는 경우가 많이 있다. 부모-자식 관계를 계속 찾아가다 보면 최 하위 레벨의 개체집합의 식별자는 커지기 마련이다. 그렇다면 어떤 레벨에서 비식별자 관계를 전환할 것인가에 대한 고민이 필요하다. attachment:property15.gif 고개ID는 가변길이 문자형으로 12바이트이다. 만약 “캐릭터_아이템”의 크기가 매우 클 것으로 예상된다면 12바이트는 매우 큰 부담이 될 수도 있다. 이를 위해 “고객ID” 대신에 대리키를 두는 방법도 있다. 다음의 그림을 보자. attachment:property16.gif “고객”의 크기는 “캐릭터_아이템”의 크기보다 월등히 작다. 그러므로 대리키(int형)으로 두면 Row 당 8바이트를 줄일 수 있다. 단 고객의 수가 int 형의 Max 값인 2,147,483,647 건 이하라고 가정해야 한다. 여기서 대리키는 설계속성이고, 고객ID를 대신하는 대리키이다. ==== 유도속성 ==== 유도속성은 일반적으로 계산된 속성이라고도 한다. 대부분의 유도된 속성들은 필요 없다. 예를 들어 어떤 상품의 가격과 “부가세포함가”는 틀리다. 만약 “부가세포함가”를 하나의 속성으로 둔다면 상품의 가격에 부가세를 더해줘야 할 것이다. 그러나 유도속성을 쓰면 변화에 약해진다. 모든 상품의 가격을 10% 인상했다고 하면 그 동안 들어가 있던 유도속성 컬럼들까지 모두 변경해주어야 한다. 즉, 상품의 가격만 갱신해줘서 되는 게 아니라는 이야기다. 그러므로 정합성을 유지해야 하는 부담이 있다. 대부분은 유도속성을 쓰는 것은 금기사항으로 보면 된다. ==== 속성의 도메인 관리 ==== ||속성명||EMPNO (사원번호)|| ||데이터 타입||NUMBER|| ||길이||4|| ||형식||{{{[0-9] [0-9] [0-9] [0-9]}}}|| ||허용범위||0000 ~ 9999|| ||의미||사원개체 집합에서 각각의 사원을 구분지을 수 있는 역할을 하는 주 식별자|| ||유일성||UNIQUE|| ||널값허용||NOT NULL|| ||기본값||없음||