메뉴 건너뛰기

bysql.net

8. 인덱스 설계

2011.03.07 05:00

멋진넘 조회 수:8345

08 인덱스 설계
(1)가장 중요한 두 가지 기준
   1. 조건절에 항상 사용되거나, 자주 등장하는 컬럼들을 선정한다.
   2. '=' 조건으로 자주 조회되는 컬럼들을 앞쪽에 둔다
  
(2)인덱스 설계는 공식이 아닌 전략과 선택의 문제
   ▶간단한 설계 예제 - 전제조건 총 고객수 100만명, 상품은 10만개
     ------------------------------------------------------------------
     | 검색조건 1 | WHERE 고객번호 = 1                                |
     |            |   AND 거래일자 BETWEEN '20090101' AND '20091231'  |
     ------------------------------------------------------------------
     | 검색조건 2 | WHERE 상품번호 = 'A'                              |
     |            |   AND 거래일자 BETWEEN '20090101' AND '20091231'  |
     ------------------------------------------------------------------
     | 검색조건 3 | WHERE 고객번호 = 1                                |
     |            |   AND 상품번호 = 'A'                              |
     |            |   AND 거래일자 BETWEEN '20090101' AND '20091231'  |
     ------------------------------------------------------------------
     | 검색조건 4 | WHERE 거래일자 BETWEEN '20090101' AND '20091231'  |
     ------------------------------------------------------------------
  
   ▶Review(p182 참조)
     A : '=' 조건 컬럼을 모두 선두에 사용할려는 인덱스 스캔 효율을 100% 달성하려는 스타일
         나중에 새로운 액세스 유형이 도출되었을 때 문제가 될 수 있다.
     C : 검색조건 3일 때도 불필요한 테이블 액세스가 발생하지 않도록 하려고 X1 인덱스에
         상품번호를 추가한 점이 좋은 전략이라고 평가할 수 있다.
     (중간 생략...)
     I : X2 인덱스 선두에 거래일자를 둠으로 약간의 비효율은 생기겠지만 모든 검색조건에
         범용적으로 사용될 수 있다. 또한 모든 검색 조건을 만족하면서도 인덱스 개수를
         최소화 했다는 측면에서 높이 평가할 수 있따.
  
   ▶효율성 비교 분석 - Data상황 및 전제조건 참조 (그림 1-55 참조)
     -----------------------------------------------------------------------------
      [전략1 : 스타일 A]                    | [전략2 : 스타일 I]
     -----------------------------------------------------------------------------
      X1 : 고객번호 + 거래일자              | X1 : 고객번호 + 거래일자
      X2 : 상품번호 + 거래일자              | X2 : 거래일자 + 상품번호 + 고객번호
      X3 : 상품번호 + 고객번호 + 거래일자   |
      X4 : 거래일자                         |
     -----------------------------------------------------------------------------
    
     --------------------------------------------------------------------------------------
     | 검색조건 1 | 1번 고객의 거래 데이터가 2,3 블럭에 흩어져 있다.
     |            | 따라서 전략1,2 모두 X1 인덱스를 사용함으로 동일함
     --------------------------------------------------------------------------------------
     | 검색조건 2 | 상품 A가 1,2,3 블럭에 흩어져 있다.
     |            | 전략 1,2 모두 X2 인덱스 사용. Random Access또한 3번 동일
     |            | 다만 전략 2의 x2는 불필요한 상품번호까지 스캔(인덱스의 구성차이)
     --------------------------------------------------------------------------------------
     | 검색조건 3 | 조건을 만족하는 데이터는 2번 테이블 블록에 단 한건 존재
     |            | 전략 1의 X3 인덱스는 단 한 블록만 Random Access
     |            | 전략 2의 x1 인덱스 이용시는 테이블 Access가 2번 발생
     |            | 거래일자 범위가 넓지 않다면 인덱스 비효율을 감소하더라도
     |            | X2 인덱스를 사용해 불필요한 테이블 Random Access를 고려할 수 있음
     --------------------------------------------------------------------------------------
     | 검색조건 4 | 전략 1의 X4 인덱스나 전략 2의 X2 인덱스 둘다 1~3 블럭을 동일하게
     |            | 처리함. Random Access 횟수도 똑같음
     --------------------------------------------------------------------------------------
  
