728x90

1. View는 논리적인 가상 Table

View는 한 개 이상의 Table 질의에 대한 가상 Table입니다.

  View는 실제로 data를 포함하거나 저장하지 않는 논리적인 구조로 Base Table data

  조회하거나 변경할 수 있습니다.

 

  (1) View의 문법 및 생성

      create [or replace] [force | noforce]  view  view

      (column1, column2, column3,)

as

Select

[with check option [constraint constraint]]

[with read only]

      . [or replace]는 동일한 view가 존재할 경우 기존 view를 대체(replace)시킨다.

      . [force] option base table이 존재하지 않을 경우에도 강제로 view를 만든다.

         [noforce] option base table이 없는 경우 view가 작성되지 않게 한다.(default)

      . [with check option] select문의 where절의 조건에 대한 checking을 합니다.

         [with read only] view를 통한 data의 변경을 금지하고 조회만 가능하게 한다.

. emp Table에서 부서 번호가 20 Row를 얻는 view를 생성시킨다.

SQL> create view emp_20_vu

  2  as

  3  select empno, ename, sal, deptno

  4  from   emp

  5  where  deptno = 20

  6  order  by sal;

뷰가 생성되었습니다.

. view의 구조는 아래와 같다.

SQL> desc emp_20_vu

 이름                                          ?      유형

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

 EMPNO                                           NUMBER(4)

 ENAME                                           VARCHAR2(10)

 SAL                                               NUMBER(7,2)

 DEPTNO                                          NUMBER(2)

. view를 이용해 자료를 조회한다.

SQL> select empno, ename, sal, deptno

  2  from   emp_20_vu;

 

    EMPNO ENAME            SAL    DEPTNO

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

     7369   SMITH             800        20

     7876   ADAMS           1100        20

     7566   JONES            2975        20

     7788   SCOTT           3000        20

     7902   FORD             3000        20

. column alias를 이용한 view 생성

   emp Table에서 column alias를 이용해 자료를 조회한다.

 SQL> create or replace view emp_20_vu

  2  (eno, ename, salary, deptno)

  3  as

  4  select empno, ename, sal * 1.15, deptno

  5  from   emp

  6  where  deptno = 20

  7  order  by 3;

뷰가 생성되었습니다.

     o. view를 이용해 자료를 조회한다.

 SQL> select eno, ename, salary, deptno

  2  from   emp_20_vu;

 

      ENO ENAME         SALARY    DEPTNO

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

     7369 SMITH               920        20

     7876 ADAMS             1265        20

     7566 JONES           3421.25        20

     7788 SCOTT             3450        20

     7902 FORD               3450        20

  (2) view를 사용하는 장점

     . base table을 직접 access하지 않으므로 보안에 유리합니다.

     . 복잡한 질의를 간단하게 접근할 수 있다.

     . View는 가상의 table이므로 삭제해도 base table에는 전혀 영향이 없다.

(3) View의 원리

   VIEW user_view라는 data dictionary SELECT문으로서 저장되어 있다.

  

   . user_views의 구조를 살펴보자.

SQL> desc user_views

 이름                                ?      유형

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

 VIEW_NAME                     NOT NULL VARCHAR2(30)

 TEXT_LENGTH                              NUMBER

 TEXT                                        LONG

 TYPE_TEXT_LENGTH                        NUMBER

 TYPE_TEXT                                  VARCHAR2(4000)

 OID_TEXT_LENGTH                          NUMBER

 OID_TEXT                                    VARCHAR2(4000)

 VIEW_TYPE_OWNER                          VARCHAR2(30)

 VIEW_TYPE                                   VARCHAR2(30)

   . user_views에 대해 부분 질의를 하면 아래와 같다.

SQL> select view_name, text

  2  from   user_views;

 

VIEW_NAME       TEXT

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

EMP_20_VU        select empno, ename, sal * 1.15, deptno

from   emp

where  deptno = 20

order  by 3;

     . emp_20_vu에 대한 질의를 하면 oracle user_views라는 data dictionary를 통해

emp_20_vu를 작성할 때 사용한 질의를 찾아서 실행합니다.

(4) 복잡한 View의 실습

    . s_dept s_emp에서 부서명과 평균급여액, 최고급여액을 구하는 view이다.

SQL> create view dept_avgmax_vu

  2  (dept_name, avg_sal, max_sal)

  3  as select d.name, avg(e.salary), max(e.salary)

  4  from s_dept d, s_emp e

  5  where d.id = e.dept_id

  6  group by d.name;

    . 생성된 view를 생성하면 아래와 같다.

SQL> select dept_name, avg_sal, max_sal

  2  from   dept_avgmax_vu;

 

DEPT_NAME             AVG_SAL   MAX_SAL

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

Administration                 2025      2500

Finance                        1450      1450

Operations                   1086.8      1450

Sales                     1367.8571      1525

(5) View를 통한 data 변경

   . View가 하나의 Table을 사용하고 변경할 권한이 있으면, View를 참조하는

      base Table을 변경할 수 있다.

   . View가 여러 개의 Table을 사용한다면 View를 통한 data변경은 힘들다.

   . dept Table을 이용해 view를 생성하고, insert문을 이용해 data를 입력한다.

 SQL> create view dept_vu

  2  as

  3  select * from dept;

 

뷰가 생성되었습니다.

 

SQL>  insert into dept_vu (deptno, dname, loc)

  2   values (50, 'MANAGE', 'KOREA');

 

1 개의 행이 만들어졌습니다.

     . 실제 dept Table을 확인해 보자.

 SQL> select * from dept;

 

   DEPTNO DNAME          LOC

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

       10 ACCOUNTING     NEW YORK

       20 RESEARCH       DALLAS

       30 SALES          CHICAGO

       40 OPERATIONS     BOSTON

50 MANAGE         KOREA

 

(6) View의 삭제

     drop  view  view;

 

2. Sequence

  Sequence Table에서 유일한 번호를 자동으로 만들어 주는 oracle object입니다.

 

(1) 문법

    create sequence  sequence

    [increment by n]             è default 1

    [start with n]                è default 1

    [maxvalue n]                è default 10 27

    [minvalue n]                 è maxvalue일때 시작값

    [cycle]                      è default nocycle

    [cache n | nocache]         è cache n default 20

 

ㄱ. increment by sequence를 증가할 때 증가분이다.

ㄴ. start with sequence의 최초값이다.

ㄷ. maxvalue 10 27, minvalue10 26승까지 값을 가진다.

. cache 다음의 수는 oracle이 미리 할당하고 유지할 값의 수이다.

. cycle maxvalue minvalue까지 값이 모두 생성되었을 때 계속 sequence

생성되도록 하는 option이다.

     . cycle option은 기본키(PK)로 정의된 column에는 사용하지 않습니다.

(2) sequence의 값 구하기

ㄱ.    현재값 구하기

sequence.currval

ㄴ.    다음값 구하기

sequence.nextval

(3) sequence 변경

alter sequence  sequence

    [increment by n]                 

    [maxvalue n]               

    [minvalue n]                

    [cycle]                     

    [cache n | nocache]        

 

    여기서 주의할 것은 start with는 변경할 수 없다는 것이다.

(4) sequence 삭제

   drop  sequence  sequence;

(5) sequence에 대한 data 조회

   sequence에 대한 data 조회는 user_sequences data dictionary를 사용합니다.

 

