[DB] 2장. 인덱스 기본

2025. 1. 25. 00:45·Study/친절한 SQL 튜닝

1. 인덱스구조 및 탐색 

🔎 DB 테이블에서 데이터를 찾는 방법

  • 테이블 전체를 스캔함 → 튜닝요소가 많지 않음 
  • 인덱스를 이용함 → 튜닝 요소가 많고, 기법도 매우 다양 
찾으려는 데이터가 중복이 많다면 전자가 좋을 것이고, 별로 없다면 후자가 좋을 것이다.
인덱스는 큰 테이블에서 소량 데이터를 검색할때 사용  →  온라인 트랜잭션 처리 ( Online Transaction Processing 'OLTP' ) 시스템에서 소량 데이터를 주로 검색하므로 인덱스 튜닝이 중요 !

 

🔎 인덱스 튜닝의 두 가지 핵심 요소

 

1. 인덱스 스캔 효율화 튜닝

  • 인덱스 스캔 과정에서 발생하는 비효율을 줄이는 것

예 ) 학생명부에서 시력이 1.0 ~ 1.5인 홍길동 학생을 찾는 경우

학생명부를 이름과 시력순으로 정렬해 두었다면, 소량만 스캔이 가능 → 2개의 로우 스캔 

이름 시력 학년-반-번호
김수지 1.5 4학년 3반 37번
김철수 0.5 3학년 3반 23번
.... ..... ....
홍길동 1.0 2학년 4반 22번
홍길동 1.5 1학년 2반 39번
홍길동 2.0 3학년 9반 2번

반면, 학생명를 시력과 이름순으로 정렬해 두었다면, 많은 양을 스캔해야함  → 3개의 로우 스캔 

시력 시력 학년-반-번호
0.5 김철수 4학년 3반 37번
1.0 홍길동 3학년 3반 23번
.... ..... ....
1.5 이영희 2학년 7반 17번
1.5 홍길동 2학년 4반 22번
2.0 홍길동 1학년 2반 39번

 


2. 랜덤 액세스 최소화 튜닝

  • 테이블 액세스 횟수를 줄이는 것 
  • 인덱스 스캔 후 테이블 레코드를 액세스할 때 랜덤 I/O방식을 사용함
예 ) 학생명부에서 시력이 1.0 ~ 1.5인 홍길동 학생을 찾는 경우
- 시력이 1.0~1.5인 학생은 총 50명
- 이름이 '홍길동'인 학생은 총 5명
시력이 1.0 ~ 1.5인 홍길동 학생은 총 2명일때 이름순으로만 정렬한 명부 vs 시력순으로만 정렬한 명부 중 무엇이 더 효과적일까? 
 → 이름순으로 정렬한 명부가 더 효과적 ! 이름순은 홍길동인 학생의 교실을 5번만 찾아가면 되고, 시력순은 총 50명의 학생의 교실을 찾아가야 하기 때문

 

인덱스 스캔 효율화 튜닝과 랜덤 액세스 최소화 튜닝 중 뭐가 더 중요할까?
: 랜덤 액세스 최소화 튜닝 !  레코드 하나를 읽기위해 하나의 블럭에 접근을 최소화 (성능에 미치는 영향이 더 크기 때문)
데이터베이스는 읽어야할 데이터량이 많고, 그 과정에서 수많은 디스크 I/O가 발생하기에 느리기에 SQL 튜닝은 랜덤 I/O최소화가 중요하다 → SQL 튜닝은 랜덤 I/O와의 전쟁이다.

 

🔎 인덱스의 구조

  • Index (색인) : 대용량 테이블에서 필요한 데이터만 빠르게 효율적으로 액세스하기 위해 사용하는 오브젝트
