데이터베이스 인덱스란 무엇인가?
인덱스는 데이터베이스에서 특정 레코드를 빠르게 찾기 위해 사용하는 자료 구조입니다. 기본적으로 데이터를 검색할 때 데이터베이스는 테이블 전체를 순차적으로 훑는 방식(Full Table Scan)을 사용할 수 있습니다.
하지만 데이터의 양이 많아질수록 이 방식은 매우 비효율적입니다. 마치 백과사전에서 특정 항목을 찾기 위해 처음부터 끝까지 다 읽는 것과 같습니다. 이를 해결하기 위해 등장한 것이 인덱스입니다. 인덱스는 사전의 색인처럼, 특정 컬럼의 값을 기준으로 정렬된 자료 구조를 유지하며, 검색 조건에 맞는 데이터를 빠르게 탐색할 수 있도록 도와줍니다.
다만, 인덱스를 유지하기 위해서는 삽입, 갱신, 삭제시마다 인덱스 구조도 함께 수정되어야 하므로, 검색 속도는 빨라지는 대신 쓰기 작업의 성능은 다소 저하될 수 있습니다. 때문에 인덱스를 생성 시 해당 테이블의 의도를 정확하게 파악한 후에 상황에 맞게 적절한 칼럼으로 Clustered Index와 Non Clustered Index를 구성해야 합니다.
간단히 먼저 비유적으로 알아보자면
클러스터링 인덱스는 책의 내용이 이름순으로 정렬되어 있는 이력서 책과 같아서, 바로 그 페이지를 펼치면 해당 사람의 이력서가 나옵니다.
넌 클러스터링 인덱스는 책 맨 뒤에 있는 색인표처럼, 원하는 사람의 이름을 색인에서 찾아보고, 거기 적힌 페이지를 다시 넘겨서 찾아야 합니다.
테이블 전체 스캔은 색인 없이 처음부터 한 장씩 넘기며 찾는 방식입니다.
클러스터링 인덱스와 넌 클러스터링 인덱스
클러스터링 인덱스
클러스터링 인덱스는 테이블의 레코드를 특정 열 기준으로 물리적으로 정렬하여 저장하는 인덱스입니다. 이 인덱스를 구성할 때, 데이터베이스는 해당 열을 기준으로 테이블의 실제 행 데이터를 정렬한 후 이를 기반으로 인덱스 트리를 생성합니다.
트리는 루트 노드와 리프 노드로 구성되며, 루트 노드는 검색을 위한 경로를 제공하고, 리프 노드는 실제 데이터 페이지를 가리킵니다. 리프 노드는 곧 실제 데이터를 포함하고 있는 구조이기 때문에, 클러스터링 인덱스는 인덱스 자체가 곧 테이블이라고 표현되기도 합니다.

이러한 구조의 가장 큰 특징은 별도의 인덱스 저장소가 따로 존재하지 않는다는 점입니다. 테이블은 클러스터링 인덱스에 따라 정렬된 상태로 저장되고, 데이터는 해당 인덱스 구조의 리프 노드에 포함됩니다. 따라서 하나의 테이블에는 오직 하나의 클러스터링 인덱스만 존재할 수 있습니다.
일반적으로 기본 키를 지정하면 해당 키가 자동으로 클러스터링 인덱스로 설정됩니다. 만약 사용자가 명시적으로 다른 열을 클러스터링 인덱스로 설정하고자 한다면, 설정을 통해 이를 지정할 수 있습니다.
클러스터링 인덱스의 가장 큰 장점은 검색 성능입니다. 인덱스를 통해 검색하면 중간 단계 없이 바로 원하는 데이터에 접근할 수 있기 때문에 빠른 속도를 자랑합니다.
특히 정렬된 데이터가 필요한 쿼리, 예컨대 ORDER BY, GROUP BY, MIN, MAX와 같은 연산에서 매우 효과적입니다. 또한 범위 검색이 빈번한 경우에도 성능 이점이 큽니다. 이처럼 클러스터링 인덱스는 읽기 중심의 시스템에서 매우 유리한 구조를 제공합니다.
하지만 단점도 분명합니다. 테이블이 항상 정렬된 상태를 유지해야 하기 때문에, 새로운 데이터를 삽입하거나 기존 데이터를 수정, 삭제할 때 페이지 분할이나 병합 등의 연산이 발생할 수 있습니다.
이러한 연산은 전체적인 정렬 구조를 재조정해야 하므로 성능 저하로 이어질 수 있습니다. 예를 들어 리프 노드가 가득 찬 상태에서 새로운 레코드가 삽입되면, 기존 데이터를 반으로 나눠 새로운 페이지로 이동시켜야 하고 이로 인해 추가적인 I/O와 트리 재조정이 필요합니다.
넌 클러스터 인덱스
넌 클러스터링 인덱스는 클러스터링 인덱스와는 다르게 동작합니다. 넌 클러스터링 인덱스는 인덱스와 데이터가 분리된 구조이며, 테이블의 데이터는 별도로 저장되고, 인덱스는 이를 참조하는 형태입니다.
인덱스 자체는 특정 열을 기준으로 정렬되며, 리프 노드에는 해당 키 값과 함께 원본 데이터가 존재하는 위치 정보를 저장합니다. 이 위치 정보는 일반적으로 RID(Row Identifier)라고 불리며, 파일 그룹 번호, 데이터 페이지 번호, 그리고 페이지 내의 행 번호로 구성되어 있습니다.
포인터(RID): '파일그룹번호+데이터페이지 번호 + 페이지 내의 로우 번호'으로 구성되는 포인팅 정보

