Search

인덱스

생성일
2024/10/26 08:58
태그

인덱스란

데이터베이스의 인덱스(Index)는 추가적인 메모리와 쓰기 작업을 수행해 데이터베이스 테이블에 저장된 데이터 검색 속도를 향상시키기 위한 자료구조를 말한다.
위 그림처럼 인덱스는 데이터베이스 내의 특정 컬럼(열)이나 컬럼들의 조합(복합 인덱스)으로 해당 값이 저장된 레코드(행)의 위치를 매핑하여 데이터 베이스 쿼리의 성능을 최적화한다.
인덱스의 장점
데이터베이스에서 검색 쿼리에 인덱스를 사용하면, 테이블 전체를 스캔하여 레코드를 찾는 것이 아니라 검색이 빠른 자료 구조의 인덱스를 통해 빠르게 레코드 위치를 찾아내 성능의 향상이 크게 이루어진다.
인덱스의 단점
하지만 데이터를 추가적인 메모리에 정렬해 저장해둬야하고, 정렬 상태를 유지해야하기 때문에 데이터의 삽입이나 삭제, 수정이 이루어지면 인덱스도 수정이 필요해 성능이 떨어진다는 단점이 있다. 이러한 이유로 데이터의 삽입이나 삭제, 수정보다 조회가 훨씬 자주 발생하는 애플리케이션에서 효율적이다.
또한 인덱스 검색을 수행하기 위해, 주기적으로 인덱스를 포함하여 통계 정보를 업데이트 해주어야 하기 때문에 이로 인한 오버헤드가 발생한다.

인덱스 자료구조

인덱스는 특정 컬럼의 값과 해당 레코드(행)의 위치를 특정한 자료구조를 통해 저장한다. 이에 사용되는 자료 구조는 몇 가지가 있다.
해시 테이블(Hash Table) 인덱스
해시 테이블은 컬럼의 값과 물리적 주소를 key-value 쌍으로 저장하는 자료구조로, 해시 함수를 통해 컬럼의 위치를 저장하고 찾는다.
해시 테이블은 등호(=) 연산에 최적화 되어있고 데이터가 정렬되어있지 않아 범위 연산(>, >=, <, <=)에는 비효율적이고 부등호(<>) 연산에는 사용할 수 없다. 실제 DBMS에서 해시 인덱스 기능을 제공하기는 하지만, 이러한 이유로 인해 잘 사용되지는 않는다.
비트맵(BitMap) 인덱스
인덱스 컬럼의 데이터를 Bit 값인 0 또는 1로 변환하여 인덱스 키로 사용하는 방법으로, 카디널리티가 낮은 컬럼에서 효과적인 성능 향상 효과를 얻을 수 있다. 또한 BitMap을 사용하기 때문에 연산속도가 빠르고 저장 공간이 작다.
일반적으로 대량의 데이터를 한번에 처리하여 분석하거나 통계 정보를 출력할 때 많이 사용된다.
B-tree, B*tree, B+tree 인덱스
트리 구조를 통해 값을 저장하는 자료 구조로, 정렬된 균형 트리의 빠른 검색 속도로 인해 검색 조건에 효과적으로 대응할 수 있다.
B-tree
B-tree는 하나의 노드에 여러 값을 저장할 수 있고 두 개 이상의 자식을 가질 수 있는 트리 구조를 말한다. 하나의 노드에 여러 값을 저장하기 때문에, 이진 트리보다 훨씬 많은 데이터를 정렬을 유지하며 효율적으로 저장할 수 있다. 또한 균형 트리(balanced-tree)이기 때문에, 최악의 경우에도 O(logN)의 검색 성능을 보장한다.
B*tree
B-tree는 제약조건으로 M개의 노드가 있을 때, 자식의 개수가 최소 (⌈M/2⌉ - 1)개 이상 M개 이하여야 한다. B*tree는 이러한 제약조건을 최소 (M * 2/3)개 이상으로 늘리고 노드가 가득차면 분열하는 대신 이웃한 형제 노드로 재배치하여, 노드의 추가적인 생성과 연산을 최소화한 트리 구조이다.
B+tree
B-tree는 범위 탐색의 경우 각 값마다 노드를 별개로 찾아서 이동해야 한다는 단점을 가지고 있다. B+tree는 이러한 단점을 해소하기 위해, 모든 값들을 leaf 노드(최하위 노드)에만 저장하고 모든 leaf 노드를 Linked List 형태로 연결한 트리 구조이다.
Oracle에서는 인덱스 자료구조로 B*tree를 사용하며, MySQL은 B-tree와 B+tree를 사용한다.