인덱스를 사용하는 이유 
- 인덱스 없이 데이터를 검색하면, 테이블을 처음부터 끝까지 모두 읽어야하지만,
인덱스를 사용하면 일부만 읽고 멈출 수 있음 → 범위스캔(Range Scan) 가능 !
( 범위스캔이 가능한 이유 : 인덱스가 정렬되어 있기 때문 )

그림1

 

B+Tree 이란?
- 인덱스를 이루고 있는 자료구조 중 하나이고,데이터가 정렬된 상태로 유지가 가능
- 나무(Tree)를 거꾸로 뒤집은 모양이라서 뿌리(Root)가 위쪽에 있고, 가지(Branch)를 거쳐 맨 아래 잎사귀(Leaf)가 있음
- DBMS에서 일반적으로 B+Tree 인덱스를 사용함(검색, 삭제, 삽입 등 여러 작업을 효율적으로 처리하면서 트리의 균형을 유지)

 

- B+Tree 인덱스의  'B'가 Balanced의 약자

Balanced : 어떤 값을 탐색하더라도 인덱스 루트에서 리프 블록에 도달하기 까지 읽는 블록 수가 같음을 의미

즉, Root 로부터 모든 Leaf 블록까지의 높이는 항상 같음.


  • Root와 Branch 블록에 있는 각 레코드는 하위 블록에 대한 주소값을 가짐
  • Key값은 하위 블록에 저장된 Key값의 범위를 나타냄
LMC(Leftmost Child)
- 가장 왼쪽 첫번째 레코드이며, 자식 노드 중 가장 왼쪽 끝에 위치한 블록을 가리킴
- Root와 Branch 블록에는 Key값을 갖지 않는 특별한 레코드
- LMC가 가리키는 주소로 찾아간 블록에는 Key값을 가진 첫번째 레코드보다 작거나 같은 레코드가 저장

 

그림2

  • Leaf 블록에 저장된 각 레코드는 Key값 순으로 정렬되어 있으며, 테이블 레코드를 가리키는 주소값(ROWID)을 가짐
  • 인덱스 Key값이 같으면 ROWID순으로 정렬됨
  • 인덱스를 스캔하는 이유는 검색 조건을 만족하는 소량의 데이터를 빠르게 찾고 거기서 ROWID를 얻기 위해

ROWID 관련 참고 : [http://www.gurubee.net/lecture/2927]

테이블 레코드를 찾을 수 있는 값
- ROWID  = 데이터 블록 주소(DBA  Data Block Address) + 로우 번호
- 데이터 블록 주소  = 데이터 파일 번호 + 블록 번호
- 블록 번호 : 데이터파일 내에서 부여한 상대적 순번
- 로우 번호 : 블록 내 순번 

 

🔎 인덱스 탐색 과정 ( 수직적 탐색, 수평적 탐색 )

  1. 인덱스 수식적 탐색
  • 인덱스 스캔 시작지점을 찾는 과정
  • 정렬된 인덱스 레코드 중 조건을 만족하는 레코드를 찾는 과정이 아닌 조건을 만족하는 첫번째 레코드를 찾는 과정
  • 인덱스 수직적 탐색은 Root 블록에서부터 시작 ( Root - Branch - Leaf )
  • Root를 포함해 Branch 블록에 저장된 각 인덱스 레코드는 하위 블록에 대한 주소값을 가지기 때문에 Root부터 시작이 가능 
  • 수식적 탐색 과정에 찾고자 하는 값보다 크거나 같은 값을 만나면, 바로 직전 레코드가 가리키는 하위 블록으로 이동 
 ex) [그림2]에서 '이재희' 찾기 
1. Root 블록에는 크거나 같은 값이 없음  → 맨 마지막 '서' 레코드가 가리키는 하위블록으로 이동 
2. Branch블록에는 '이재희'보다 큰 레코드 '정재우'를 찾음
3. 바로 직전 레코드 '이재룡'가 가리키는 하위 블록으로 이동하여 Leaf 블록에 도달하여 '이재희' 찾음 

 

