Problem
백만건이 넘는 서비스 테이블에 LIKE 연산자를 사용하여 검색하면, 너무나도 늦게 결과값이 나옵니다. 아무리 인내심이 많은 사용자라도 기다릴 수 없는 시간이었습니다. INDEX(인덱스)는 데이터 저장, 수정, 삭제에 대한 성능을 희생시켜 탐색에 대한 성능을 대폭 상승시키는 효과가 있습니다. LIKE 연산자를 사용하는 컬럼에 INDEX를 적용하면, 검색 속도가 빨라 질 것을 기대했지만 전혀 성능이 좋아지지 않았습니다.
INDEX에 조사하던 중 아무 타입 지정 없이 INDEX를 적용하면 B-Tree 타입으로 색인을 하는 것을 알았습니다. B-Tree 타입은 인덱스를 적용하는 컬럼의 값을 변형하지 않고 원래의 값을 이용합니다. 따라서 = 연산과 같은 값 자체에 대한 탐색 (single value search)에는 효과적이지만 %LIKE% 연산과 같이 검색어가 데이터 값에 포함 되었는지 여부 (testing the presence of an item)를 확인하는 것에는 적용되기 어렵습니다.
반면, GIN (Generalized Inverted Index) 인덱스는 인덱스를 적용하는 컬럼의 값을 일정한 규칙에 따라 쪼개고(split), 이렇게 쪼갠 요소들을 사용합니다. 이에 따라 포함 여부를 확인하는 경우 보다 효과적으로 동작할 수 있습니다.
B-Tree 인덱스
B-Tree 인덱스는 데이터베이스의 인덱싱 알고리즘 가운데 가장 일반적으로 사용되고, 또한 가장 먼저 도입된 알고리즘이입니다. B-Tree는 Balanced Tree의 줄임말로 트리의 양쪽이 거의 같은 양의 데이터를 가지도록 인덱싱을 하는 것입니다. 그리고 컬럼의 값을 변형하지 않고, 원래의 값을 이용해 인덱싱하는 알고리즘입니다. B-Tree는 많은 양의 데이터를 읽고 써야하는 경우 용이하게 사용됩니다.
다음은 정수 키가 있는 한 필드에 대한 인덱스의 단순화된 예입니다.
예를 들어 49번 key의 값을 찾는다고 한다면, 루트 노드에서 하위 노드 중 어떤 노드로 내려가야 하는지 결정해야 합니다. 루트 노드(4, 32, 64)의 키를 알고 있으므로 자식 노드의 값 범위를 파악합니다. 32 ≤ 49 < 64이므로 두 번째 자식 노드로 내려가야 합니다. 다음으로 필요한 TID를 얻을 수 있는 리프 노드에 도달할 때까지 동일한 프로세스를 재귀적으로 반복합니다.
이렇게 순차적으로 비교하면서 찾아가는 구조이므로, LIKE ‘%’ 연산과 같이 데이터 중간에 속한 값에 대한 인덱스 적용이 힘든 부분을 보입니다.
GIN 인덱스
GIN는 Generalized Inverted Index의 약자로, 기본적으로 Full Text Search를 위해 만들어진 인덱스입니다. 데이터 내의 각 요소, 즉 문장이라면 각 단어, 비정형 데이터라면 각 요소들을 인덱싱하여 키워드의 등장 위치와 상관 없이 해당 키워드를 사용하여 찾고자 하는 값이 데이터의 중간에 등장하더라도 인덱스를 적용할 수 있습니다.
다음은 GIN 인덱스의 구조를 추상화 한 예시입니다.
INDEX 생성 방법
CREATE INDEX trgm_idx_name ON table_name USING GIN (column_name gin_trgm_ops);
주의점
1. 다중 열 인덱싱은 지원되지 않습니다.
2. NULL 값을 인덱싱하지 않습니다.
3. 동등성이나 간단한 비교 연산자를 지원하지 않으므로 일반 B-tree 인덱스도 필요할 수 있습니다.
4. GIN의 경우 B-Tree 에 비해 인덱싱 속도가 좀 더 느리고 디스크 용량도 더 차지하게 됩니다.