인덱스 종류

클러스터 인덱스(Clustered Index, Primary Index)
데이터를 일정 기준으로 정렬하여 데이터 페이지로 저장하는 인덱스 방식이다. 인덱스 생성 시에 물리적으로 데이터 페이지 전체를 다시 정렬하고 쓰기 때문에, 대용량의 데이터를 인덱스로 생성하고자 하면 심각한 부하가 발생한다.
또한 정렬된 채로 저장하기 때문에, 한 테이블 당 하나씩 밖에 만들 수 없다. MySQL에서는 Primary Key가 있다면 클러스터 인덱스로 사용되고, 없다면 Unique 조건이면서 Not NULL인 컬럼이 클러스터 인덱스로 사용된다. 만약 이런 조건을 가진 컬럼마저도 없다면, 보이지 않는 임의의 컬럼을 만들어 클러스터 인덱스로 지정한다.
모든 실제 레코드는 클러스터 인덱스의 리프 노드에 저장된다. 데이터를 정렬하여 저장하기 때문에, 저장 공간을 적게 사용하고 보조 인덱스에 비해 검색 속도는 더 빠르지만 반대로 입력/삭제/수정의 경우에는 더 느리다.
보조 인덱스(Non-clustered Index, Secondary Index)
클러스터 인덱스와 달리 자동 정렬을 하지 않는 인덱스로, 별도의 페이지에 인덱스를 구성한다. 데이터 페이지에 변화를 주지 않기 때문에, 클러스터 인덱스와 달리 여러 개 생성이 가능하다.
하지만 별도의 추가 저장 공간을 사용하기 때문에, 인덱스를 너무 많이 생성하게 되면 공간 낭비가 발생한다.
클러스터 인덱스에 비해 검색 속도는 더 느리지만, 데이터 입력/수정/삭제는 더 빠르다.

B-tree 인덱스 구조

이와 같이 루트 노드 - 브랜치 노드 - 리프 노드 구조를 갖는 B-tree에서 인덱스는 각 노드를 페이지로 구성하여 저장한다.
페이지는 이와 같이 여러 개의 인덱스 레코드를 블록으로 묶은 단위를 말하며, 디스크와 메모리(버퍼)에 데이터를 읽고 쓰는 최소 단위이다. 때문에 한 개의 레코드를 읽더라도 최소 하나의 블럭(페이지)는 읽게 된다. 때문에 페이지에 저장되는 레코드 양이 커질수록 디스크 I/O가 잦아지고 메모리에 캐싱할 수 있는 페이지의 수가 줄어들 수 있다.
MySQL의 페이지 구성
MySQL의 인덱스를 구성하는 페이지는 위와 같이 페이지의 맨 앞과 맨 뒤에 infimum과 supremum 인덱스 레코드를 삽입하여 저장한다. 이를 Gap-lock이나 Nextkey-lock에 활용하여 동시성 제어를 수행하게 된다.
인덱스는 페이지 단위로 저장되고, 항상 인덱스 키를 기준으로 정렬하여 저장한다.
루트 노드나 브랜치 노드의 경우에는 자식 노드의 (인덱스 키, 페이지 번호) 형태로 인덱스 레코드를 저장한다. 리프 노드의 경우에는 클러스터 인덱스와 보조 인덱스가 조금 다르다.
PK 인덱스인 클러스터 인덱스의 경우에는 (PK, 실제 레코드 데이터) 형태로 저장이 되고, 보조 인덱스의 경우에는 (인덱스 키, PK) 형태로 저장되어 있어 보조 인덱스를 통해 PK를 얻은 후 클러스터 인덱스를 다시 타서 레코드를 가져온다. 이런 구조를 통해 조회하는 이유는, 인덱스는 테이블과 독립적인 공간에 저장되기 때문에 데이터를 조회하기 위해서는 먼저 PK를 찾고 그 이후에 PK를 따라 실제 레코드를 읽어온다.

