Database

[Oracle Tunning] 인덱스 활용과 튜닝

춘햄 2024. 8. 28. 11:28

Clustering Factor

- 데이터가 모여 있는 정도를 수치로 보여주는 것
- 데이터 분포도에 따라서 데이터 블록에 접근하는 횟수가 달라지기 때문에 성능 측면에서 중요한 요소이다.

- 클러스터링 펙터 출력 쿼리

SELECT 
	T.TABLE_NAME, 
	I.INDEX_NAME, 
	T.BLOCKS, 
	T.NUM_ROWS, 
	I.CLUSTERING_FACTOR
FROM USER_TABLES T, USER_INDEXES I
WHERE I.TABLE_NAME = T.TABLE_NAME
	AND T.TABLE_NAME IN ('ORD_ITEM', 'ORD_ITEM_RANDOM');

- 클러스터링 팩터는 테이블 자체로는 좋다 나쁘다고 할 수 없다. 특정 인덱스를 이용할 때, 해당 인덱스으 클러스터링 팩터가 좋은지 나쁜지를 말할 수 있다.
- 클러스터링 팩터의 값이 블록 수에 가까우면 좋은 것이고, 로우 수에 가까우면 나쁜 것이다.


기존 인덱스에 컬럼 추가하기

아래와 같은 테이블과 인덱스 구성이 있다고 가정하자.
    - 테이블
        - 상품 ID(PK)
        - 상품명
        - 상품구분코드
        - 판매여부
    - 인덱스
        - 상품구분코드
        - 상품명
 

위와 같은 인덱스를 사용하여

SELECT * FROM 상품 WHERE 상품구분코드 = '100101' AND 판매여부 = 'Y';

 

쿼리를 날리면


1. 상품 구분 코드 인덱스를 스캔하고 상품 구분 코드가 100101인 데이터를 모두 테이블에서 랜덤 엑세스 한 뒤, 판매여부가 조건에 맞는 데이터를 찾는다.


2. 조건으로 붙이는 상품구분코드 + 판매여부 순서로 된 인덱스를 추가하면 가장 좋겠지만 이런 식으로 인덱스를 추가하면 추후에 다른 조건으로 select를 수행할 때마다 비슷한 식으로 인덱스를 추가해야 한다. 


3. 이런 상황에서는 기존 인덱스의 맨 뒤에 판매여부 칼럼을 추가하여 사용해야 한다.


4. 인덱스의 용량은 증가하겠지만, 테이블로의 래덤 엑세스는 최종 결과 집합에 해당하는 데이터만큼만 엑세스 하기 때문에 성능면에서 좋은 효과를 볼 수 있다.


다른 경우로 ORD_X01 => ORD_DT + ORD_HMS가 있고

SELECT 
	COUNT(UPPER_CASE)
FROM ORD
WHERE ORD_DT BETWEEN '20120101' AND '20120228'
AND SHOP_NO = 'SH0001';

위 쿼리를 날렸을 때, SHOP_NO가 인덱스에 없는 칼럼이기 때문에 테이블의 모든 데이터에 엑세스하여

SHOP_NO = 'SH0001' 조건을 처리하여 6213건을 최종 집합으로 만든다.

-------------------------------------------------------------------------------------------------
| Id  | Operation                            | Name    | E-Rows |E-Bytes| Cost (%CPU)| E-Time   |
-------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                     |         |        |       | 32551 (100)|          |
|   1 |  SORT AGGREGATE                      |         |      1 |    27 |            |          |
|*  2 |   TABLE ACCESS BY INDEX ROWID BATCHED| ORD     |   6213 |   163K| 32551   (1)| 00:00:02 |
|*  3 |    INDEX RANGE SCAN                  | ORD_X01 |   1251K|       |  4692   (1)| 00:00:01 |
-------------------------------------------------------------------------------------------------

 

즉, 125만 건 이상의 INDEX RANGE SCAN이 불필요한 것이다.


이럴 때도 기존 인덱스에 SHOP_NO 컬럼을 추가하면 SHOP_NO만 가지는 새 인덱스를 추가하는 것 보다 효율적이다. (새 인덱스를 스캔하는 시간을 줄이기 때문에)

DROP INDEX ORD_X01;
CREATE INDEX ORD_X01 ON ORD(ORD_DT, ORD_HMS, ORD_NO);

 

이제 동일한 쿼리를 날려보면

-------------------------------------------------------------------------------------------------
| Id  | Operation                            | Name    | E-Rows |E-Bytes| Cost (%CPU)| E-Time   |
-------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                     |         |        |       |  1080 (100)|          |
|   1 |  SORT AGGREGATE                      |         |      1 |    27 |            |          |
|   2 |   TABLE ACCESS BY INDEX ROWID BATCHED| ORD     |   6213 |   163K|  1080   (1)| 00:00:01 |
|*  3 |    INDEX RANGE SCAN                  | ORD_X01 |   6213 |       |   915   (1)| 00:00:01 |
-------------------------------------------------------------------------------------------------

 

랜덤 액세스 횟수가 비약적으로 감소하는 것을 확인할 수 있다.


PK 인덱스 확장하기

PK 인덱스를 변경할 때 주의할 사항은 기존 PK 컬럼들을 선두에 놓아야 한다는 것이다.


PK 인덱스의 변경 처리는 아래와 같다.

CREATE TABLE ITEM (
	ITEM_ID NUMBER(10),
	ITEM_NM VARCHAR2(100),
	ITEM_TYPE_CD VARCHAR2(6),
	SALE_YN VARCHAr2(1),
	...
);

CREATE INDEX ITEM_PK ON ITEM(ITEM_ID, ITEM_NM);
ALTER TABLE ITEM ADD CONSTRAINT ITEM_PK PRIMARY KEY (ITEM_ID) USING INDEX ITEM_PK;

인덱스 스캔 범위 계산

SELECT * FROM 상품 WHERE 상품명 = '불고기버거' AND 상품구분코드 BETWEEN '100100' AND '100101';

위 쿼리가 있을 때,


INDEX01(상품구분코드, 상품명)과 INDEX02(상품명, 상품구분코드)를 사용한 스캔의 범위는 분명한 차이가 난다.


1번은 상품구분코드의 범위를 다 읽으면서 불고기 버거를 필터링하는 방식이고,
2번은 = 조건이 먼저 있어서 상품명을 인덱스 시작지점으로 찾은 후에 상품코드를 찾게 된다.

 

CREATE INDEX ORD_X02 ON ORD(ORD_DT, SHOP_NO);
CREATE INDEX ORD_X03 ON ORD(SHOP_NO, ORD_DT);

SELECT /*+ INDEX(ORD_X02) */
    * 
FROM ORD
WHERE ORD_DT BETWEEN '20120101' AND '20120131'
AND SHOP_NO = 'SH0001';

------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name    | E-Rows |E-Bytes| Cost (%CPU)| E-Time   |
------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |         |        |       |   280 (100)|          |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| ORD     |    305 | 47275 |   280   (1)| 00:00:01 |
|*  2 |   INDEX SKIP SCAN                   | ORD_X02 |    305 |       |     6  (17)| 00:00:01 |
------------------------------------------------------------------------------------------------

SELECT
    * 
FROM ORD
WHERE ORD_DT BETWEEN '20120101' AND '20120131'
AND SHOP_NO = 'SH0001';

------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name    | E-Rows |E-Bytes| Cost (%CPU)| E-Time   |
------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |         |        |       |   278 (100)|          |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| ORD     |    305 | 47275 |   278   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN                  | ORD_X03 |    305 |       |     4   (0)| 00:00:01 |
------------------------------------------------------------------------------------------------