(3)결함 인덱스 컬럼 순서 결정 시, 선택도 이슈
   선택도(Selectivity) : 전체 레코드 중에서 조건절에 의해 선택되는 비율. 여기서는 인덱스
                         컬럼이 모드 '='로 조회할 때 평균적으로 선택되는 비율
                        
  
   인덱스 생성 여부를 결정할 때는 선택도가 충분히 낮은지가 중요한 판단기준임이 틀림없지만
   결합 인덱스 컬럼 간 순서를 정할 때 변별력이 높은 쪽을 앞에 두는 것이 유리한지는 상황에
   따라 다르다.
  
   ▶선택도가 액세스 효율에 영향을 주지 않는 경우
     eg) IDX01 : 고객등급 + 고객번호 + 거래일자 + 거래유형 + 상품번호
     예를 들어 고객번호와 고객등급은 '=' 조건으로, 거래일자는 between으로 사용된다.
 

     '=' 조건으로 항상 사용되는 컬럼들을 앞쪽에 위치시켰다면, 그 중 선택도가 낮은 것을 앞쪽에
     두려는 노력은 의미가 없거나 오히려 손해힐 수 있다. 여기선 첫번째 범위 검색(Range Scan)
     조건(위에선 거래일자 between으로 Range Scan)까지의 인덱스 레코드들은 모두 한 곳에 모여
     있기에 스캔 범위는 최소화될 것이고 인덱스 액세스 효율에 영향을 미치지 않는다.
    
   ▶선택도가 '높은 컬럼'을 앞쪽에 두는 것이 유리한 경우
     위의 예제에서 [고객등급]을 선두에 두면 나중에 이 조건절이 누락되거나 범위검색으로 조회
     되는 일이 생기더라도 Index Skip Scan을 효과적으로 할용할 수 있어서 유리하다.
    
     참고로 Index Skip Scan은 선행 컬럼의 Distinct Value 개수가 적고, 후행 컬럼의 Distinct
     Value 개수가 많아야 효율적이다. 또한 ISS(Index Skip Scan)가 아니더라도 첫 컬럼에 대해 In-List로 값을
     제공할 수 있기에 튜닝의 여지가 생긴다.
    
   ▶상황에 따라 바뀌는 경우
     위의 예제에선 두 컬럼 모두가 '=' 조건인 경우에 대해서 살펴 봄
     아래는 두 컬럼 모두가 '='가 아닌 최소한 한 컬럼이 Range로 비교되는 경우는 상황에 따라 정리
    
     CREATE TABLE TRADE_T
     NOLOGGING
     AS
     SELECT CEIL(DBMS_RANDOM.VALUE(1, 50000))     AS CUST_ID
          , DBMS_RANDOM.STRING('U', 1)            AS CUST_GR
          , SYSDATE - DBMS_RANDOM.VALUE(1, 365*3) AS TR_DATE
       FROM DUAL
     CONNECT BY LEVEL < 1000001
     ;
    
     CREATE INDEX TRADE_T_IDX01 ON TRADE_T(CUST_ID, CUST_GR, TR_DATE) NOLOGGING;
    
     CREATE INDEX TRADE_T_IDX02 ON TRADE_T(CUST_GR, CUST_ID, TR_DATE) NOLOGGING;

     DROP TABLE GRADE_T;
     CREATE TABLE GRADE_T NOLOGGING
     AS
     SELECT CHR(64 + ROWNUM) AS CUST_GR FROM DUAL CONNECT BY LEVEL < 27
     ;
    
     SELECT * FROM TRADE_T WHERE CUST_ID = 2000;
     -------------------------------------------------------------------------------------
          조 건 절    |                           인덱스  구성
     -----------------|-------------------------------------------------------------------
     고객번호|고객등급| 고객번호 + 고객등급 + 거래일자  | 고객등급 + 고객번호 + 거래일자
     --------|--------|---------------------------------|--------------------------------
             |        | 스캔범위가 넓어 불리하지만 고객 | 유리                          
             |        | 번호에 입력범위가 좁을 때는     |                         
        >=   |    =   | 비효율이 크지 않음. 입력범위가  |
             |        | 넓을 때가 문제이고 ISS 또는     |
             |        | In-List 기법을 활용하지 못하는  |
             |        | 단점이 있음                     |
     --------|--------|---------------------------------|---------------------------------
             |        | 유리                            | 선택도가 높은 고객등급 스캔범위
         =   |   >=   |                                 | 가 결정되어 불리하지만, ISS 또는
             |        |                                 | In-List를 활용한다면 큰 비효율은
             |        |                                 | 없음
     --------|--------|---------------------------------|---------------------------------
             |        | 고객번호에 입력범위가 좁을 때는 | 선택도가 높은 고객등급 스캔범위
             |        | 비효율이 크지 않음. 입력범위가  | 가 결정되어 불리하지만, ISS 또는
        >=   |   >=   | 넓을 때가 문제이고 ISS 또는     | In-List를 활용한다면 큰 비효율은
             |        | In-List 기법을 활용하지 못하는  | 없음                           
             |        | 단점이 있음                     |
     --------|--------|---------------------------------|---------------------------------
         =   | (누락) | 유리                            | ISS 또는 In-List 활용가능
     --------|--------|---------------------------------|---------------------------------
      (누락) |    =   | Full Table Scan 이 유리         | Full Table Scan 이 유리
     -------------------------------------------------------------------------------------
     종합해 보면, 선택도가 높은 컬럼(예, 고객등급)을 선두에 두면 나중에 범위검색 조건이 사용되거나
     아예 조건절에서 누락되더라도 ISS(Index Skip Scan) 또는 In-List를 활용할 수 있어 유리하다.
                                                                  
   ▶선택도가 [낮은 컬럼]을 앞쪽에 두는 것이 유리한 경우 - 실무에서 많이 적용됨
     범위검색 조건을 사이에 둔 컬럼끼리는 선택도가 낮은 컬럼을 앞쪽에 두는 것이 유리하다
    
     eg)고객 : 100만명, 상품 : 1만개
     WHERE 고객번호 = :a
       AND 상품번호 = :b
       AND 거래일자 BETWEEN :c
                        AND :d
    
     X01 : 고객번호 + 거래일자 + 상품번호
     X02 : 상품번호 + 거래일자 + 고객번호   
    
     중간에 놓인 거래일자가 BETWEEN 조건이어서 변별력이 좋은 컬럼에 의해 스캔범위가 최소화
     되기에 X01이 유리하다.                                                     
                                                                  
   ▶선택도가 [낮은 컬럼]을 [선택]하는 것이 유리한 경우 - 비 현실적이나 참고할 만함
  
   결론적으로 [결합 인덱스] 컬럼 간 순서를 정할 때는, 개별 컬럼의 선택도보다는 조건절에서
   어떤 형태로 자주 사용되는지, 사용빈도는 어느 쪽이 높은지, 데이터를 빠르게 검색하는 데에
   어느 쪽 효용성이 높은지 등이 더 중요한 판단기준이다.
 