ㄱ. user_sequences의 구조를 살펴보자.

SQL> desc user_sequences

 이름                                                  ?      유형

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

 SEQUENCE_NAME                             NOT NULL VARCHAR2(30)

 MIN_VALUE                                               NUMBER

 MAX_VALUE                                              NUMBER

 INCREMENT_BY                               NOT NULL NUMBER

 CYCLE_FLAG                                             VARCHAR2(1)

 ORDER_FLAG                                             VARCHAR2(1)

 CACHE_SIZE                                  NOT NULL NUMBER

 LAST_NUMBER                               NOT NULL NUMBER

ㄴ. 현재 생성된 sequence를 살펴보자.

SQL> select sequence_name, min_value, max_value, increment_by, last_number

  2  from   user_sequences;

 

SEQUENCE_NAME MIN_VALUE MAX_VALUE INCREMENT_BY LAST_NUMBER

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

S_CUSTOMER_ID            1      9999999                1         216

S_DEPT_ID                  1      9999999                1          51

S_EMP_ID                   1      9999999                1          26

S_ORD_ID                   1      9999999                1         113

S_PRODUCT_ID             1      9999999                1       50537

S_REGION_ID               1      9999999                1            6

S_WAREHOUSE_ID          1      9999999                1       10502

 

7 개의 행이 선택되었습니다.

 

 

 

 

(6) sequence를 이용한 실습

ㄱ. tb_seq라는 table을 생성하자.

SQL> create table tb_seq

  2  (

  3   id   number(7) not null,

  4   name varchar2(10)

  5  );

 

테이블이 생성되었습니다.

. tb_seq_id_seq 라는 sequence를 생성하여 tb_seq id라는 column에 이용하자.

SQL> create sequence tb_seq_id_seq

  2  increment by 1

  3  start with 1

  4  maxvalue 9999999

  5  cycle;

 

주문번호가 생성되었습니다.

. tb_seq table insert sequence를 이용하자.

SQL> insert into tb_seq (id, name)

  2  values (tb_seq_id_seq.nextval, '박대환');

1 개의 행이 만들어졌습니다.

. 실제 data를 확인하자.

SQL> select id, name from tb_seq;

       ID NAME

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

        1   박대환

. 현재 sequence의 값을 알아보자.

SQL> select tb_seq_id_seq.currval from dual;

 

  CURRVAL

---------

        1

 

 

 

 

3. Synonym

  Synonym DB object(Table, View, Procedure, Function)등에 대한 별명입니다.

 

(1) Synonym 생성 문법

   Create [public] synonym synonym

   For owner.object;

 

ㄱ. public은 모든 사용자가 접근 가능하도록 만든 것이다.

. owner object를 소유한 oracle 사용자명이다.

   . emp_20_vu라는 view에 대한 synonym emp20을 생성하자.

 SQL> create synonym emp20

  2  for    emp_20_vu;

 

동의어가 생성되었습니다.

(2) Synonym 삭제 문법

   Drop [public] synonym synonym;

(3) 현 사용자가 속한 private synonym을 조회하려면 user_synonyms data dictionary를 사용한다.

. user_synonyms의 구조를 살펴보자.

SQL> desc user_synonyms

 이름                                         ?      유형

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

 SYNONYM_NAME                        NOT NULL VARCHAR2(30)

 TABLE_OWNER                                     VARCHAR2(30)

 TABLE_NAME                           NOT NULL VARCHAR2(30)

 DB_LINK                                            VARCHAR2(128)

. 현 사용자에 속한 synonym을 조회하자.

SQL> select synonym_name, table_owner, table_name

  2  from   user_synonyms;

 

SYNONYM_NAME     TABLE_OWNER     TABLE_NAME

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

EMP20                  SCOTT            EMP_20_VU

 

 

 

. synonym을 이용해 data를 조회하자.

SQL> select * from emp20;

      ENO  ENAME       SALARY    DEPTNO

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

     7369  SMITH              920          20

     7876  ADAMS            1265          20

     7566  JONES          3421.25          20

     7788  SCOTT            3450          20

     7902  FORD             3450           20

 

4. Index

Index Table Row를 더 빨리 access 하기 위해 사용하는 Object이다.

Index를 적절히 생성하면 Oracle Application의 속도는 빨라질 것이다.

 

(1) Index 생성

TABLE 생성시 Primary Key Unique Constraint를 지정하면 Unique Index가 자동적으로 만들어진다.

 

. 문법

CREATE [UNIQUE] INDEX index

    ON table(column[, column...]) ;

. s_emp Table에서 name column index로 생성해보자.

SQL> create index s_emp_name_idx

  2  on s_emp (name);

 

인덱스가 생성되었습니다.

 

  (2) Index 삭제

     INDEX는 수정할 수 없으며, 수정하려면 삭제하고 다시 생성한다.

 

ㄱ. 문법

DROP INDEX index ;

    . index s_emp_name_idx을 삭제해보자.

 SQL> drop index s_emp_name_idx;

 

인덱스가 삭제되었습니다.

  (3) 현 사용자가 속한 index를 조회하려면 user_indexes data dictionary를 사용한다.

ㄱ. user_indexes의 구조를 살펴보자.

 SQL> desc user_indexes

 이름                                           ?      유형

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

 INDEX_NAME                             NOT NULL VARCHAR2(30)

 INDEX_TYPE                                         VARCHAR2(27)

 TABLE_OWNER                           NOT NULL VARCHAR2(30)

 TABLE_NAME                            NOT NULL VARCHAR2(30)

 TABLE_TYPE                                         VARCHAR2(11)

 UNIQUENESS                                         VARCHAR2(9)

 COMPRESSION                                        VARCHAR2(8)

 PREFIX_LENGTH                                      NUMBER

 TABLESPACE_NAME                                  VARCHAR2(30)

 INI_TRANS                                            NUMBER

 MAX_TRANS                                          NUMBER

 INITIAL_EXTENT                                     NUMBER

 NEXT_EXTENT                                       NUMBER

 MIN_EXTENTS                                        NUMBER

 MAX_EXTENTS                                       NUMBER

 PCT_INCREASE                                       NUMBER

 PCT_THRESHOLD                                     NUMBER

 INCLUDE_COLUMN                                    NUMBER

 FREELISTS                                            NUMBER

 FREELIST_GROUPS                                    NUMBER

 PCT_FREE                                             NUMBER

 LOGGING                                              VARCHAR2(3)

 BLEVEL                                               NUMBER

 LEAF_BLOCKS                                         NUMBER

 DISTINCT_KEYS                                       NUMBER

 AVG_LEAF_BLOCKS_PER_KEY                         NUMBER

 AVG_DATA_BLOCKS_PER_KEY                        NUMBER

 CLUSTERING_FACTOR                                 NUMBER

 STATUS                                               VARCHAR2(8)

 NUM_ROWS                                            NUMBER

 SAMPLE_SIZE                                          NUMBER

 LAST_ANALYZED                                      DATE

 DEGREE                                               VARCHAR2(40)

 INSTANCES                                           VARCHAR2(40)

 PARTITIONED                                         VARCHAR2(3)

 TEMPORARY                                          VARCHAR2(1)

 GENERATED                                          VARCHAR2(1)

 SECONDARY                                          VARCHAR2(1)

 BUFFER_POOL                                        VARCHAR2(7)

 USER_STATS                                         VARCHAR2(3)

 DURATION                                            VARCHAR2(15)

 PCT_DIRECT_ACCESS                                 NUMBER

 ITYP_OWNER                                          VARCHAR2(30)

 ITYP_NAME                                            VARCHAR2(30)

 PARAMETERS                                         VARCHAR2(1000)

 GLOBAL_STATS                                       VARCHAR2(3)

 DOMIDX_STATUS                                      VARCHAR2(12)

 DOMIDX_OPSTATUS                                   VARCHAR2(6)

 FUNCIDX_STATUS                                     VARCHAR2(8)