넌 클러스터링 인덱스를 사용한 검색 과정에서는 먼저 루트 노드에서 키 값을 비교하여 적절한 리프 노드로 이동하고, 리프 노드에서 RID를 통해 실제 데이터가 저장된 테이블의 행으로 접근하는 방식입니다. 이처럼 데이터를 직접 포함하고 있는 구조가 아니기 때문에, 한 단계 더 접근해야 하며 이 과정은 흔히 루킹(lookup)이라고도 합니다.
넌 클러스터링 인덱스의 장점은 데이터와 인덱스가 분리되어 있어 데이터를 삽입하거나 수정, 삭제할 때 클러스터링 인덱스에 비해 영향을 덜 받는다는 점입니다. 페이지 분할이 발생하지 않거나 발생하더라도 클러스터링 인덱스보다는 빈도가 낮으며, 데이터 정렬을 유지할 필요가 없기 때문에 DML 연산에 더 적합합니다.
또 하나의 테이블에 여러 개의 넌 클러스터링 인덱스를 생성할 수 있어, 다양한 검색 조건에 유연하게 대응할 수 있습니다. 예를 들어 WHERE, JOIN, LIKE 등의 필터링 조건에 자주 사용되는 열에 대해 넌 클러스터링 인덱스를 부여하면 쿼리 성능을 높일 수 있습니다.
하지만 단점도 존재합니다. 가장 큰 문제는 검색 시 두 번의 접근이 필요하다는 점입니다. 키를 찾기 위해 인덱스를 탐색한 후, 실제 데이터를 찾기 위해 테이블을 다시 조회해야 하므로 I/O 비용이 상대적으로 큽니다. 또한 별도의 인덱스 페이지를 저장해야 하므로 디스크 공간을 더 많이 차지하고, 클러스터링 인덱스의 키가 바뀌면 이에 따라 넌 클러스터링 인덱스도 함께 수정되어야 하는 비용이 발생합니다.
둘의 차이 비교 표

인덱스가 사용하는 자료구조
인덱스는 여러 자료구조를 이용해서 구현하 수 있는데, 대표적인 자료구조로 해시 테이블과 B+Tree가 있습니다.
해시 테이블(Hash Table)

해시 테이블은 키-값 쌍으로 데이터를 저장하는 자료구조로, 해시 함수를 이용해 특정 키에 대한 주소를 바로 계산할 수 있어 정확한 값 비교에 대해 매우 빠른 조회 속도를 보장합니다. 하지만 정렬된 구조가 아니기 때문에 범위 검색에는 부적합합니다.
또한 해시 충돌이 발생할 경우 별도의 충돌 해결 방식(체이닝, 오픈 어드레싱 등)을 적용해야 하며, 충돌이 많아질수록 성능이 저하됩니다.
이러한 이유로 대부분의 DBMS에서는 해시 기반 인덱스는 특정 상황에서만 제한적으로 사용되며, 주류 인덱스 구조로 채택되지는 않습니다. 예를 들어 PostgreSQL의 Hash Index는 특별한 조건에서만 성능이 보장되며, 기본 인덱스로는 잘 쓰이지 않습니다.
B+Tree