(4)소트 오퍼레이션을 생략하기 위한 컬럼 추가
   인덱스는 항상 절력 상태를 유지하므로 ORDER BY, GROUP BY를 위한 소트 연산을 생략할 수
   있도록 해준다. 따라서 조건절에 사용하지 않은 컬럼이더라도 소트 연산을 대체할 목적으로
   인덱스 구성에 포함시킴으로써 성능 개선을 도모할 수 있다.
  
   CREATE TABLE T
   NOLOGGING
   AS
   SELECT ROWNUM A, ROWNUM B, ROWNUM C, ROWNUM D, ROWNUM E
     FROM DUAL
   CONNECT BY LEVEL <= 100000;
  
   CREATE INDEX T_IDX ON T(A, B, C, D);
      
   SELECT * FROM T WHERE A = 1           ORDER BY A, B, C
   --UNION ALL
   SELECT * FROM T WHERE A = 1 AND B = 1 ORDER BY C, D
   --UNION ALL
   SELECT * FROM T WHERE A = 1 AND C = 1 ORDER BY B, D
   --UNION ALL
   SELECT * FROM T WHERE A = 1 AND B = 1 ORDER BY A, B, C, D;
 

   ▶PLAN을 살펴보면 SORT ORDER BY가 없음
   -------------------------------------------------------------------------------------
   | Id  | Operation                   | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
   -------------------------------------------------------------------------------------
   |   0 | SELECT STATEMENT            |       |     1 |    65 |     3   (0)| 00:00:01 |
   |   1 |  TABLE ACCESS BY INDEX ROWID| T     |     1 |    65 |     3   (0)| 00:00:01 |
   |*  2 |   INDEX RANGE SCAN          | T_IDX |     1 |       |     2   (0)| 00:00:01 |
   -------------------------------------------------------------------------------------
  
   단, ORDER BY 절의 순서가 INDEX 순서와 다를 경우는 SORT 발생함
   SELECT * FROM T WHERE A = 1           ORDER BY C, B, A;    
   --------------------------------------------------------------------------------------
   | Id  | Operation                    | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
   --------------------------------------------------------------------------------------
   |   0 | SELECT STATEMENT             |       |     1 |    65 |     4  (25)| 00:00:01 |
   |   1 |  SORT ORDER BY               |       |     1 |    65 |     4  (25)| 00:00:01 |
   |   2 |   TABLE ACCESS BY INDEX ROWID| T     |     1 |    65 |     3   (0)| 00:00:01 |
   |*  3 |    INDEX RANGE SCAN          | T_IDX |     1 |       |     2   (0)| 00:00:01 |
   --------------------------------------------------------------------------------------

  
   또한, 아래처럼 ORDER BY 절에 기술한 순서가 인덱스 순서와 일치하기만 한다면 조건절에서
   어떤 연산자로 비교되더라도 정렬작업이 생략될 수 있다
   --------------------------------------------------------------------
         INDEX RANGE SCAN(A)  |            INDEX FULL SCAN(B)
   --------------------------------------------------------------------
    SELECT * FROM T           | SELECT /*+ INDEX(T, T_IDX) */ * FROM T
     WHERE A BETWEEN 1 AND 2  |  WHERE B BETWEEN 2 AND 3
       AND B NOT IN (1, 2)    |  ORDER BY A, B, C, D;
       AND C BETWEEN 2 AND 3  |
     ORDER BY A, B, C, D;     |
                              |
    SELECT * FROM T           |
     WHERE A BETWEEN 1 AND 2  |
       AND C BETWEEN 2 AND 3  |
     ORDER BY A, B, C;        |
                              |
    SELECT * FROM T           |
     WHERE A BETWEEN 1 AND 2  |
       AND B <> 3             |
     ORDER BY A, B, C;        |
   --------------------------------------------------------------------
   ▶PLAN을 살펴보면 SORT ORDER BY가 없음
     위 쿼리들에 대해 정렬작업을 생략할 수 있다는 것이지 옵티마이저가 항상 그런 선택을
     한다는 뜻은 아니다. 통계정보를 기반으로 비용을 계산한 결과 옵티마이져가 Table Full Scan을
     선택하거나 다른 인덱스를 선택한다면 정렬작업은 별도로 수행된다.
    
   ▶CASE A - 3번째 컬럼 기준
   -------------------------------------------------------------------------------------
   | Id  | Operation                   | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
   -------------------------------------------------------------------------------------
   |   0 | SELECT STATEMENT            |       |    13 |   845 |     3   (0)| 00:00:01 |
   |   1 |  TABLE ACCESS BY INDEX ROWID| T     |    13 |   845 |     3   (0)| 00:00:01 |
   |*  2 |   INDEX RANGE SCAN          | T_IDX |     1 |       |     2   (0)| 00:00:01 |
   -------------------------------------------------------------------------------------
   
   Predicate Information (identified by operation id):
   ---------------------------------------------------
      2 - access("A">=1 AND "A"<=2)
          filter("B"<>3)
  
   ▶CASE B
   -------------------------------------------------------------------------------------
   | Id  | Operation                   | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
   -------------------------------------------------------------------------------------
   |   0 | SELECT STATEMENT            |       |     5 |   325 |   430   (1)| 00:00:06 |
   |   1 |  TABLE ACCESS BY INDEX ROWID| T     |     5 |   325 |   430   (1)| 00:00:06 |
   |*  2 |   INDEX FULL SCAN           | T_IDX |   456 |       |   428   (1)| 00:00:06 |
   -------------------------------------------------------------------------------------  

   Predicate Information (identified by operation id):
   ---------------------------------------------------
      2 - access("B">=2 AND "B"<=3)
          filter("B">=2 AND "B"<=3)  
    
   ▶아래의 SQL은 인덱스로 소트오퍼레이션 대체 불가한 경우의 예제이다. 즉 ORDER BY에 기술된
     컬럼의 순서가 INDEX 순서와 맞질 않거나, 조건절의 인덱스 컬럼이 ORDER BY절에 생략된 경우이다.

   SELECT * FROM T WHERE A = 1                       ORDER BY C;
   SELECT * FROM T WHERE A = 1 AND B BETWEEN 2 AND 3 ORDER BY C, D;
   SELECT * FROM T WHERE A = 1 AND B BETWEEN 1 AND 2 ORDER BY A, C, B

   --------------------------------------------------------------------------------------
   | Id  | Operation                    | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
   --------------------------------------------------------------------------------------
   |   0 | SELECT STATEMENT             |       |     1 |    65 |     4  (25)| 00:00:01 |
   |   1 |  SORT ORDER BY               |       |     1 |    65 |     4  (25)| 00:00:01 |
   |   2 |   TABLE ACCESS BY INDEX ROWID| T     |     1 |    65 |     3   (0)| 00:00:01 |
   |*  3 |    INDEX RANGE SCAN          | T_IDX |     1 |       |     2   (0)| 00:00:01 |
   --------------------------------------------------------------------------------------