ㄴ. user_indexes의 내용을 살펴보자.

 SQL> select index_name, table_name

  2  from   user_indexes;

 

INDEX_NAME                         TABLE_NAME

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

EMPCOPY_EMPNO_PK                EMP_COPY

PK_DEPT                             DEPT

PK_EMP                              EMP

S_CUSTOMER_ID_PK                 S_CUSTOMER

S_DEPT_ID_PK                       S_DEPT

S_DEPT_NAME_REGION_ID_UK       S_DEPT

S_EMP_ID_PK                        S_EMP

S_EMP_USERID_UK                   S_EMP

S_INVENTORY_PRODID_WARID_PK   S_INVENTORY

S_ITEM_ORDID_ITEMID_PK           S_ITEM

S_ITEM_ORDID_PRODID_UK           S_ITEM

 

S_ORD_ID_PK                         S_ORD

S_PRODUCT_ID_PK                   S_PRODUCT

S_PRODUCT_NAME_UK               S_PRODUCT

S_REGION_ID_PK                     S_REGION

S_REGION_NAME_UK                 S_REGION

S_TITLE_TITLE_PK                   S_TITLE

S_WAREHOUSE_ID_PK                S_WAREHOUSE

 

18 개의 행이 선택되었습니다.

출처 : http://simmys.tistory.com/category/DataBase/Oracle

'데이터베이스 > 오라클' 카테고리의 다른 글

오라클 NLS_LANG 설정변경  (0) 2010.08.16
Oracle 시스템정보보기 관련 쿼리  (0) 2010.08.16
오라클 데이터사전  (0) 2010.08.16
인덱스 저장위치 설정  (0) 2010.08.16
P/L SQL PACKAGE  (0) 2008.08.18
728x90

송혁 : SQL Server 2005 Query optimizer의 변경된 내용

제 2강 : SQL Server 2005의 인덱싱 된 뷰 와 포괄 열 인덱스에 대해서…

이름: 송혁
Email : hyok81@nate.com
넥슨 DSM팀 DBA로 근무


1. 시작

일반적으로 하나의 테이블에는 하나의 클러스터 인덱스만 존재 할 수 있으며, 힙 영역에 클러스터 인덱스를 만들게 되면 해당 데이터 영역은 클러스터 영역으로 변하게 됩니다.
서비스에 따라 데이터 갱신에 대한 비용보다 많은 조회에 의한 비용이 많이 들 수 있으며 이럴 경우 두 개 이상의 클러스터 인덱스가 필요 할 수 있을 것입니다.
기존 SQL Server 2000에서는 인덱싱 된 뷰를 생성 하여 두 개 이상의 클러스터 인덱스가 있는 것처럼 활용이 가능 했으며 2005에서는 인덱싱 된 뷰 와 새롭게 추가된 포괄 열 인덱스로써 구현이 가능 합니다.
인덱싱 된 뷰, 포괄 열 인덱스에 대한 활용방안, 두 가지의 차이점 및 유사점에 대해서 시작 하도록 하겠습니다.

2. 인덱스란?

인덱스를 우리말로 번역을 하면 색인 이란 단어로 번역 될 수 있습니다. 색인을 국어사전에서 찾아보면 이와 같이 정의를 하고 있습니다. “책 속의 내용 중에서 중요한 단어나 항목, 인명 따위를 쉽게 찾아볼 수 있도록 일정한 순서에 따라 별도로 배열하여 놓은 목록”
SQL Server에서 불리는 색인 즉 인덱스도 위 단어처럼 책 속이 아닌 테이블에 있는 내용을 보다 쉽게 찾기 위해 따로 일정한 순서로 정리해둔 목록 이라고 볼 수 있습니다.

SQL Server또는 다른 DBMS의 경우에도 인덱스가 RDBMS에서 얼마나 중요한 것인지 누구나 알고 있을 것 입니다. 보다 빨리 데이터를 조회하고 갱신하기 위해서는 인덱스라는 것이 필요하고 인덱스가 존재 함으로써 DBMS의 성능의 차이도 발생 할 수 있으며, 잘 사용한다면 성능을 극대화 시킬 수 있지만 잘못 사용한다면 오히려 성능 문제를 일으킬 수 있습니다.

SQL Server는 이전부터 클러스터 인덱스와 넌 클러스터 인덱스 두 가지 종류의 인덱스를 제공 하고 있습니다. 두 가지의 가장 큰 차이점은 소스 데이터를 리프레벨에 포함하는 여부 입니다. 다른 일반적인 구조는 두 가지 모두 비슷합니다.
클러스터 인덱스는 소스 데이터를 포함 하고 있기에 기본적으로는 하나밖에 생성을 할 수 없으며, 넌 클러스터 인덱스는 리프 레벨에 소스 데이터가 아닌 키로 정의된 열에 대한 데이터 만을 리프레벨에 가지고 있기에 두 개 이상 생성이 가능 합니다. “그렇다면 소스데이터가 여러 개가 있다면 클러스터 인덱스도 여러 개 만들 수 있지 않을까요?” 이 물음에 대해서는 아래에서 보다 자세히 살펴보도록 하겠습니다.

아래는 BOL에 설명하는 클러스터 인덱스와 넌 클러스터 인덱스의 내부 구조를 보여 주고 있습니다.
두 개 모두 인덱스 이기에 모두 루트와 데이터 페이지가 존재 하며, 클러스터 인덱스의 데이터 페이지는 실질적인 데이터를 가르키며, 넌 클러스터 인덱스의 데이터 페이지(리프레벨)는 인덱스 페이지라고 부르는 것이 더욱 쉽게 이해할 수 있을 것입니다. 여기서는 이 두 개의 구조적 차이와 자세한 내용을 언급하는 것이 아니지만 보다 쉽게 이해하기 위해서 선수조건이 되면 좋을 것 같습니다. 이 부분에 대해 더 자세한 내용은 온라인 설명서 및 SQL관련 서적을 참조하시길 바랍니다.

[클러스터형 인덱스 구조]
ms-help://MS.SQLCC.v9/MS.SQLSVR.v9.ko/udb9/html/26b28045-c3c2-465a-b564-bf2189e93fdc.htm

[비클러스터형 인덱스 구조]
ms-help://MS.SQLCC.v9/MS.SQLSVR.v9.ko/udb9/html/1efeba1f-f848-4861-9af3-594e5ab3b597.htm



3. 인덱싱 된 뷰, 포괄 열 인덱스 란

이제 위에서 말한 하나의 테이블에 두 개 이상의 클러스터 인덱스를 생성 할 수 있는 방법(?)에 대해서 살펴보도록 하겠습니다.

