인덱스란?
인덱스는 데이터베이스 테이블에 대한 검색 성능의 속도를 높여주는 자료 구조입니다.
특정 컬럼에 인덱스를 생성하면 해당 컬럼의 데이터들을 정렬하여 별도의 메모리 공간에 데이터의 물리적 주소와 함께 메모리 저장소에 저장됩니다. 원본 테이블과의 비교를 위해 인덱싱되어 메모리 저장소에 저장된 데이터들을 인덱스 테이블이라고 하겠습니다.
인덱스 생성 컬럼을 Where 조건으로 조건을 거는 등의 작업을 하면 생성된 인덱스 테이블에 대해서 우선적으로 검색하게 됩니다. 해당하는 데이터를 찾았다면 데이터의 물리적 주소를 기반으로 원본 테이블에 접근하여 데이터를 가져오는 식으로 동작합니다.
인덱스는 보통 책에서의 목차라고 생각하면 됩니다. 우리가 책에서 정보를 빠르게 찾을 때도 원하는 카테고리를 목차에서 찾은 다음 페이지 번호를 찾아가듯이 인덱스도 원하는 데이터를 먼저 찾은 다음 물리적 주소로 찾아갑니다.
인덱스를 사용하는 이유
인덱스의 가장 큰 특징은 데이터들이 정렬이 되어있다는 점입니다. 해당 특징으로 인해서 검색에 대하여 성능 향상을 꾀할 수 있습니다.
테이블을 생성할 때 기본적으로 primary key 또는 unique 컬럼에 대해 자동으로 인덱스가 생성됩니다.
Where 절의 효율성
테이블의 레코드는 내부적으로 순서 없이 저장이 됩니다. Where 절에 조건을 걸어서 검색할 때에 인덱스를 사용하지 않는다면 테이블의 전체 데이터를 스캔하는 Full Scan 방식을 사용합니다. 하지만 인덱스를 사용하게 되면 정렬된 인덱스 테이블에 대해서 검색을 진행하니 데이터를 빠르게 찾을 수 있습니다.
Order by 절의 효율성
Order by를 사용하게 되면 사용할 때 마다 원본 테이블에 대해서 정렬하는 작업을 필요로 하게 됩니다.
MIN, MAX의 효율적인 처리
이것 또한 정렬된 데이터에 대해 얻을 수 있는 장점입니다. 인덱스를 사용하지 않으면 Full Scan 방식으로 최솟값과 최댓값을 찾아내지만 인덱스를 사용하면 인덱스 테이블의 첫번째 값과 마지막 값을 찾으면 그것이 최솟값과 최댓값이 됩니다.
인덱스의 단점
인덱스의 가장 큰 문제점은 정렬된 상태를 계속 유지시켜줘야 한다는 점입니다. 원본 테이블에 검색이 아닌 삽입/수정/삭제 연산시 인덱스 테이블은 내부적으로 재정렬하기 위한 연산을 필요로 합니다.
SELECT를 제외한 DML이 일어났을 때 인덱스 테이블의 상황은 아래와 같습니다.
부하를 낮추기 위해 DELETE 연산 시에는 삭제가 아닌 '사용 안함' 표시만 합니다.
- INSERT : 새로운 데이터에 대한 정렬 진행 후 저장하는 작업 수행
- UPDATE : 인덱스 테이블은 UPDATE가 없기 때문에 DELETE+INSERT(정렬) 작업 수행
- DELETE : 해당 데이터의 인덱스를 사용 안함으로 표시하는 작업 수행
그리고 인덱스는 저장공간의 10%에 해당하는 공간이 추가로 필요하기에 이 부분도 고려를 해야합니다.
즉, 사용하면 좋은 경우와 피해야하는 경우를 정리해보면 아래와 같습니다.
사용하면 좋은 경우
- Where 절에서 자주 사용되는 컬럼
- 외래키가 사용되는 컬럼
- Join에 자주 사용되는 컬럼
사용을 피해야 하는 경우
- Data 카디널리티가 낮은 컬럼
- DML이 자주 일어나는 컬럼
카디널리티에 대한 개념을 설명하겠습니다.
카디널리티(Cardinality)는 특정 데이터 집합의 유니크(Unique)한 값의 갯수입니다.
예를 들어 데이터베이스에 '성별' 컬럼의 경우 남자와 여자라는 값을 갖습니다. 이 경우 카디널리티는 2가 됩니다.
카디널리티가 낮은 경우의 대표적인 속성들로는 성별, 부서, 지역 등이 있습니다.
카디널리티가 높은 경우의 대표적인 속성들로는 주민번호, 사원번호 등이 있습니다.
데이터베이스 인덱스가 사용하는 자료구조
그렇다면 인덱스는 어떠한 자료구조를 사용하고 있을까요?
DB의 인덱스는 B-Tree 라는 자료구조를 이용하여 테이블의 요소를 빠르게 탐색하도록 설계되어있습니다.
B-Tree는 Balaned Tree의 하나의 유형으로 그 외에 RedBlack-Tree가 있습니다.
일반적인 트리는 최악의 경우 트리 노드의 요소가 한쪽 방향으로 쏠려있는 형태를 취하게 되는데, Balanced Tree는 이러한 단점을 보완하여 자식들의 균형(Balance)을 유지시켜줍니다.
그리고 하나의 노드는 여러 개의 키를 가질 수 있으며 여러 개의 자식노드를 가질 수 있습니다.
위의 메모리 저장소 영역을 B-Tree 구조로 볼 수 있습니다.
- 인덱스 탐색은 Root -> Branch -> Leaf -> 디스크 저장소 순으로 진행됩니다.
- 예를 들어 Branch (페이지 번호 2) 는 dept_no가 d001이면서 emp_no가 10017 ~ 10024까지인 Leaf의 부모로 있습니다.
- 즉, dept_no=d001 and emp_no=10018로 조회하면 페이지 번호 4인 Leaf를 찾아 데이터파일의 주소를 불러와 반환하는 과정을 하게 됩니다.
- 인덱스의 두번째 컬럼은 첫 번째 컬럼에 의존해서 정렬되어 있습니다.
- 즉, 두번째 컬럼의 정렬은 첫번째 컬럼이 똑같은 열에서만 의미가 있습니다.
- 만약 3번째, 4번째 인덱스 컬럼도 있다면 두번째 컬럼과 마찬가지로 3번째 컬럼은 2번째 컬럼에 의존하고, 4번째 컬럼은 3번째 컬럼에 의존하는 관계가 됩니다.
- 디스크에서 읽는 것은 메모리에서 읽는것보다 성능이 훨씬 떨어집니다.
- 결국 인덱스 성능을 향상시킨다는 것은 디스크 저장소에 얼마나 덜 접근하게 만드느냐, 인덱스 Root에서 Leaf까지 오고가는 횟수를 얼마나 줄이느냐에 달려있습니다.
참고
https://coding-factory.tistory.com/746
https://jojoldu.tistory.com/243
https://helloinyong.tistory.com/296
https://ssup2.github.io/theory_analysis/B_Tree_B+_Tree/