※ 인덱스를 수식적으로 탐색 할때 Root 를 포함한 Branch 블록은  '푯말' 같은 역할을 함
조건을 만족하는 첫 번째 레코드가 목표 지점이며,  푯말을 만날때마다 어느쪽으로 가면 목표 레코드를 만날 수 있는지 확인하면서 이동하고,  푯말이 알려주는 대로 따라가다보면 조건을 만족하는 첫 번째 레코드를 만날 수 있다!

 

2. 인덱스 수평적 탐색

  • 인덱스에서 본격적으로 데이터를 찾는 과정
  • 수직적 탐색을 통해 스캔 시작점을 찾았으면, 찾고자 하는 데이터가 더이상 안나타날때까지 인덱스 Leaf 블록을 수평적으로 스캔 (수직 탐색으로 도달한 Leaf 노드에서 데이터를 한 방향으로 순차적으로 스캔)
  •  양방향 연결 리스트 구조(double lineked list) 이므로 좌 → 우  우 →  좌 수평적 탐색이 가능
인덱스를 수평적으로 탐색하는 이유
1. 조건적을 만족하는 데이터를 모두 찾기 위해
2. ROWID를 찾기 위해

 

※ 필요한 컬럼을 인덱스가 모두 갖고 있어 인덱스만 스캔하고 끝나는 경우도 있지만,

인덱스를 스캔하고 테이블도 ROWID를 통해 액세스함(필요한 컬럼을 인덱스가 모두 갖지 않는 경우가 있기 때문)

 

수직적 탐색은 Root에서 Leaf로 이동 , 특정 키나 범위를 찾기 위해 사용됨
수평적 탐색은 Leaf에서 옆으로 이동, 수직 탐색 후 Leaf에서 데이터의 조건을 충족하는 범위를 처리함

 

🔎 결합 인덱스 구조와 탐색

  • 두 개 이상 컬럼을 결합해서 인덱스를 만들 수 있음
결합 인덱스 생성 시 컬럼 배치 순서 
1. 인덱스를 성별 + 이름 순으로 구성한 경우 → 성별찾고, 이름 검사 
2. 인덱스를 이름 + 성별 순으로 구성한 경우  → 이름 찾고, 성별 검사
select 이름, 성별 
from 사원
where 성별 = '여자'
and 이름 = '유관순'

 

→ 선택도가 낮은 컬럼을 앞에 두고 결합인덱스를 생성해야 검사 횟수를 줄일 수 있어 성능에 유리?  ❌

  • 인덱스 선두 컬럼을 모두 "=" 조건으로 검색할 때는 어느 컬럼을 인덱스 앞쪽에 두든 블록 I/O 개수가 같으므로 성능도 같음
  • DBMS의 인덱스는 B*Tree 방식을 사용하고 있기 때문에 컬럼의 순서와 상관없이 일량은 일정함 → 수직적 탐색 장점

2. 인덱스 기본 사용법

🔎 인덱스를 사용한다는 것 

  • 선두컬럼을 가공하지 않아야 인덱스를 정상적으로 사용 가능 
  • "인덱스를 정상적으로 사용한다" = Index Range Scan 
    •  Index Range Scan  :  Leaf 블록 일부만 스캔, Leaf 블록에서 스캔 시작점을 찾고 거기서부터 스캔하다가 중간에 멈추는 것을 의미
더보기

※ 인덱스 컬럼을 가공하면 인덱스를 사용할 수 없나요?

결론 : 사용할수 있음! 단, 스캔 시작점을 찾을 수 없고, 멈출 수도 없어 리프 블록 전체를 스캔해야함

(일부가 아닌 전체를 스캔하는 Index Full Scan 방식)

 

🔎 인덱스를 Range Scan 할 수 없는 이유

인덱스 컬럼을 가공하면 인덱스를 정상적으로 사용(Range Scan) 할 수 없다.

 

 