인덱싱 된 뷰는 SQL Server 2005이전부터 활용된 기능으로써 인덱싱 된 뷰를 추가적으로 생성하여 조회 쿼리에 대해서 많은 이점을 주었습니다.
기존 테이블에 스키마 바인딩 뷰를 생성 후 해당 뷰에 고유한 클러스터 인덱스를 추가 하게 되면 논리적인 뷰가 아닌 물리적으로 저장이 되는 인덱싱 된 뷰가 생성 되게 됩니다.

물론 하나의 테이블 만이 아닌 조인을 이용하여 여러 개의 테이블을 참조하는 뷰에 대해서도 인덱싱 된 뷰는 활용이 가능하기에 많은 조인, 정열 및 집계 등에 많은 비용이 든다면 충분히 고려해 볼 수 있는 기능 입니다. 그러나 데이터를 중복으로 가짐으로써 성능 상 문제가 될 수 도 있습니다. 그러기에 모든 환경에서의 최적의 솔루션이 아닌, 자기 환경에 정확한 이해를 바탕으로 도입 하는 것이 무엇 보다 중요합니다.
주로 데이터의 갱신작업 이 많이 일어나는 OLTP환경보다, OLAP환경과 같은 DW에 보다 유리하며, OLTP환경도 서비스 마다 틀리겠지만 일반적인 환경에서는 데이터 갱신 보다 데이터 조회의 비용이 더 많이 발생되는 것이 일반 적입니다.

SQL Server 2005에서 새롭게 추가된 넌 클러스터 인덱스에 INCLUDE라는 포괄 열 인덱스라는 것이 추가 되었습니다.
포괄 열 인덱스라는 INCLUDE절은 기존 넌클러스터 인덱스에 키와 인덱스 리프레벨에 포함되는 데이터를 따로 설정 할 수 있으며 인덱스 키만이 비리프레벨에 존재 하고, 나머지 INCLUDE절에 포함된 열에 대해서는 리프레벨에만 존재 하게 됩니다. 열 개수 및 데이터 형 제한에 대해서 기존 넌클러스터 인덱스보다 유연하여 인덱싱 된 뷰의 기능을 과 유사하게 사용 될 수 있습니다.
하지만, 만약 소스 테이블의 열이 16개 이하이고 LOB 데이터형도 없는 경우, 넌 클러스터 인덱스로 비슷한 기능을 구현은 가능 합니다. 그러나 비리프레벨에 모든 인덱스 키의 값이 존재 하기에 비리프레벨 페이지에 많은 하위 페이지에 대한 포인터 정보를 넣을 수 없어 인덱스 깊이가 증가 될 것 입니다. 이것으로 충분한 성능의 문제를 초래 할 수 도 있습니다.

포괄 열 인덱스가 포용할 수 있는 기존 넌클러스터 인덱스의 제한

   - 인덱스의 열 개수의 제한
   - 인덱스 크기(900바이트)의 제한
   - 인덱스의 데이터 타입의 제한(LOB형)
   - 저장 될 모든 열을 인덱스 키로 설정 함으로서 비 리프레벨에도 모든 열의 값이 존재함.

4. 인덱싱 된 뷰, 포괄 열 인덱스와의 차이점

위에서 알아보았듯이 인덱싱 된 뷰는 기존에도 많은 조회용 쿼리를 사용하는 환경에서 사용되었습니다.
정열 된 순서대로 많은 데이터를 가져와야 하고 클러스터 인덱스는 다른 열에 있어 클러스터 인덱스를 활용할 수 없고 covered Index로 처리도 어려운 경우에 인덱싱 된 뷰를 사용하여 보다 빠른 결과를 가져올 수 있었습니다. 또는 집계 데이터 빈번한 조인이 사용하는 쿼리 등등 사용 예를 볼 수 있습니다.
여기서 설명 하지 못한 내용도 충분히 많을 것 이며, 복합 넌 클러스터 인덱스로 불가능한 부분에 대해서 사용하였습니다. 그러나 SQL Server 2005에서는 포괄 열 인덱스를 지원함으로써 단일 테이블, 집계가 없을 경우에는 포괄 열 인덱스를 사용이 가능하기에 보다 쉽게 구현이 가능 하며 관리상의 이슈도 적어집니다. 아래는 간단히 인덱싱 된 뷰와 포괄 열 인덱스에 대한 차이를 정리하였습니다.

[표 1] 포괄 열 인덱스와 인덱싱 된 뷰 비교

인덱싱 된 뷰

포괄 열 인덱스

인덱스 키가 아닌 데이터가 비리프레벨에 존재 여부

아니오

아니오

LOB형 지원(TEXT등 제외) 여부

열 개수 제한

아니오

아니오(1023 개)

집계 값 포함 여부

아니오

크기 제한

아니오

아니오

데이터 변경 시 worktable사용 여부

아니오

소스 테이블에 대한 키 존재 여부

아니오

예(RID 또는 클러스터키값)

인덱싱 된 뷰는 기존의 넌 클러스터 인덱스 또는 포괄 열 인덱스와는 다르게 원본 테이블에 대해 연결된 키가 존재 하지 않습니다. 스키마 바인딩 뷰 생성 후 만들게 되는 고유한 클러스터 인덱스 키를 이용하여 소스 테이블의 변경된 내역을 반영하게 됩니다. 이러한 소스테이블에 종속된 키가 존재 하지 않아 데이터 갱신 작업 시 wokrtable을 이용하여 실 테이블을 갱신 작업에 대해 저장 후 이 내용을 바탕으로 인덱싱 된 뷰에서 처리하게 됩니다.

아래는 인덱싱 된 뷰가 존재하는 테이블에 대해서 특정 값을 업데이트 쿼리의 실행 계획이며, Table Spool연산자가 추가 및 출력목록에 인덱싱 된 뷰의 클러스터 키와 변경된 컬럼을 확인 할 수 있습니다.



5. 인덱싱 된 뷰 생성시 필요 조건 및 주의 사항.

인덱싱 된 뷰를 만들기 위해서는 몇 가지 주의 사항이 있습니다. 주의 사항 및 지켜줘야 하는 부분에 대해서 잠시 살펴 보도록 하겠습니다.

주의 사항

  1. CREATE VIEW 문이 실행될 때는 ANSI_NULLS 및 QUOTED_IDENTIFIER 옵션이 ON
  2. 뷰를 생성시 참조 되는 원본 테이블은 생성시 ANSI_NULLS옵션이 ON
  3. 테이블이 아닌 다른 뷰를 참조해서는 안됨.
  4. 뷰 생성시 SCHEMABINDING옵션으로 생성
  5. 테이블 및 사용자 함수에 대해서는 스키마명.객체명 사용.
  6. 뷰의 식에서 참조되는 모든 함수는 결정적 함수.
  7. GROUP BY를 사용하는 경우 SELECT List절 에 COUNT가 아닌 COUNT_BIG을 사용(테스트1 참조)
  8. Select List절에 * 같이 사용할 수 없고 컬럼 명을 명시적으로 사용

보다 자세한 내용은 BOL을 참조(인덱싱 된 뷰 만들기)
ms-help://MS.SQLCC.v9/MS.SQLSVR.v9.ko/udb9/html/f86dd29f-52dd-44a9-91ac-1eb305c1ca8d.htm