(기타참고)
http://blog.naver.com/bacchusl?Redirect=Log&logNo=50002996263

옵티마이저의 인덱스 선택기준
※ 인덱스 매칭률

   Where절에 첫째 컬럼부터 연속된 컬럼에 대해서 '='조건으로 상수가 사용된 인덱스 컬럼의 개수
   -----------------------------------------------------------------------------------------
   인덱스 컬럼의 총 개수

1. '=' 조건으로 매칭되는 where 조건절의 컬럼의 매칭율(컬럼=상수인 컬럼수/인덱스컬럼의 수)이 높은것
   예) 컬럼 3개로 구성된 결합인덱스의 컬럼이 where절에서 2개가 = 로 사용되더라도
       컬럼 1개로 구성된 인덱스컬럼이 '=' 로 사용되면 66%(2/3)와 100%(1/1) 비교로 인해
       컬럼 1개로 구성된 인덱스가 옵티마이져에의해 선택됨
2. 인덱스 컬럼의 매칭율이 동일할 경우 단일컬럼보다는 여러개의 컬럼으로 구성된 결합인덱스를 더선호하게됨
3. 인덱스 컬럼의 매칭율과 컬럼수도 같다면, 최근에 생성된것을 더 선호함


http://www.dator.co.kr/data24hh/textyle/44413