● 인덱스 컬럼을 가공했을때 인덱스를 정상적으로 사용할 수 없는 이유 → 인덱스 스캔 시작점을 찾을 수 없기 때문

  • Index Range Scan(인덱스에서 일정 범위를 스캔)  →  일정 범위를 스캔하려면 시작시점과 끝지점이 있어야 함!
where 업체명 like '%대한%'
  • LIKE로 중간 값을 검색 할 때도 Index Range Scan을 사용할 수 없음 → '대한'로 시작하는 값은 특정 구간에 모여 있어 Range Scan이 가능하지만, '대한'을 포함하는 값은 전체 구간에 있기 때문에 Range Scan이 불가능
  • OR 조건 = IN () 으로 검색할 때도 Range Scan 방식을 사용할 수 없기 때문에 UNION ALL을 사용해서 개별적으로 조회하면 Range Scan이 가능
  • IN 조건절에 대해서는 SQL 옵티마이저가 IN-List Iterator 방식을 사용( IN-List 개수 만큼 Index Range Sacn을 반복 )
조건절에 인덱스 컬럼을 가공하면 인덱스를 정상적으로 사용할 수 없다.

 

※ 인덱스를 Range Scan 하기 위한 조건
가장 첫 번째 조건은 인덱스 선두 컬럼이 가공되지 않는 상태로 조건절에 있어야 함
다른 말로, 인덱스 선두 컬럼이 가공되지 않는 상태로 조건절에 있으면 인덱스 Range Scan은 무조건 가능
But, 인덱스를 Range Scan한다고 해서 항상 성능이 좋은 건 아님!

 


⚙️ 지금까지의 정리
1. 인덱스는 데이터가 정렬되어 있기 때문에  Range Scan(범위 스캔)이 가능  
2. 조건절에 인덱스를 가공하면 인덱스를 정상적으로 사용 할 수 없음
3. 인덱스 컬럼을 가공해도 인덱스를 사용할 순 있지만, 찾고자 하는 데이터가 전체에 흩어져 있기 때문에 Range Scan이 불가능하거나 비효율이 발생

 


🔎 인덱스를 이용한 소트 연산 생략

  • 인덱스는 Range Scan이 가능하기 때문에, 소트연산 생략 효과도 부수적으로 얻을 수 있음
  • (오름차순) ASC정렬일 때는 조건을 만족하는 가장 작은 값을 찾아 좌측으로 수직적으로 탐색 후 우측으로 수평 탐색 진행
  • (내림차순) DESC정렬일 때는 조건을 만족하는 가장 큰 값을 찾아 우측으로 수직적 탐색한 후 좌측으로 탐색을 진행

🔎 Order By 절에서 컬럼 가공

  • 조건절이 아닌 ORDER BY 또는 SELECT-LIST에서 컬럼을 가공해서 인덱스를 사용할 수 없는 경우도 존재
  • ORDER BY 변경일자 || 변경순번으로 컬럼을 가공한다면 정렬 연산 생략을 할 수 없음
select * 
from 상태변경이력
where 장비번호 = '1
order by 변경일자::변경순번

 

  • SELECT문에서 가공한 컬럼을 ALIAS로 지정 후 ORDER BY에서 ALIAS로 사용하는 경우
SELECT *
FROM (
	SELECT TO_CHAR(A.주문번호, 'FM000000') AS 주문번호, A.업체번호
    FROM 주문 A
    WEHRE A.주문일자 = :dt
    AND   A.주문번호 > NVL(:next_ord_no, 0)
    ORDER BY A.주문번호
)
WHERE ROWNUM <= 30

 

🔎 SELECT-LIST 에서 컬럼 가공 

  • 최소값: 수직적 탐색을 통해 조건을 만족하는 가장 왼쪽 지점으로 내려가서 첫 번째 읽는 레코드 (정렬 연산 수행하지 않음)
  • 최대값 : 수직적 탐색을 통해 오른쪽 지점으로 내려가서 첫 번째 읽는 레코드

 