뷰 생성 후 해당 옵션으로 생성된 것인지 확인 하기 위해서 OBJECTPROPERTY함수를 사용하여 확인

SELECT
     OBJECTPROPERTY ( OBJECT_ID ( 'Sales.vOrders' , 'view' ) , 'ExecIsAnsiNullsOn' )
    ,OBJECTPROPERTY ( OBJECT_ID ( 'Sales.vOrders' , 'view' ) , 'ExecIsQuotedIdentOn' )
  1 = True
  0 = False


[테스트 1 - GROUP BY절을 사용하고 COUNT_BIG함수를 사용하지 않을 경우 오류 메시지]

SET NUMERIC_ROUNDABORT OFF ;
SET ANSI_PADDING, ANSI_WARNINGS, CONCAT_NULL_YIELDS_NULL, ARITHABORT ,
   QUOTED_IDENTIFIER, ANSI_NULLS ON ;

GO
--스키마 바인딩 뷰 생성
IF OBJECT_ID ('Sales.vOrders', 'view' ) IS NOT NULL
DROP VIEW Sales .vOrders ;
GO
CREATE VIEW Sales .vOrders
WITH SCHEMABINDING
AS
   SELECT SUM (UnitPrice *OrderQty *(1.00 -UnitPriceDiscount )) AS Revenue ,
      OrderDate , ProductID , COUNT(*) AS COUNT
   FROM Sales .SalesOrderDetail AS od ,Sales .SalesOrderHeader AS o
   WHERE od .SalesOrderID = o .SalesOrderID
   GROUP BY OrderDate , ProductID ;
GO
--뷰에 고유한 클러스터 인덱스 생성
CREATE UNIQUE CLUSTERED INDEX IDX_V1
   ON Sales .vOrders (OrderDate , ProductID );
GO

' 메시지 10136, 수준16, 상태1, 줄1
뷰 "AdventureWorks.Sales.vOrders"이 (가) 집계 COUNT 를사용하므로이뷰에인덱스을 (를) 만들수없습니다.
대신 COUNT_BIG 을사용하십시오 .'


6. 하나의 테이블에 클러스터 와 힙 두 가지 모두 만들 수 없을까?

그렇다면 두 가지를 모두 가져 서비스에 좋은 영향을 줄 수 있을까?

아시다시피 SQL Server에는 두 가지 데이터 영역이 존재 합니다. 클러스터 인덱스가 있는 테이블은 클러스터 데이터 영역이라고 부르겠으며, 클러스터 인덱스가 존재 하지 않는 테이블은 힙 데이터 영역이라고 부르겠습니다.
클러스터 데이터 영역은 클러스터 키로 논리적으로 정열이 된 데이터를 말하며 힙 영역은 특정 키로 정열 되지 않은 영역으로 말할 수 있습니다.

SQL Server에는 어떠한 영역이 유리하다고 보기는 매우 어려우며, 두 개 모두 각각의 환경에서 주는 이점이 있습니다.
클러스터 영역이라면 클러스터 키로 소스데이터가 정열 되었기에 집계 및 정열 등에 좋은 성능을 가질 수 있습니다. 그러나 다른 넌클러스터 인덱스는 소스데이터를 접근 하기 위한 포인터로서 클러스터 키를 가지고 있어, 넌클러스터 영역에서 소스데이터 영역을 접근 하기 위해서는 클러스터 인덱스 루트 레벨부터 데이터를 찾아가야 합니다. 단순히 한 두 개의 행을 = 비교로 가지고 와야 한다면 넌 클러스터 인덱스에서 클러스터 영역으로 데이터를 찾는 방식이 성능상 불리 할 수 도 있습니다.

하지만 이런 점은 SQL Server가 발전되면서 변화된 모습이며 이전의 SQL Server에서는 클러스터 인덱스가 생성된 테이블이라고 하더라도 넌클러스터 인덱스가 소스데이터를 참조하기 위해서 RID를 가지고 있었다고 합니다. 소스데이터의 이동이 발생 하면 RID가 변경 되기에 속하는 넌클러스터 인덱스에 대한 RID값도 변해야 합니다. 페이지 분할 등의 작업이 이루어 진다면 넌클러스터 인덱스의 RID를 변경 하는 작업도 SQLServer에는 많은 영향을 주었을 것 이며, 이러한 문제를 해결 하기 위해 RID대신 클러스터 키 값으로 변경 된 것으로 생각 됩니다. 이러한 변화로 인해 많은 장점을 주고 있지만, 모든 면에서 장점을 주는 것은 아이며, 변화로 인해 클러스터 영역인 경우 넌클러스터에서 접근 시 꼭 클러스터인덱스 루트레벨부터 다시 찾아야 하는 단점이 생겼습니다.

이런 단점을 보완 할 수 있는 방법은 없을까요? “클러스터 영역과 힙 영역을 모두 가지고 있으면 단점이 보완 될 수 있지 않을까?” 라는 생각을 해보았습니다. 물론 데이터의 중복으로 인해 데이터 갱신작업에는 기존보다 오버헤드가 있어 성능에 많은 영향을 줄 수 있습니다. 그러나 특정 서비스인 경우에는 클러스터 영역과 힙 영역을 모두 가짐으로써 좋은 영향을 줄 수 있다고 생각합니다. 하지만 많은 서비스는 이점을 줄 수 없을 것입니다. 이러한 방법이 서비스에 어떠한 영향을 준다 보다는 “이렇게도 가능 하지 않을까” 라는 생각을 해보며 작성 하였습니다.

구현을 위해서 먼저 힙 테이블을 만듭니다. 그리고 힙 테이블에 인덱싱 된 뷰를 생성합니다. 인덱싱 된 뷰를 만들기 위해서는 스키마 바인딩 뷰에 고유한 클러스터 인덱스를 처음에 만들어야 합니다. 그러기에 인덱싱 된 뷰 는 클러스터 인덱스를 가지는 영역이 되며, 소스 테이블에는 힙 상태를 그대로 유지하게 됩니다. 소스테이블에 넌 클러스터 인덱스를 생성 하게 되면 RID를 가지는 넌클러스터 인덱스, 인덱싱 된 뷰에 넌클러스터 인덱스를 만들면 클러스터 키를 포인터로 가지는 넌클러스터 인덱스가 됩니다.

개인적인 생각으로는 특정 환경에서는 충분히 활용 가치가 있을 수 있다고 생각됩니다. 위에서 설명한 하나의 테이블에 두 개의 영역으로 나눈 후 각각의 영역에 동일한 컬럼을 가지고 넌 클러스터 인덱스를 만들어서 테스트를 해보았습니다.
아래는 SQL Server 2005에 있는 샘플데이터베이스인 AdventureWorks에 있는 테이블을 가지고 간단히 구현을 하였습니다. 마지막에 두 가지 쿼리는 몇 개 되지 않는 행을 반환 받는 쿼리입니다.

USE AdventureWorks
GO
-- 샘플 테이블을 생성
SELECT * INTO DBO .SalesOrderDetail FROM Sales .SalesOrderDetail;