보조 인덱스 성능에 영향을 미치는 요소

1.
PK의 크기
위에서 보조 인덱스의 경우에는 리프 노드에 (인덱스 키, PK) 형태로 저장된다고 언급했다. 페이지의 크기는 고정되어 있기 때문에, PK 하나하나가 차지하는 저장 공간이 크다면 하나의 페이지에 더 적은 레코드가 구성될 것이다. 때문에 PK 값이 클수록 한 페이지에 담을 수 있는 인덱스 레코드가 줄고, 디스크 I/O나 메모리도 더 많이 사용해야하여 성능에 좋지않다.
2.
카디널리티
카디널리티는 데이터의 고유성을 말하는데, 인덱스로 지정된 컬럼의 데이터 중복이 많을수록 카디널리티가 낮고 반대로 데이터 중복이 적을수록 카디널리티가 높다고 표현한다. 보조 인덱스로 지정할 때 카디널리티가 높은 컬럼을 지정 할수록, 실제 조회 시 선택되는 레코드의 범위가 줄어 클러스터 인덱스를 통해 검색해야하는 PK 개수가 줄어 더 성능이 좋아진다.
3.
리프 노드의 깊이
리프 노드는 일반적으로 3~4단계의 깊이를 가지고 있는데, 자식 노드의 위치를 찾는 동작도 디스크 I/O가 수행되기 때문에 리프 노드의 깊이가 깊을수록 성능이 떨어진다. 데이터의 양이 많아지게 되는 경우 속도가 느려지는 원인이다.
4.
인덱스의 정렬 및 스캔 방향
보조 인덱스는 생성 시에 정렬 순서를 설정할 수 있는데, 읽을 때 정렬된 순서대로 읽는게 아니라 역순으로 읽는 것도 가능하다.
이와 같이 인덱스가 정렬된 순서로 읽는 것을 Forward Index Scan(정순 인덱스 스캔)이라 부르고, 반대 방향으로 읽는 것을 Backward Index Scan(역순 인덱스 스캔)이라 부른다.
역순 스캔은 정순 스캔보다 느린데, 그 이유는 두 가지가 있다.
페이지 Lock 방식이 정순 스캔에 최적화
정순 스캔의 경우 페이지 잠금을 다음 페이지 잠금 → 현재 페이지 잠금 해제 순으로 이루어진다. 하지만 내부적으로 데드락을 방지하기 위해 페이지 잠금을 왼쪽에서 오른쪽(forward) 방향으로만 획득하도록 하고 있어, 역순 스캔의 경우 페이지 잠금을 획득하는 과정이 상당히 복잡하여 처리가 지연된다.
페이지 내 인덱스 레코드가 Single Linked List
리프 노드의 한 페이지 안에는 수많은 레코드가 저장될 수 있는데, 이 많은 레코드를 하나씩 순차적으로 비교한다면 레코드 검색이 상당히 느릴 것이다. 때문에 정렬된 레코드를 4~8개씩 묶어서 별도의 리스트로 관리한다.
문제는 B-tree의 리프 노드 페이지들은 Double Linked List로 구성되어 있지만, 페이지 내부의 이 인덱스 레코드 리스트는 Single Linked List로 구성되어 있다. 따라서 역순 스캔 시 페이지 내에서 리스트를 정방향으로 순회 후에 찾고, 없다면 다시 순회하여 이전 레코드를 찾고 하기 때문에 속도가 느리다.

CRUD의 인덱스 영향