🔎 테이블 자동 형변환

  • 자동 형변환 기능에 의존하지 말고, 인덱스 컬럼 기준으로 반대편 컬럼 또는 값을 정확히 형변화해주는 것을 추천

3. 인덱스 확장기능 사용법

🔎 Index Range Scan

  • B+Tree인덱스의 가장 일반적이고 정상적인 형태의 액세스 방식
  • 인덱스 루트에서 Leaf 블록까지 수직적으로 탐색한 후에 필요한 범위만 스캔
  • 인덱스를  Range Scan 하려면 선두 컬럼을 가공하지 않는 상태로 조건절에 사용해야하고, 반대로, 선두 컬럼을 가공하지 않는 상태로 조건절에 사용하면 Index Range Scan은 무조건 사용 가능함
  • 성능은 인덱스 스캔 범위, 테이블 액세스 횟수를 얼마나 줄일 수 있냐로 결정됨

🔎 Index Full Scan

  • 수직적 탐색 없이 인덱스 Leaf 블록을 처음부터 끝까지 수평적으로 탐색하는 방식
  • 데이터 검색을 위한 최적의 인덱스가 없을때 차선으로 선택됨

🔎 Index Full Scan의 효용성

  • 인덱스 선두 컬럼이 조건절에 없으면 옵티마이저는 먼저 Table Full Sacn을 고려함.
- 인덱스를 Range Scan 할 수 없을 때, 테이블 전체를 스캔하는 것보다 인덱스 전체를 스캔하면 어떨까?
: 인덱스 스캔 단계에서 대부분 레코드를 필터링하고, 아주 일부만 테이블을 액세스하는 상황이라면, 면적이 큰 테이블보다 인덱스를 스캔하는 쪽이 유리하다. 이때, 옵티마이저는 Index Full Scan 방식을 선택함
//sal > 900인 사원이 전체 중 극히 일부라면 Table Full Scan 보다 Index Full Scan이 효과적 ! 
select * 
from emp 
where sal > 9000 
order by ename;

 

 

🔎 인덱스를 이용한 소트연산 생략

  • 인덱스를 Full Scan하면 Range Scan과 동일하게 결과집합이 인덱스 컬럼순으로 정렬되므로, Sort Order By 연산을 생략할 목적으로 사용가능 

ex) SAL > 1000인 사원을 찾을때

select /*+ first_rows */ *
from emp 
where sal > 1000
order by ename;

Execution Plan
----------------------------------------------
0     SELECT STATEMENT Optimizer=HINT: FIRST_ROWS 
1   0   TABLE ACCESS (BY INDEX ROWID) OF 'EMP' (TABLE)
2   1     INDEX (FULL SCAN) OF 'EMP_ENAME_SAL_IDX' (INDEX)

 

 > 소트 연산을 생략함으로써 전체 집합 중 빠르게 출력할 목적으로 옵티마이저가 Index Full Scan 방식을 선택한 것

 

🔎 Index Unique Scan

  • 수직적 탐색으로만 데이터를 찾는 스캔방식
  • Unique인덱스를 '=' 조건으로 탐색하는 경우에 작동
  • Unique 인덱스는 범위검색 조건(between, 부등호, like)인 경우 Index Range Scan으로 처리됨

🔎 Index Skip Scan

  • Root 또는 Branch 블록에서 읽은 컬럼 값 정보를 이용해 조건절에 부합하는 레코드를 포함할 '가능성이 있는' Leaf 블록만 골라서 액세스하는 스캔 방식 (선두 컬럼이 조건절이 없는 경우에도 인덱스를 활용하는 방법)
  • Distinct Value(고유값) 개수가 적은 선두 컬럼이 조건절에 없고, 후행 컬럼의 Distinct Value 개수가 많을 때 효과적! 

 