--스키마 바인딩 뷰를 생성
IF OBJECT_ID ('DBO.VSalesOrderDetail', 'view') IS NOT NULL
DROP VIEW DBO .VSalesOrderDetail ;
GO
CREATE VIEW DBO . VSalesOrderDetail
WITH SCHEMABINDING
AS
SELECT
SalesOrderID ,SalesOrderDetailID ,CarrierTrackingNumber
,OrderQty ,ProductID ,SpecialOfferID ,UnitPrice ,UnitPriceDiscount
,LineTotal ,rowguid ,ModifiedDate
FROM DBO .SalesOrderDetail

--뷰에 고유한 클러스터 인덱스를 생성하여 인덱싱 된 뷰로 생성
Create Unique Clustered Index cl_VSalesOrderDetail on VSalesOrderDetail (SalesOrderDetailID );

--인덱싱 된 뷰, 힙 테이블에 SalesOrderID를 넌 클러스터 인덱스로 만들자
CreateIndex IX_VSalesOrderDetail on VSalesOrderDetail (SalesOrderID );
CreateIndex IX_SalesOrderDetail on SalesOrderDetail (SalesOrderID );

--IO양을 확인하기 위해서.
SET STATISTICS IO ON
select * from SalesOrderDetail where SalesOrderID = 55277;
테이블 'VSalesOrderDetail'. 검색수1 , 논리적읽기수86 , 물리적읽기수0 , 미리읽기수0 , LOB 논리적읽기수0 , LOB 물리적읽기수0 , LOB 미리읽기수0.



select*from SalesOrderDetail with(index(2 ))where SalesOrderID = 55277;
테이블'SalesOrderDetail'. 검색수1 , 논리적읽기수30 , 물리적읽기수0 , 미리읽기수0 , LOB 논리적읽기수0 , LOB 물리적읽기수0 , LOB 미리읽기수0.



SET STATISTICS IO OFF


7. 마무리

너무 단편적인 예를 들어 설명한 것은 아닌지 걱정이 앞섭니다. 항상 강조하지만 서비스에 대한 정확한 이해를 바탕으로 한 고려가 필요합니다. 잘못 사용되면 성능적 많은 문제가 발생 할 수 있기 때문입니다.
굳이 제가 하고 싶은 말을 두 가지로 정리해 본다면 아래와 같습니다.
“다른 것처럼 보이는 기능이지만 비슷한 기능으로 구현 할 수 없을까?”
”인덱스에 대해 조금 더 생각을 해보자!”

이전 아티클에서 다음 아티클은 괜찮은 아티클로 뵙겠다고 하였는데, 이번에도 그 약속을 지키지 못한 것 같습니다. 다음에는 그 약속을 꼭 지키도록 하겠습니다.^^;;

아참!! 그리고 마지막으로 질문 하나를 하겠습니다.
“인덱싱 된 뷰를 이용하여 넌클러스터 인덱스 기능을 대처 할 수 있지 않을까요?”

감사합니다.

혹시 이 글에 대한 궁금증 및 잘못된 내용이 있다면 위에 있는 제 메일로 보내주시길 바랍니다.

728x90

김연욱(Vichang)님의 Inside Query Performance - (2)

 

안녕하세요. 김대우 입니다. 더운 여름 잘 지내고 계신지요?

이번에 소개해 드릴 내용은 김연욱님이 이끌어 주시는 Inside Query Performance 입니다.

아주 좋은 SQL 쿼리 성능에 대한 연재글을 올려 주고 계시며 SQLER에 많은 도움을 주시는 분이지요.

이하 내용은 김연욱님이 SQLER 자유게시판에 올려주신 항목을 정리한 내용입니다.

언제나 많은 도움 주시는 김연욱님 감사 드리구요. 앞으로도 좋은... 더운 여름 더더욱 열나게 하는 ^_^;;;

좋은 글 부탁 드립니다. 감사합니다.

이하 김연욱님의 글입니다.


 

 

일반적으로 이력관리 모델에서는 시작일자와 종료일자컬럼을 가진다.

--> 이부분에 대한것은 워낙에 내용이 길기때문에 생략.

--> 뭐 그냥 시작일자나 종료일자 하나만 가지는 설계를 사용하신다면 할 말 없음

이력모델에서 시작일자 + 종료일자 인덱스를 사용하는것이 좋을지

아니면 종료일자 + 시작일자 인덱스를 사용하는것이 좋을지

한번 테스트 해보자.

 


 


선행지식을 위해 between을 생각해보자.

일반적으로 우리가 사용하는 between은 컬럼 between 상수값1 and 상수값2  의 형태이다.

하지만, 조금 변형하여 상수값 between 컬럼1 and 컬럼2 의 형태로도 사용이 가능하다.

간단하게 예를 들어보면

벙개를 위한 장소를 몰색하던 00군이 인터넷으로 벙개용 좌석예약을 할려구 한다.

 

--drop table 좌석예약
create table 좌석예약(좌석등급 char(1), 시작번호 int, 종료번호 int, 좌석수 int, 구분 varchar(10))
go
insert into 좌석예약 values('A', 1, 100, 100, '공석')
insert into 좌석예약 values('B', 1, 100, 100, '공석')
go

 


마침 00주점에 이러한 시스템이 있었다.

사무실에서 pc로 단가가 적은 B석 51번부터 10개의 좌석을 예약한다고 해보자.

이럴때 어떻게 쿼리해야할까 ?

 

--1. 좌석상태확인
select * from 좌석예약
where 좌석등급 = 'B' and 좌석수 >= 10 and 구분='빈자리'
and 51 between 시작번호 and 종료번호

좌석 등급        시작번호    종료번호    좌석수         구분        
---- ----------- ----------- ----------- ----------
B    1           100         100         빈자리

(1개 행 적용됨)


이제 between 사용에 감이 잡히겠지요.

기왕 하는김에 좌석예약을 해보자.
 

 

--2. 좌석예약
set nocount on
declare @좌석등급 char(1), @좌석수 int, @시작번호 int, @종료번호 int, @구분 varchar(10)
declare @좌석위치 int
set @좌석등급 = 'B'
set @좌석수 = 10
set @좌석위치 = 51
set @구분 = 'SQLER 벙개'

set xact_abort on
begin tran
   if exists(select * from 좌석예약 where 좌석등급 = 'B' and 좌석수 >= 10 and 구분='빈자리'
                                      and @좌석위치 between 시작번호 and 종료번호)
      begin
         update 좌석예약
         set 시작번호 = @좌석위치,
             종료번호 = @좌석위치 + @좌석수 - 1,
             좌석수 = @좌석수,
             구분 = @구분,
             @시작번호 = 시작번호,
             @종료번호 = 종료번호
         where 좌석등급 = 'B' and 좌석수 >= 10 and 구분='빈자리'
           and @좌석위치 between 시작번호 and 종료번호

         if @시작번호 < @좌석위치  
            begin
               insert into 좌석예약 values (@좌석등급, @시작번호, @좌석위치-1,
                                            @좌석위치-@시작번호, '빈자리')
            end

         if @종료번호 > (@좌석위치 + @좌석수)
            begin
               insert into 좌석예약 values (@좌석등급, @좌석위치+@좌석수, @종료번호,
                                            @종료번호-@좌석위치-@좌석수, '빈자리')
            end
      end
commit tran
set nocount off
go

 



예약을 잘되었는지 확인해보면...

 

 

select * from 좌석예약 where 좌석등급 = 'B' order by 시작번호

좌석 등급        시작번호    종료번호    좌석수         구분        
---- ----------- ----------- ----------- ----------
B    1           50          50          빈자리
B    51          60          10          SQLER 벙개
B    61          100         39          빈자리