결합 인덱스 생성시 컬럼 순서

특정 테이블에 인덱스를 생성하는 경우, 특히 여러 개의 컬럼을 결합하여 인덱스를 생성할 때
다음과 같은 방법으로 컬럼을 선정하는 것을 권고합니다.

1. 항상 사용되는 컬럼인가?
   ▶ 당연히 사용되지 않는 컬럼을 인덱스에 포함할 필요는 없겠죠?
2. 항상 '=' 조건으로 사용되는 컬럼인가?
   ▶ 인덱스에서 선두 컬럼의 조건이 범위로 들어오는 경우 해당 컬럼 이후의 모든 컬럼들은
      필터로 처리되기 때문에 처리 범위를 줄여줄 수 없습니다.(★★★)
      따라서 가능하면 '=' 조건으로 사용되는 컬럼을 선두로 하는 것이 유리합니다.

   SELECT * FROM T        
    WHERE A = 2
      AND B = 3
   UNION ALL
   SELECT * FROM T        
    WHERE A BETWEEN 1 AND 2
      AND B = 3;
     
   --------------------------------------------------------------------------------------
   | Id  | Operation                    | Name  | Rows  | Bytes | Cost (%CPU)| Time     |
   --------------------------------------------------------------------------------------
   |   0 | SELECT STATEMENT             |       |     4 |   260 |     4  (75)| 00:00:01 |
   |   1 |  UNION-ALL                   |       |       |       |            |          |
   |   2 |   TABLE ACCESS BY INDEX ROWID| T     |     1 |    65 |     1   (0)| 00:00:01 |
   |*  3 |    INDEX RANGE SCAN          | T_IDX |     1 |       |     1   (0)| 00:00:01 |
   |   4 |   TABLE ACCESS BY INDEX ROWID| T     |     3 |   195 |     3   (0)| 00:00:01 |
   |*  5 |    INDEX RANGE SCAN          | T_IDX |     1 |       |     2   (0)| 00:00:01 |
   --------------------------------------------------------------------------------------
   
   Predicate Information (identified by operation id):
   ---------------------------------------------------
      3 - access("A"=2 AND "B"=3)
      5 - access("A">=1 AND "B"=3 AND "A"<=2)
          filter("B"=3)  