인덱스는 정렬된 상태로 저장하는 B-tree의 균형 트리이기 때문에, 노드에 레코드가 부족하거나 넘치는 경우 재배치가 발생한다. 기본적으로 B-tree는 한 노드가 최소 50% 이상의 채움률(fill factor)을 유지하면 되어, 재배치(재구성)가 자주 발생하지 않도록 설계 되어 있다. 예를 들어 4개의 키를 저장 가능하다면, 노드에 레코드가 2~4개가 있다면 균형이 유지된다고 보고 재배치를 하지 않는다. 하지만 레코드가 1개거나 5개인 경우에는 재배치가 발생하게 된다.
1.
삽입
레코드가 추가되면 인덱스도 같이 추가되어야 하고, 위에서 설명했듯이 대부분의 경우 단순히 인덱스 레코드를 삽입하고 동작이 마무리된다. 하지만 삽입한 레코드로 인해 노드가 가득 찼다면, 노드의 분할(split)이 발생한다.
2.
삭제
삭제 역시 마찬가지로 대부분 인덱스 레코드 삭제로 끝나지만, 삭제로 인해 노드의 레코드가 부족해지면 형제 노드와 재분배하거나 노드 병합이 발생하게 된다.
3.
수정
PK 값을 수정하게 되는 경우, 클러스터 인덱스의 물리적인 레코드 저장 위치를 바꾸어야하기 때문에 단순히 값을 변경하는게 아니라 delete 후 insert 해주어야한다. 두 번의 디스크 I/O가 발생하기 때문에 PK 값은 변하지 않는 값으로 설정하는 것이 중요하다.
4.
조회
B-tree는 인덱스 키 값을 변형하지 않고 정렬된 상태로 저장하는데, 때문에 동등 조건이나 부등호 조건의 쿼리에 인덱스를 사용한다. 하지만 like ‘%name%’와 같이 인덱스 키의 뒷 부분을 통한 검색은 인덱스를 활용할 수 없다.
조회의 경우 인덱스에 직접적으로 영향을 미치지는 않는다. 반대로 인덱스의 조건에 따라 조회의 성능이 영향을 받을 수 있다. 유니크 인덱스의 경우 옵티마이저가 1건만 조회 후 더 찾지 않아도 되는 것을 알기 때문에 성능이 빠르다. 하지만 PK나 인덱스가 아닌 값으로 검색을 하고자 하면 레코드가 어디 있는지 모르기 때문에 테이블을 전체 탐색(테이블 풀 스캔)하게 된다.
MySQL의 데이터 변경 시 주의 사항
다른 DBMS와 다르게 MySQL의 Record Lock은 실제 레코드가 아닌 인덱스를 잠근다. 인덱스를 잠그기 때문에, 데이터 변경 시 검색한 모든 인덱스에 락을 걸게 된다. 만약 인덱스조차 없다면 테이블 풀 스캔이 발생하여 테이블의 모든 레코드에 락이 걸린다. 때문에 데이터 변경 시에는 검색 조건을 잘 설정해야한다.

인덱스 성능 최적화 전략

인덱스는 성능을 최적화하기 여러 방법을 사용하고 있다.
먼저 조회 시에는 자주 사용되는 페이지는 디스크 I/O가 발생하는 페이지 탐색보다 속도가 빠른 메모리 버퍼 풀에 페이지를 캐싱해 사용한다.
삽입과 삭제의 경우 노드의 재배치로 인해 성능이 떨어지는 것을 최소화하기 위해, 각 노드를 완전히 채우지 않고 여유 공간을 확보해두고 삭제된 레코드도 바로 공간을 회수하지 않고 추후 삽입에 재사용하기도 한다. 이러한 이유로 기본적으로 재배치가 자주 발생하지 않기도 하고, 삽입과 삭제로 인해 전체 트리를 재구성하는 경우는 거의 없다.
하지만 대량의 데이터가 한 번에 저장되는 경우, 지연 쓰기(lazy write) 전략을 사용해 최적화 하기도 한다. 지연 쓰기 전략은 수정 사항을 캐싱해두고 일정 시간이 지나거나 버퍼가 가득차면 일괄적으로 수정사항을 반영하는 방법이다. 삽입(Insert buffer)의 경우 메모리에 저장해두고 임계점이 지나면 일괄 삽입한다. 삭제(Deletion Maring)의 경우에는 물리적 삭제를 하지 않고 메모리에 삭제 플래그를 설정해두고 마찬가지로 임계점이 지나면 일괄 삭제한다. 이런 지연 쓰기 전략은 MVCC(다중 버전 동시성 제어)와 연계하여, 트랜잭션 롤백을 더 쉽게 하고 공간 재사용 최적화를 가능하게 만든다.
하지만 지연 쓰기 전략은 메모리 사용량이 증가한다는 점과 시스템 장애 시 버퍼 데이터 손실이 발생할 수 있어 주기적인 체크포인트가 필요하다. 또한 중복된 값을 체크하는 등의 데이터 무결성이 필요한 경우에는 지연 쓰기 전략을 사용하지 못한다.