(3개 행 적용됨)

51번부터 60번까지 예약이 잘되있다...  근데 난 언제나 벙개에 함 나갈볼런지...

 


 

 

/*
선행지식
    1. 재미없는 긴글을 끝까지 읽어줄 강인한 인내력 (필수)

    2. sql서버는 복합컬럼인덱스일경우 첫번째 컬럼의 통계정보만을 사용함. (중요)
    3. 인덱스에 대한 기본지식
    4. between에 대한 기본지식
       일반적으로 between을 이용할경우는 변수 between 시작값 and 종료값 의 형식으로
       사용하지만 상수값 between 시작변수 and 종료변수 와 같이 사용할 수 도 있다.
       (좀더 자세한 내용은 관련 서적 참고)

    해당인덱스를 clustered index 로 할것인지 nonclustered index 로 할것인지는 별개의
    문제이므로 논외로 합니다.
*/

--언제나 테스트는 tempdb에서
use tempdb
go

--역시나 테스트를 위해서 테이블 하나 만들구
--drop table a
create table a(edt int not null, sdt int not null, num int identity)
go

--테스트를 편하게 하고자 날짜형 대신에 숫자형으로 테스트
/* ========================================================================== */
--켜져있으면 끄놓구
set statistics io off
set statistics profile off

--테스트용 샘플 넣구
set nocount on
declare @i int
set @i = 0
while (@i < 100000)
begin
insert a values(@i+1, @i)
set @i = @i + 2
end
set nocount off
select count(*) from a  --50000건

--테스트를 위해 켜두고
set statistics io on
set statistics profile on
go

 


 

 

--아무런 제약조건, 인덱스 없는 상태에서 그냥
select num from a

|--Table Scan(OBJECT:([tempdb].[dbo].[A]))
--논리적 읽기 수 137 (전체 페이지수가 137page임)
 

--먼저 시작일 + 종료일자로 pk 잡아주고
alter table a add constraint pk_a primary key nonclustered(sdt, edt)


--저 앞에 있는놈을 보면
select num from a where 90 between sdt and edt

|--Table Scan(OBJECT:([tempdb].[dbo].[A]), WHERE:(Convert([@1])>=[A].[SDT]
                                                AND Convert([@2])<=[A].[EDT]))
-- 논리적 읽기 수 137
 

-- 저 뒤에 있는 놈을 보면
select num from a where 90000 between sdt and edt

|--Table Scan(OBJECT:([tempdb].[dbo].[A]), WHERE:(Convert([@1])>=[A].[SDT]
                                                AND Convert([@2])<=[A].[EDT]))
--논리적 읽기 수 137
 

-- 강제로 pk를 타라고 힌트를 주면 어떨까 ?
select num from a (index=pk_a) where 90000 between sdt and edt
  |--Bookmark Lookup(BOOKMARK:([Bmk1000]), OBJECT:([tempdb].[dbo].[A]))

       |--Index Seek(OBJECT:([tempdb].[dbo].[A].[PK_A]), SEEK:([A].[SDT] <= 90000),
                                                         WHERE:(90000<=[A].[EDT])
--논리적 읽기 수 108
 

--그럼, top 1 을 주고 인덱스를 타라고 힌트를 주면 어떨까 ?
select top 1 num from a (index=pk_a) where 90000 between sdt and edt

  |--Top(1)
       |--Bookmark Lookup(BOOKMARK:([Bmk1000]), OBJECT:([tempdb].[dbo].[A]))
            |--Index Seek(OBJECT:([tempdb].[dbo].[A].[PK_A]), SEEK:([A].[SDT] <= 90000),
                          WHERE:(9000<=[A].[EDT]) ORDERED FORWARD)

--논리적 읽기 수 108
 

--일단, pk 지우고
alter table a drop constraint pk_a
 

--이번에는 종료일 + 시작일루 pk 잡아주고
alter table a add constraint pk_a primary key nonclustered(edt, sdt)
 

--저 앞에 있는놈을 보면
select num from a where 90 between sdt and edt

  |--Table Scan(OBJECT:([tempdb].[dbo].[A]), WHERE:(Convert([@1])>=[A].[SDT]
                                                AND Convert([@2])<=[A].[EDT]))
--논리적 읽기 수 137
 

--그럼, pk를 타라구 힌트를 주면
select num from a (index(pk_a))
where 90 between sdt and edt
select num from a (index(pk_a))  where 90 between sdt and edt

  |--Bookmark Lookup(BOOKMARK:([Bmk1000]), OBJECT:([tempdb].[dbo].[A]))
       |--Index Seek(OBJECT:([tempdb].[dbo].[A].[PK_A]), SEEK:([A].[EDT] >= 90), 
                             WHERE:(90>=[A].[SDT]) ORDERED FORWARD)
--논리적 읽기 수 120
 

--요번에는 top 1 까지 주면
select top 1 num from a (index(pk_a))
where 90 between sdt and edt

  |--Top(1)
       |--Bookmark Lookup(BOOKMARK:([Bmk1000]), OBJECT:([tempdb].[dbo].[A]))
            |--Index Seek(OBJECT:([tempdb].[dbo].[A].[PK_A]), SEEK:([A].[EDT] >= 90), 
                                  WHERE:(90>=[A].[SDT]) ORDERED FORWARD)
--논리적 읽기 수 3
 

-- 요번엔 저뒤에 있는놈을 보면
select num from a where 90000 between sdt and edt

  |--Table Scan(OBJECT:([tempdb].[dbo].[A]), WHERE:(Convert([@1])>=[A].[SDT]
                                               AND Convert([@2])<=[A].[EDT]))
--논리적 읽기 수 137

--이번에두 힌트사용
select num from a (index(pk_a)) where 90000 between sdt and edt

  |--Bookmark Lookup(BOOKMARK:([Bmk1000]), OBJECT:([tempdb].[dbo].[A]))
       |--Index Seek(OBJECT:([tempdb].[dbo].[A].[PK_A]), SEEK:([A].[EDT] >= 90000), 
                             WHERE:(90000>=[A].[SDT]) ORDERED FORWARD)
--논리적 읽기 수 15
 

--그럼, top 1 을 주면
select top 1 num from a (index(pk_a)) where 90000 between sdt and edt

  |--Top(1)
       |--Bookmark Lookup(BOOKMARK:([Bmk1000]), OBJECT:([tempdb].[dbo].[A]))
            |--Index Seek(OBJECT:([tempdb].[dbo].[A].[PK_A]), SEEK:([A].[EDT] >= 90000), 
                          WHERE:(90000>=[A].[SDT]) ORDERED FORWARD)
--논리적 읽기 수 3
 

-- 통계정보를 만들어 주면 좀더 낳지 않을까(역시 동일한 결과) - 각자 해보세요...
CREATE STATISTICS ust_a_edt
   ON test..a (edt)
   WITH FULLSCAN
CREATE STATISTICS ust_a_sdt
   ON test..a (sdt)
   WITH FULLSCAN
UPDATE STATISTICS a(pk_a)
   WITH SAMPLE 100 PERCENT

 


결론 : sql서버에서 --> 상수값 between 시작값 and 종료값 조인을 할경우
1. 시작일 + 종료일 인덱스일경우
  1) 시작일만을 사용하므로 해당조건의 선택도가 항상동일함에도 불구하고,

     시작일 <= 상수값 의 조건만을 활용한다.

       |--Index Seek(OBJECT:([test].[dbo].[A].[PK_A]), SEEK:([A].[SDT] <= 90), 
                     WHERE:(90<=[A].[EDT]) ORDERED FORWARD)

  2) 시작일 <= 상수값의 조건으로 인덱스를 스캔하면서 종료일 >= 상수값의 조건으로

     필터처리한다.(무식한놈)