※ Index Skip Scan이 작동하기 위한 조건

더보기

1. 결합 인덱스가 존재→ Index Skip Scan은 복합 인덱스에서만 작동
2. 선두 컬럼에 조건이 없거나 일부 값만 사용할 때
3. 선두컬럼이 유일한 값 또는 상대적으로 적은  Distinct Value값을 가질때 
4 . 후속 컬럼에 조건이 있을때
5. 테이블의 다른 실행계획보다 Index Skip Scan이 더 효율적일때

 

🔎 Index Fast Full Scan

Index Full Scan은 1→2→3→4....→10

 

Index Fast Full Scan은 1→2→10→3....→6

  • Index Full Scan 보다 빠름
  • 논리적인 인덱스 트리 구조를 무시하고 인덱스 세그먼트 전체를 Multiblock I/O 방식
  • Multiblock I/O 방식을 사용하므로 디스크로부터 대량의 인덱스 블록을 읽어야 할 때 큰 효과냄
  • 속도는 빠르나, 인덱스 Leaf 노드가 갖는 연결리스크 구조를 무시한 채 데이터를 읽기 때문에 결과집합이 인덱스 Key 순서대로 정렬되지 않음
  • 쿼리에 사용한 컬럼이 모두 인덱스에 포함돼 있을 때만 사용가능
  • Index Range Scan 또는 Index Full Scan과 다르게, 인덱스가 파티션 되어 있지 않더라도 병렬 쿼리가 가능
  • 병렬 쿼리 시에는 Direct Path I/O 방식을 사용하기 때문에 I/O 속도가 더 빨라짐
Index Full Scan Index Fast Full Scan
1.인덱스 구조를 따라 스캔
2.결과집합 순서 보장
3.Single Block I/O
4.(파티션 돼 있지 않다면) 병렬스캔 불가
5.인덱스에 포함되지 않은 컬럼 조회 시에도 사용 가능
1.세그먼트 전체를 스캔
2.결과집합 순서 보장 안 됨
3.Mutiblock I/O
4.병렬스캔 가능
5.인덱스에 포함된 컬럼으로만 조회할 때 사용 가능

 

 

🔎 Index Range Scan Descending

 

 

 

  • Index Range Scan과 기본적으로 동일한 스캔 방식
  • Index Range Scan과의 차이점: 인덱스를 뒤 → 앞으로 스캔하기 때문에 내림차순으로 정렬된 결과집합을 얻음

 

 

'Study > 친절한 SQL 튜닝' 카테고리의 다른 글

[DB] 5장. 소트 튜닝  (0) 2025.03.17
[DB] 4장. NL조인  (0) 2025.02.25
[DB] 3장. 인덱스 튜닝(2)  (0) 2025.01.31
[DB] 3장. 인덱스 튜닝(1)  (0) 2025.01.31
[DB] 1장.SQL 처리과정과 I/O  (0) 2025.01.25
'Study/친절한 SQL 튜닝' 카테고리의 다른 글
  • [DB] 4장. NL조인
  • [DB] 3장. 인덱스 튜닝(2)
  • [DB] 3장. 인덱스 튜닝(1)
  • [DB] 1장.SQL 처리과정과 I/O
happy_dev
happy_dev
복사하고 붙여넣기 잘하고 싶어요
  • happy_dev
    happy의 개발일지
    happy_dev
  • 전체
    오늘
    어제
    • 전체 (43)
      • Java (0)
      • React (1)
      • DB (1)
      • Study (41)
        • 친절한 SQL 튜닝 (9)
        • 모던 리액트 Deep Dive (18)
        • 젠킨스로 배우는 CI,CD 파이프라인 구축 (14)
        • Studyd (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    인덱스의기본
    DB
    oracle
    젠킨스
    toad
    SQL
    Jenkins
    리액트
    조인
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
happy_dev
[DB] 2장. 인덱스 기본
상단으로

티스토리툴바