3. 액세스(Access) 조건으로 사용될 수 있는 컬럼은 어느 컬럼이며 어떤 컬럼이 처리범위를 줄여줄 수 있는가?
   ▶ 인덱스에서 액세스 범위를 줄여줌
4. 자주 정렬되는 컬럼의 순서는?
   ▶ order by에 대한 부담을 제거할 수 있으며, 부분 범위 처리로 유도할 수 있음
5. 부가적으로 추가될 수 있는 컬럼은 무엇인가?
   ▶ 인덱스에서 필터 조건으로 사용될 수 있거나
   ▶ select list절에서 사용될 수 있는지(Covered Index)
   ▶ 인덱스에서 테이블 랜덤 액세스에 대한 횟수를 줄여줄 수 있음

 

번호 제목 글쓴이 날짜 조회 수
60 Front Page file 운영자 2011.02.16 149419
59 3. 인덱스 파티셔닝 darkbeom 2011.06.20 53824
58 3. 다양한 인덱스 스캔 방식 file 멋진넘 2011.02.19 33807
57 8. 통계정보 Ⅱ [1] 멋진넘 2011.04.30 31075
56 2. 파티션 Pruning 실천하자 2011.06.22 26018
55 3. 뷰 Merging 실천하자 2011.05.15 23381
54 3. 해시 조인 file darkbeom 2011.03.21 21530
53 2. 서브쿼리 Unnesting darkbeom 2011.05.16 19700
52 4. 통계정보 Ⅰ darkbeom 2011.04.26 18091
51 7. 인덱스 스캔 효율 [1] 휘휘 2011.03.09 16889
50 7. Sort Area 크기 조정 실천하자 2011.06.14 15067
49 4. 테이블 Random 액세스 부하 [1] file darkbeom 2011.02.24 14677
48 4. 조인 순서의 중요성 운영자 2011.03.28 14240
47 1. 인덱스 구조 [1] file 실천하자 2011.02.16 14190
46 1. 기본 개념 멋진넘 2011.06.28 13396
45 8. 고급 조인 테크닉-1 [1] file darkbeom 2011.04.04 13265
44 9. 비트맵 인덱스 file 실천하자 2011.03.06 12342
43 1. 옵티마이저 file 실천하자 2011.04.18 11211
42 6. 히스토그램 실천하자 2011.04.25 10917
41 6. Sort Area를 적게 사용하도록 SQL 작성 file 실천하자 2011.06.14 8857