인덱스 스캔의 종류

인덱스 전체 스캔(Index Full Scan)
인덱스의 모든 데이터를 읽어오는 방법으로, 인덱스의 모든 키 값을 검색하여 특정 조건을 만족하는 레코드를 찾을 때 사용된다. 테이블 풀 스캔에 비해 특정 조건에 맞는 레코드를 빠르게 찾을 수 있지만, 인덱스의 크기와 검색 조건에 따라 성능이 달라진다.
SELECT * FROM Customer WHERE date >= '2020-01-01' AND date <= '2020-01-31';
SQL
복사
인덱스 조건의 크기가 커지거나 해당 조건 사이의 레코드가 너무 많으면 테이블 전체 스캔이 사용되는 경우가 많다.
인덱스 고유 스캔(Index Unique Scan)
인덱스의 유일한(unique) 값을 읽어오는 방법으로, 인덱스 컬럼이 유일한 값을 가지고 있을 때만 사용된다. 인덱스의 크기와 테이블의 크기에 관계없이 항상 빠른 검색을 보장한다. 주로 WHERE 조건에 등호(=)가 사용되고, 사용된 컬럼이 PK나 Unique 조건인 경우에 사용된다.
SELECT * FROM Customer WHERE id = 1;
SQL
복사
인덱스 범위 스캔(Index Range Scan)
인덱스의 일부 데이터만 읽어오는 방법으로, 인덱스의 범위를 지정하여 빠른 검색이 가능하다. 인덱스의 크기와 테이블의 크기에 관계없이 검색 조건 범위의 크기에만 영향을 받는다. 주로 WHERE 절에서 범위 검색을 수행할 때 사용된다.
SELECT * FROM Customer WHERE date BETWEEN '2020-01-01' AND '2020-12-31';
SQL
복사
WHERE 절의 범위 검색 뿐만 아니라 ORDER BY 절을 통한 정렬이나 JOIN 연산을 수행할 때도 사용되기도 한다.
인덱스 루스 스캔(Index Loose Scan)
인덱스의 범위가 아닌 일부 데이터만 읽어오는 방법으로, 중간중간 필요 없는 인덱스 키 값을 건너뛰고 다음으로 넘어가 검색하는 방식이다. 이 역시 마찬가지로 인덱스의 크기와 테이블의 크기와는 관계없지만 정확도가 떨어질 수 있다. 주로 WHERE 절에 LIKE 키워드를 사용하여 특정 문자열을 검색하는 경우 발생한다.
SELECT * FROM Customer WHERE name LIKE '박%';
SQL
복사
인덱스 온리 스캔(Index Only Scan)
인덱스만을 사용하여 쿼리를 처리하는 방법으로, 커버링 인덱스(covering Index)로도 불린다. 실제 데이터 테이블에는 접근이 발생하지 않고, 검색 쿼리의 선택 컬럼이 인덱스로 지정된 값만 포함하는 경우에 사용된다.
SELECT id FROM Customer;
SQL
복사
인덱스 병합 스캔(Index Merge Scan)
여러 개의 인덱스를 병합하여 검색하는 방법으로, 복잡한 검색 조건을 처리할 수 있지만 병합으로 인한 시간이 추가적으로 소요된다. 인덱스의 크기가 작은 경우에만 유용하다. 주로 OR 조건이나 AND 조건이 포함된 쿼리에서 각각의 조건에 해당하는 인덱스를 사용하는 상황이나, UNION ALL 연산자를 수행하는 경우에 사용된다.
SELECT * FROM Customer WHERE id = 1 AND name = '박지원';
SQL
복사

