인덱스
인덱스는 데이터를 빠르게 찾을 수 있는 하나의 장치이다.
책을 예로들면,
책의 본문이 있고, 그 본문 안에 내가 찾고자 하는 '항목'을 찾아보기를 통해 빠르게 찾을 수 있는 것처럼 말이다.
이와 마찬가지로, 인덱스를 설정하면, 테이블 안에 내가 찾고자 하는 데이터를 빠르게 찾을 수 있다.
B-트리
인덱스는 보통 'B-트리'라는 자료 구조로 되어있다. 루트 노드, 리프 노드, 그리고 루트 노드와 리프 노드 사이에 있는 브랜치 노드로 나뉜다.
루트 노드와 리프 노드를 기반으로 설명하면 아래 그림처럼 설명할 수 있다.
위와 같은 상황에서 E를 찾는다고 할 때, 전체 테이블을 탐색하는 것이 아니라 E가 있을 법한 리프 노드로 들어가서 E를 탐색하면 좀 더 쉽게 찾을 수 있을 것이다. 만약, B-트리 자료구조 없이 탐색하고자 하면 A,B,C,D,E, 5번의 탐색을 해야 하지만, 위처럼 나누면 2번 만에 리프 노드에서 찾을 수 있다.
조금 더 자세하게 예시를 들어보면, 몸무게 57에 해당하는 데이터를 검색해야 한다고 했을 때, B-트리가 아래와 같다면,
트리 탐색은 맨 위 루트 노드부터 탐색이 일어나며, 브랜치 노드를 거쳐 리프 노드까지 내려온다. '57보다 같거나 클 때까지 <='라는 것을 기반으로 처음 루트 노드에서 39,83 이후, 아래 노드로 내려와 46,53,57 등 정렬된 값을 기반으로 탐색하는 것을 볼 수 있다. 이렇게 루트 노드부터 시작해 마지막 리프 노드에 도달해 57이 가리키는 데이터 포인터를 통해 결과값을 반환하게 되는 것이다.
인덱스가 효율적인 이유는 효율적인 단계를 거쳐 모든 요소에 접근할 수 있는 균형 잡힌 트리 구조와 트리 깊이의 대수확장성 때문이다.
'대수확장성'이란, 트리 깊이가 리프 노드 수에 비해 매우 느리게 성장하는 것을 뜻한다. 기본적으로 인덱스가 한 깊이씩 증가할 때마다 최대 인덱스 항목의 수는 4배씩 증가한다.
인덱스 만드는 방법
인덱스를 만드는 방법은 데이터베이스마다 다른데, MySQL와 MongoDB를 기준으로 아래에 설명하겠다.
MySQL의 경우, '클러스터형 인덱스'와 '세컨더리 인덱스'가 있다. '클러스터형 인덱스'는, 테이블 당 하나를 설정할 수 있다. primary key 옵션으로 기본키로 만들면 클러스터형 인덱스를 생성할 수 있고, 기본키로 만들지 않고 unique not null 옵션을 붙이면 클러스터형 인덱스로 만들 수 있다.
create index...~ 명령어를 기반으로 만들면 세컨더리 인덱스를 만들 수 있다.
하나의 인덱스만 생성할 것이라면 클러스터형 인덱스를 만드는 것이 세컨더리 인덱스를 만드는 것보다 성능이 좋다.
'세컨더리 인덱스'는 보조 인덱스로 여러 개의 필드 값을 기반으로 쿼리를 많이 보낼 때 생성해야 하는 인덱스이다. age라는 하나의 필드만 쿼리를 보낸다면 클러스터형 인덱스만 필요할 것이다. 하지만 age, name, address 등과 같은 다양한 필드를 기반으로 쿼리를 보낼 때는 세컨더리 인덱스를 사용해야 한다.
MongoDB의 경우, 도큐먼트를 만들면 자동으로 ObjectID가 형성되고, 해당 키가 기본키로 설정된다. 세컨더리키도 부가적으로 설정해서 기본키와 세컨더리키를 같이 쓰는 '복합 인덱스'를 설정할 수도 있다.
인덱스 최적화 기법
인덱스 최적화 기법은 데이터베이스마다 조금씩 다르긴 하지만 기본적인 골조는 똑같다. 따라서 이번 글에서는 MongoDB를 기반으로 설명해보겠다.
인덱스는 2번 탐색하도록 강요한다. 인덱스 리스트, 컬렉션 순으로 탐색하기 때문에 읽기 비용이 든다.
또한, 컬렉션이 수정되었을 때 인덱스도 수정되어야 한다. 책의 내용이 수정되면 목차나 찾아보기도 수정해야하는 것과 비슷한 맥락이다. 이때 B-트리의 높이를 균형 있게 조절하는 비용도 들고, 데이터를 효율적으로 조회할 수 있도록 분산시키는 비용도 들게 된다.
따라서 쿼리에 있는 필드에 인덱스를 무작정 다 설정하는 것은 답이 아니다. 또한, 컬렉션에서 가져와야 하는 양이 많을수록 인덱스를 사용하는 것은 비효율적이다.
인덱스 최적화 기법은 서비스 특징에 따라 달라진다. 서비스에서 사용하는 객체의 깊이, 테이블의 양 등이 다르기 때문이다. 그렇기에 항상 테스팅하는 것이 중요하다.
explain() 함수를 통해 인덱스를 만들고, 쿼리를 보내내 이후 테스팅을 하며 걸리는 시간을 최소화해야 한다.
아래는 MySQL 테스팅 수행 예시 코드로 참고할 수 있을 것이다.
EXPLAIN
SELECT * FROM c1
JOIN c2 ON c1.a1 = c2.a1
보통 여러 필드를 기반으로 조회를 할 때 '복합 인덱스'를 생성하는데, 이럴 때, 인덱스를 생성할 때 순서가 있고, 생성 순서에 따라 인덱스 성능이 달라진다.
같음 -> 정렬 -> 다중 값 -> 카디널리티 순으로 생성해야 한다.
1) 어떠한 값과 같음을 비교하는 ==이나 equal이라는 쿼리가 있다면 제일 먼저 인덱스로 설정한다.
2) 정렬에 쓰는 필드라면 그다음 인덱스로 설정한다.
3) 다중 값을 출력해야 하는 필드, 즉 쿼리 자체가 >이거나 <등, 많은 값을 출력해야 하는 쿼리에 쓰는 필드라면 나중에 인덱스를 설정한다.
4) 유니크한 값의 정도를 카니럴리티라고 한다. 카디널리티가 높은 순서를 기반으로 인덱스를 생성해야 한다. 만약 age, email이 있다고 했을 때, 카디널리티는 email이 높을 것이다. 즉, email이라는 필드에 대한 인덱스를 먼저 생성해야 한다.