Search

데이터베이스 index와 구조

데이터베이스 성능 최적화에 있어 인덱스(Index)는 중요한 개념이다. 인덱스를 사용하면 데이터를 빠르게 조회할 수 있어 효율적인 쿼리 실행이 가능하다. 이번 글에서는 인덱스가 무엇이며 어떻게 동작하는지, 인덱스를 사용할 때 고려해야 할 것이 무엇인지 알아보겠다.

인덱스란?

데이터베이스의 값을 미리 정렬해 두는 데이터 구조
조건을 만족하는 튜플을 빠르게 조회할 수 있음
책의 목차처럼 데이터베이스도 인덱스를 사용해 특정 값이 어디에 있는지 찾아갈 수 있게 된다. 인덱스가 없다면 특정 조건을 만족하는 행을 찾기 위해 테이블 전체를 스캔해야해서 데이터가 많을수록 시간이 오래 걸리게 된다. 인덱스가 있다면 이를 먼저 스캔해 구조적으로 더 빠르게 값을 찾아갈 수 있게 된다.

인덱스의 생성과 사용

인덱스는 컬럼에 생성할 수 있으며, 다중 컬럼에도 생성이 가능하다. 특정 컬럼에 인덱스를 생성하면 데이터베이스는 해당 컬럼의 값을 미리 정렬해놓은 사본을 만든다. 이 사본은 실제 데이터 테이블과 별도로 저장되며 검색시 빠르게 접근 할 수 있는 트리 구조나 해시 테이블 형태로 구성된다.
쿼리가 실행되면, 데이터베이스의 Optimizer가 쿼리를 분석하고 가장 효율적인 방식으로 데이터를 조회할 방법을 결정한다. Optimizer가 조건에 따라 인덱스를 사용하는게 더 빠르다고 판단하면 인덱스를 먼저 조회해 값을 찾는다. 하지만 조건에 해당하는 값이 너무 많거나 인덱스를 사용하는 것이 효율적이지 못하다고 판단할 경우, 테이블 풀스캔을 하게 된다.
다중 컬럼 인덱스
다중 컬럼의 경우 컬럼의 순서에 따라 정렬이 이루어지기 때문에 컬럼의 순서가 중요하다. 예를들어 (A, B) 순으로 인덱스를 생성하면 A가 먼저 정렬되고 그 후 B가 정렬된다.
CREATE INDEX idx_example ON employees (department_id, salary);
위와 같은 인덱스를 생성할 경우, department_id 정렬 후 salary 정렬. 따라서 WHERE department_id = 10 AND salary > 5000 과 같은 쿼리는 인덱스를 효과적으로 사용할 수 있지만, WHERE salary > 5000 만 사용하는 쿼리는 이 인덱스를 효과적으로 활용할 수 없다. 이는 인덱스가 다중 컬럼 인덱스에서 첫 번째 컬럼을 기반으로 데이터를 정렬하기 때문이다.

인덱스의 종류

다양한 구조를 사용해 인덱싱을 하며 각각 특징에 따라 적합한 용도가 있다.

B-tree

데이터베이스 인덱싱에서 자주 사용되리 구조로 균형잡힌 트리 형태로 데이터를 저장한다.

특징

모든 노드에 키와 값을 저장하며 여러개의 키를 저장할 수 있다
내부 노드는 자식 노드로의 포인터를 포함
루트 노드에서 리프 노드까지의 경로가 동일
B-tree의 핵심은 균형을 유지하는 것인데, 이는 삽입, 삭제, 수정이 발생해도 트리의 높이가 일정하게 유지된다는 뜻이다. 노드에 저장된 값이 너무 많다면 노드를 분할(가운데 키를 부모로 올림)하고, 저장된 값이 너무 적다면 노드를 병합한다. 이를 통해 B-tree는 모든경로가 동일한 길이를 유지하게 되어 효율적인 검색을 보장한다.
예를들어 3개 값을 저장 할 수 있는 노드가 4개 값을 가지게 되면 가운데 키인 20을 부모 노드로 올리고, 왼쪽 노드에는 10, 오른쪽 노드에는 30, 40을 넣는다.

B+tree

B+tree는 B-tree에서 추가적으로 성능 최적화를 적용한 형태이다.

특징

리프 노드에만 데이터를 저장
내부 노드는 키와 포인터만 저장
리프 노드가 연결리스트 형태로 연결
실제 값은 리프 노드에만 있고 내부 노드에는 키만 있기 때문에 메모리 공간을 효율적으로 쓸 수 있고, 내부 노드에 더 많은 키를 포함 할 수 있어 결과적으로 트리가 낮아져 검색 속도가 빨라진다. 또한 리프 노드간에 포인터로 연결되어 있어 순차적인 데이터 검색(범위검색)에 매우 효율적이다.

해시 인덱스

해시 함수를 사용해 해시 값을 계산해 그 값을 기준으로 데이터를 저장한다. 해시 값을 이용하기 때문에 특정 값에 대한 검색에서 매우 빠르지만, 해시 값에는 순서가 존재하지 않아 범위 검색(>, <, between 등)이 불가능하다.

비트맵 인덱스

데이터 값이 고정되거나 범위가 작을경우 효율적인 인덱싱 방식이다. 비트 배열을 사용해 데이터를 압축된 형태로 저장하고, 여러 비트맵을 조합해 특정 조건을 만족하는 데이터를 찾는다. 데이터 값을 비트로 저장하기 때문에 저장공간을 효율적으로 사용할 수 있으나, 범위 검색이 어렵고 대규모 데이터 처리에는 적합하지 않다.

인덱스 사용시 고려사항

인덱스는 읽기 성능을 크게 향상 시키지만 쓰기 작업에서는 성능저하를 일으킬 수 있고, 관리하는데에 추가적인 비용이 발생한다.
새로운 행을 추가, 업데이트, 삭제 할 때 인덱스가 재정렬되기 때문에 성능저하 발생
매번 즉시 재정렬을 하는게 효율적이지 않기 때문에 변경사항을 한 번에 모아서처리하는 메커니즘을 적용하기도 한다
테이블이 커질수록 인덱스 크기도 같이 커져 더 많은 저장 공간이 필요 > 스토리지 비용 증가
카디널리티(Cardinality) 고려. 컬럼의 고유값이 많을수록 인덱스 효과가 크고, 반대로 고유값이 적다면 인덱스 효과가 크지 않음.
예를 들어 값이 두개 밖에 없는 성별 같은 컬럼에 인덱스를 달면, 결국 테이블의 절반 이상을 읽어야 할 수도 있어 테이블 풀스캔과 비슷한 성능을 내게 된다.
+ 알아두면 좋을 내용 UUIDv4와 B-tree
UUIDv4와 같은 랜덤한 값을 B-tree나 B+tree 인덱스에서 PK로 사용하는 것은 비효율적일 수 있다. increament key를 사용해야 범위 검색등 효과가 좋다.

정리

인덱스는 특정 값을 기준으로 데이터를 정렬해놓고 이를 통해 빠른 조회가 가능하도록 하는 데이터 구조이다. 데이터베이스의 성능을 크게 향상 시킬 수 있지만, 관리 비용이 들기 때문에 무조건 모든 컬럼에 인덱스를 다는 것은 좋지 않다. 쿼리에서 자주 사용하는 조건이나 카디널리티, 데이터 구조 등을 고려해 인덱스를 적절하게 생성하는 것이 중요하다.