복합 인덱스

복합 인덱스(Composite Index)는 두 개 이상의 컬럼을 조합하여 생성하는 인덱스를 말한다.
결합 인덱스 복합 인덱스는 결합 인덱스(Joined Index)와는 다른 개념인데, 결합 인덱스는 두 개 이상의 테이블에서 조인 조건으로 사용되는 컬럼을 모두 포함하는 인덱스를 말한다.

복합 인덱스 구조와 사용 조건

복합 인덱스는 DBMS마다 조금씩 다르지만 일반적으로 순서가 매우 중요하다. 이는 B-tree의 정렬 방식과 관련이 있는데,
CREATE TABLE users ( id INT PRIMARY KEY, a VARCHAR(50); b INT, c INT ); CREATE INDEX idx_a_b ON users (a, b);
SQL
복사
이와 같이 인덱스를 생성한다면
리프 노드에는 위와 같이 a → b 순서로 정렬하여 인덱스를 생성한다. 만약 a, b, c 3개의 컬럼을 사용하여 인덱스를 생성한다면, a 컬럼 기준으로 정렬 - a 컬럼이 같은 경우 b 컬럼으로 정렬 - b 컬럼까지 같은 경우 c 컬럼으로 정렬 이와 같은 방식으로 순차적으로 정렬한다.
이렇게 인덱스 생성 시의 순서에 맞춰 정렬하기 때문에, 인덱스를 통한 검색을 사용하기 위해서는 생성 순서를 알고 검색 조건에 사용해야한다.
-- 완전 일치 WHERE a = ? AND b = ? AND c = ? # 인덱스 사용 가능 -- 앞 부분 일치 WHERE a = ? AND b = ? # 인덱스 사용 가능 WHERE a = ? # 인덱스 사용 가능 -- 중간 건너뛰기 WHERE a = ? AND c = ? # a 컬럼 검색 시에만 인덱스 사용 가능 -- 앞 부분 건너뛰기 WHERE b = ? AND c = ? # 인덱스 사용 불가능 WHERE c = ? # 인덱스 사용 불가능 -- 범위 조건 검색 WHERE a = ? AND b BETWEEN ? AND ? # 인덱스 사용 가능 WHERE a = ? AND c BETWEEN ? AND ? # a 검색 시만 인덱스 사용 가능, c 검색 시에는 필터링으로 처리
SQL
복사
이와 같이 인덱스 생성 시 설정한 좌측 조건부터 검색을 해야 인덱스를 통해 검색을 수행하고, 이 규칙을 Left prefix 규칙 혹은 Left-most prefix 규칙이라 부른다.

복합 인덱스 최적화 전략