2. 종료일 + 시작일 인덱스일경우
  1) 종료일만을 사용하므로 해당조건의 선택도가 항상동일함에도 불구하고,

     종료일 >= 상수값 의 조건만을 활용한다.

        |--Index Seek(OBJECT:([test].[dbo].[A].[PK_A]), SEEK:([A].[EDT] >= 90000), 
                      WHERE:(90000>=[A].[SDT]) ORDERED FORWARD)

  2) 종료일 >= 상수값의 조건으로 인덱스를 스캔하면서 시작일 <= 상수값의 조건으로

     필터처리한다.(무식한놈)


3. 결론
  1) 종료일 + 시작일 순으로 인덱스를 만들고나서

  2) SELECT 절에 TOP 1 을 추가하고

  3) INDEX(인덱스명) 힌트를 사용하면 항상 동일한 결과를 나타낸다.

  ※ 시작일 + 종료일 순으로 인덱스를 만들면 위와같이(1번에서 3번까지) 해도 비효율적인 스캔을 한다.

 


누군가가 딴지를 걸어주기를 기다렸는데 아무두 안걸어주어서 내가 딴지건다.

딴지) 뭐땜시, 복잡하게 종료일자 + 시작일자로 인덱스를 만드나, 그냥 시작일자 하나만 있어두 되던데...

 

--켜져있으면 끄놓구
set statistics io off
set statistics profile off
go

--일단, pk 지우고
alter table a
drop constraint pk_a

--이번에는 종료일 + 시작일루 pk 잡아주고
alter table a add constraint pk_a primary key nonclustered(sdt)

--테스트를 위해 다시 켜두고
set statistics io on
set statistics profile on
go

select top 1 num from a where sdt <= 90 order by sdt desc

  |--Top(1)
       |--Bookmark Lookup(BOOKMARK:([Bmk1000]), OBJECT:([tempdb].[dbo].[a]))
            |--Index Seek(OBJECT:([tempdb].[dbo].[a].[pk_a]),
                     SEEK:([a].[sdt] <= 90) ORDERED BACKWARD)
--논리적 읽기 수 3

select top 1 num from a where sdt <= 9000 order by sdt desc

  |--Top(1)
       |--Bookmark Lookup(BOOKMARK:([Bmk1000]), OBJECT:([tempdb].[dbo].[a]))
            |--Index Seek(OBJECT:([tempdb].[dbo].[a].[pk_a]),
                     SEEK:([a].[sdt] <= 9000) ORDERED BACKWARD)
--논리적 읽기 수 3

 

두경우 모두 논리적 읽기 수가 3이다. 이것이 더 좋지 않느냐, 쿼리도 깔끔하구..
 


그럼, sample을 조금 바꿔서 다른경우를 살펴보자.

 

--켜져있으면 끄놓구
set statistics io off
set statistics profile off


--drop table k1
create table k1(상품코드 int not null, 상품명 varchar(100))
go
alter table k1
   add constraint pk_k1 primary key nonclustered(상품코드)
go
set nocount on
declare @i int
set @i = 1
while(@i <= 1000)
begin
   insert into k1 values(@i, 'HDD ' +convert(varchar(10),@i)+'G')
   set @i = @i + 1
end
set nocount off
go

--drop table k2
create table k2(적용일자 datetime not null, 종료일자 datetime not null, 단가 int)
go
set nocount on
declare @i datetime
set @i = '2000-01-01'
while(@i <= '2003-04-30')
begin
    insert into k2 values(@i, dateadd(dd,1,@i), convert(int, rand()*1000))
    set @i = dateadd(dd, 1, @i)
end
set nocount off

--drop table k3
create table k3(상품코드 int not null, 적용일자 datetime not null,
                종료일자 datetime not null, 단가 int)
go
alter table k3
   add constraint pk_k3 primary key nonclustered(상품코드, 적용일자)
go
set nocount on
insert into k3
   select k1.상품코드, k2.적용일자, k2.종료일자, k2.단가
   from k1 cross join k2
set nocount off

--drop table k4
create table k4(상품코드 int not null, 적용일자 datetime not null,
                종료일자 datetime not null, 단가 int)
go
alter table k4
   add constraint pk_k4 primary key nonclustered(상품코드, 종료일자, 적용일자)
go
set nocount on
insert into k4
   select k1.상품코드, k2.적용일자, k2.종료일자, k2.단가
   from k1 cross join k2
set nocount off

 


 


 

 

--다시 테스트를 위하여 켜놓구
set statistics io on
set statistics profile on
go

select count(*) from k3 --1216000
'k3' 테이블. 스캔 수 1, 논리적 읽기 수 3458

select count(*) from k4 --1216000
'k4' 테이블. 스캔 수 1, 논리적 읽기 수 4662

 

인덱스size 차이로 인하여 k4가 page수가 더 많다.

 

--1. 1번 상품의 '2003-03'월 단가변동내역이 필요하다면 ?

select 적용일자, 단가
from k3
where 적용일자 <= '2003-03-31' and 종료일자 >= '2003-03-01'
and 상품코드=1


|--Table Scan(OBJECT:([tempdb].[dbo].[k3]),
           WHERE:(([k3].[적용일자]<=Convert([@1]) AND [k3].[종료일자]>=Convert([@2]))
           AND [k3].[상품코드]=Convert([@3])))

'k3' 테이블. 스캔 수 1, 논리적 읽기 수 5197

 

적용일자가 2003-03-31일 보다 작은것이 대부분이기 때문에 table scan을 선택했다.

 

 

/* ========================================================================== */

select 적용일자, 단가
from k4
where 적용일자 <= '2003-03-31' and 종료일자 >= '2003-03-01'
and 상품코드=1

  |--Bookmark Lookup(BOOKMARK:([Bmk1000]), OBJECT:([tempdb].[dbo].[k4]))
       |--Index Seek(OBJECT:([tempdb].[dbo].[k4].[pk_k4]),
                SEEK:([k4].[상품코드]=Convert([@3]) AND [k4].[종료일자] >= Convert([@2])), 
                WHERE:([k4].[적용일자]<=Convert([@1])) ORDERED FORWARD)

'k4' 테이블. 스캔 수 1, 논리적 읽기 수 36

 

인덱스만을 읽어서 범위를 줄인후에 테이블을 읽어서 단가를 가져왔다.

논리적읽기수를 비교해봐라 엄청난 차이다.


물론, 상황에 따라서 적절한 인덱스 설계를 해야하겠지만

일반적으로 이력모델에서는 종료일자 + 시작일자 의 복합인덱스가 유리한 경우가 많다.

 


김연욱(Vichang)님의 Inside Query Performance - (2)

 

저작권 : 이 홈페이지의 내용은 모두 자유롭게 사용하실 수 있습니다.

+ Recent posts