대부분의 관계형 데이터베이스에서 인덱스는 B+Tree 구조로 구현되어 있습니다. 이는 B-Tree에서 파생된 구조로, 검색과 정렬, 특히 범위 검색에 최적화된 자료구조입니다.
먼저 B-Tree와 B+Tree의 공통 구조를 이해하려면 트리의 계층을 알아야 하는데, 두 자료구조 모두 루트 노드, 브랜치 노드(중간 노드), 리프 노드(말단 노드)로 구성된 균형 트리입니다. 각 노드는 여러 개의 키를 포함할 수 있고, 트리의 균형을 유지하기 위해 삽입과 삭제 시 자동으로 재구성됩니다.
B-Tree는 루트, 브랜치, 리프 노드 모두에 데이터가 저장될 수 있는 구조입니다. 즉, 리프 노드뿐만 아니라 중간 노드에도 실제 데이터가 존재할 수 있기 때문에, 데이터를 검색하다가 중간 노드에서 결과가 나올 수도 있고, 리프 노드까지 가야 할 수도 있습니다.
이 때문에 범위 검색이나 정렬이 필요한 경우에는 탐색이 다소 비효율적일 수 있습니다. 또한 노드 간 순차 연결이 없기 때문에 리프 노드들을 따라가며 연속된 데이터를 읽는 것도 어렵습니다.
반면에 B+Tree는 데이터를 오직 리프 노드에만 저장하는 구조입니다. 루트와 브랜치 노드는 오직 키 값과 하위 노드의 포인터만을 포함하며, 리프 노드는 모든 실제 데이터를 담고 있습니다. 중요한 차이는 여기에 더해, 모든 리프 노드가 순차적으로 연결되어 있다는 점입니다. 이 연결 덕분에 정렬된 데이터 집합을 순서대로 빠르게 순회할 수 있고, 범위 기반 연산이 매우 효율적으로 수행됩니다.
또한 리프 노드에만 데이터를 저장하기 때문에, 모든 검색은 항상 리프 노드에서 종료되며, 이로 인해 탐색 경로가 더 예측 가능하고 일관됩니다. 트리의 깊이가 얕아지는 효과도 있어 디스크 접근 횟수를 줄일 수 있습니다. 이러한 특징 때문에 대부분의 RDBMS는 B-Tree 대신 B+Tree를 인덱스 구조로 채택하고 있습니다.
- - B-Tree는 검색 중간에 데이터를 찾을 수 있지만 순차적 접근에는 불리합니다.
- - B+Tree는 항상 리프 노드까지 가야 하지만 그 대신 정렬성과 범위 탐색 성능이 탁월하며, 리프 노드 간 연결로 연속적인 탐색에도 강한 구조입니다.
결국 데이터베이스 인덱스가 정렬, 범위 탐색, 대용량 데이터 효율성 등을 고려해야 하는 특성상, B+Tree가 훨씬 적합한 구조인 것입니다.
MySQL 스캔 방식
인덱스 레인지 스캔(Index Range Scan)은 인덱스를 활용하는 가장 일반적이고 빠른 방식입니다. 특정 조건에 해당하는 값들의 범위가 명확히 주어졌을 때 사용되며, 먼저 시작 위치를 탐색한 후, 그 위치부터 조건을 만족하는 인덱스 항목들을 순차적으로 읽어 내려갑니다.

레코드 접근 시 랜덤 I/O가 동반되기 때문에, 전체 데이터의 20~25% 이상을 읽게 된다면 오히려 풀 테이블 스캔이 더 효율적일 수 있습니다.
인덱스 풀 스캔(Index Full Scan)은 인덱스를 처음부터 끝까지 전부 읽는 방식입니다. 이는 인덱스를 활용하긴 하지만 조건절이 인덱스의 선두 컬럼을 포함하지 않거나 정렬을 위해 필요한 경우 등에 사용됩니다.

인덱스가 정렬되어 있기 때문에 데이터 정렬을 최소화할 수는 있지만, 조건 필터링 효과는 떨어집니다. 테이블 전체를 읽는 것보다는 덜 부담스럽지만, 인덱스를 설계한 본래 목적에는 부합하지 않을 수 있습니다.
루스 인덱스 스캔(Loose Index Scan)은 인덱스를 부분적으로 건너뛰며 읽는 방식입니다. 필요한 값만 골라서 읽고, 중간 값을 생략하기 때문에 전체 범위를 읽지 않아도 되는 장점이 있습니다. 주로 GROUP BY, MIN(), MAX() 같은 연산이 포함된 쿼리에서 사용되며, 이러한 집계 연산을 인덱스 수준에서 최적화해주는 경우가 많습니다. 이 방식은 범위를 tight하게 읽는 앞선 두 방식과 구분하여 '루스(loose)'하게 읽는다고 표현합니다.
마무리 정리
요약하자면, 인덱스는 검색을 빠르게 하기 위해 존재하며, 구현 방식에 따라 클러스터링 인덱스는 인덱스 자체가 데이터를 포함하고 있고, 넌 클러스터링 인덱스는 주소만 포함하여 한 단계 더 조회가 필요합니다. 그리고 인덱스는 내부적으로 B+Tree라는 자료구조를 사용함으로써 정렬된 데이터를 기반으로 효율적인 탐색과 범위 검색을 가능하게 합니다. 해시 테이블은 특정 용도에서는 빠르지만, 범용 인덱스 구조로는 부적합합니다.
'CS' 카테고리의 다른 글
| 프록시와 리버스 프록시, 그리고 자바 스프링에서의 프록시 활용 정리 (0) | 2025.04.17 |
|---|---|
| 스프링 빈 생명주기(Bean LifeCycle)가 무엇인가요? (0) | 2025.04.14 |
| 일급 컬렉션이 무엇인가요? (0) | 2025.04.09 |
| Spring Data JPA에서 새로운 Entity인지 판단하는 방법은 무엇일까요? (0) | 2025.04.08 |
| 스프링 Ioc와 DIP란? (1) | 2025.02.10 |