Left-most prefix 규칙 때문에, 복합 인덱스를 생성하는 경우 쿼리 패턴을 분석 후에 사용해야한다.
복합 인덱스에서 각 인덱스를 통해 값을 줄여나가는 과정은 다음의 두 가지로 볼 수 있다.
1.
범위 결정 조건 : 조회된 결과가 범위로 묶여있어 해당 범위에 대한 스캔만 수행
2.
필터링 조건 : 조회된 결과가 흩어져있어 스캔 중 필터링을 수행
위 두 상황 중 당연하게도 범위를 결정하는 인덱스가 성능과 효율이 더 좋다. 때문에 복합 인덱스를 통해 조회 쿼리를 최적화 한다면, 왼쪽의 인덱스 탐색 결과가 필터링 조건이 아닌 범위를 결정하는 조건으로 잡아두는 것 좋다. 인덱스는 내부를 정렬된 상태를 유지한다는 점에 유의하면서 이를 고려해 인덱스를 설계할 필요가 있다.
다음의 간단한 예시를 통해 이를 이해해보자.
status
orderdAt
주문완료
2025-02-11
주문완료
2025-02-12
주문완료
2025-02-13
주문실패
2025-02-11
주문실패
2025-02-14
1.
INDEX (status, orderedAt)
orderedAt
status
2025-02-11
주문완료
2025-02-11
주문실패
2025-02-12
주문완료
2025-02-13
주문완료
2025-02-14
주문실패
2.
INDEX (orderedAt, status)
status = ?orderedAt between ? and ?의 조건으로 검색할 때, 위와 같이 복합인덱스를 어떻게 설정하느냐에 따라 조회 성능이 달라진다. status를 먼저 두는 경우 뒤의 검색 수행이 범위 탐색이 되는데 반해, orderedAt을 먼저 두는 경우 전체를 스캔하면서 status를 필터링해야한다.
이와 같은 조건들을 고려할 필요가 있기 때문에, 비즈니스의 패턴과 조건절의 선택도, 자주 사용되는 조건 순서, 조건의 정렬이나 그룹화 활용 등을 고려하여 인덱스를 생성해야한다.
일반적으로 복합 인덱스가 사용되는 상황은 자주 호출되는 특정 쿼리에 대해 최적화를 수행하여 조회 성능을 올리기 위함이기 때문에, 카디널리티가 낮은(중복이 많은) 필드를 더 왼쪽에 두고 범위 검색이나 like 검색과 같이 추출했을 때 정렬이 잘 수행되어있지 않는 필드를 오른쪽에 설정한다면 보편적으로 복합 인덱스의 성능이 높아진다.

복합 인덱스를 사용하는 이유

실무에서는 단일 인덱스보다 복합 인덱스가 더 많이 사용된다. 그 이유는 다음과 같다.
1.
쿼리 패턴
SELECT * FROM Orders WHERE customerId = 123 AND orderedAt BETWEEN '2024-01-01' AND '2024-01-31';
SQL
복사
주로 사용되는 조회 시에는 조회 성능이나 비즈니스 로직에 의해 위와 같이 특정 기간이나 특정 상태를 조합하여 검색하는 경우가 대부분이다.
위 쿼리를 Customer의 PK인 customerId로 클러스터 인덱스와 orderedAt에 보조 인덱스를 추가한 후 수행한다면, 각각의 인덱스를 통해 개별적으로 검색 후에 결과를 조합해서 찾게된다. 하지만 복합 인덱스를 통해 검색한다면 연관된 컬럼들이 함께 정렬되어 있어 더 효율적으로 검색을 수행한다. 때문에 자주 사용되는 검색 조건들을 복합 인덱스로 같이 등록하여 더 빠르고 효율적으로 데이터 검색을 수행하기 위해 사용된다.
2.
저장 공간 활용
일반적으로 여러 개의 단일 컬럼 인덱스보다 하나의 복합 인덱스가 더 적은 공간을 차지하게 된다. 인덱스도 디스크 공간을 차지하기 때문에 디스크 I/O 횟수나 저장 공간 측면에서 복합 인덱스가 더 효율적이다.
3.
인덱스 선택도(Selectivity) 향상
여러 컬럼을 조합하여 인덱스를 구성하기 때문에, 더 구체적인 검색이 가능해지고 그로 인해 인덱스 선택도가 높아져 더 빠른 속도로 조회를 수행할 수 있다.

복합 인덱스 설계 시 주의할 점

복합 인덱스는 그 저장 구조 때문에 데이터 조회 시 Left-Prefix 규칙을 고려해서 조회 해야한다.
위에서 설명했듯이 Left-Prefix 규칙 때문에 가장 먼저 선택한 컬럼부터 조회를 수행해야 인덱스 검색을 수행할 수 있고, 그 순서가 바뀐다면 인덱스가 사용되지 않는다.
또 다른 주의사항은 복합 인덱스 구성 시에 과도하게 많은 컬럼을 포함시킨다면, 오히려 성능 저하가 발생할 수 있다는 점이다.
이러한 주의사항들 때문에 실제 인덱스 설계 시에는 쿼리 패턴 분석이 선행된 후, 상황에 맞춰 단일 인덱스와 복합 인덱스를 적절하게 조합하여 사용되어야 한다.

참고