
⚡ 한 줄 요약: 인덱스는 B+Tree 자료구조를 활용해 데이터 조회 성능을 극대화하는 '지름길'이며, 쓰기 비용과의 트레이드오프와 물리적/논리적 분산 전략을 이해하는 것이 성능 최적화의 핵심입니다.
수천, 수만 개의 데이터를 다루는 다중 필터나 무한 스크롤을 구현하다 보면, 특정 조건에서 API 응답이 현저히 느려지는 경험을 하게 됩니다.
단순히 "데이터가 많아서"라고 넘기기엔 엔지니어로서 아쉬움이 남죠.
그 해답의 90%는 바로 인덱스(Index)에 있습니다.
인덱스는 데이터베이스의 검색 속도를 향상시키기 위해 목차와 같이 데이터를 정리해둔 자료구조입니다.
도서관에서 원하는 책을 찾는 상황을 떠올려 보세요.
책 제목을 하나하나 다 확인하면서 찾는다면 시간이 너무 오래 걸리겠죠?
하지만 목차나 서가 번호 분류표를 보고 가면 원하는 위치로 바로 갈 수 있습니다.
데이터베이스의 인덱스가 바로 이 목차와 분류표의 역할을 합니다.
인덱스가 없다면 컴퓨터는 데이터를 찾기 위해 처음부터 끝까지 다 뒤져야 하는 풀 스캔(Full Scan)을 해야 하지만, 인덱스를 잘 활용하면 필요한 데이터만 빠르게 뽑아낼 수 있습니다.
클러스터형 인덱스는 데이터 자체가 인덱스 순서대로 정렬되어 저장되는 방식입니다.

물리적 정렬
특징
성능
데이터를 미리 정렬해두어 조회 시 압도적인 속도를 내는 것이 핵심입니다.
비클러스터형 인덱스는 원래 테이블의 순서는 건드리지 않고, 특정 컬럼별로 정렬된 별도의 '인덱스 테이블'을 만드는 방식입니다.

별도 테이블 생성
조회 방식
특징
실제 데이터를 찾으려면 반드시 PK를 통해 이동해야 하므로, 클러스터형 인덱스(PK 정렬)가 설정되어 있다는 전제가 필요합니다.
그래서, 비클러스터형 인덱스를 보조 인덱스라고도 부릅니다.
비클러스터형 인덱스가 많을수록 좋은 것 아닌가요?
왜 클러스터형은 하나만 만들 수 있나요?
B-Tree는 데이터를 여러 노드로 분할해 저장하고, 단계별로 범위를 좁혀가며 의 속도로 데이터를 찾아내는 자료구조입니다.
데이터베이스 인덱스는 검색 성능을 높이기 위해 특정한 자료구조를 사용하는데, 그 대표적인 것이 바로 B-Tree입니다.

핵심 동작 원리
노드와 데이터의 구성
B-Tree 구조를 보면 각 노드 아래에 특정한 표시들이 달려 있는 것을 볼 수 있습니다.
이 표시가 바로 실제 데이터를 가리키는 포인터입니다.
노드에 적혀 있는 숫자는 데이터의 PK(기본 키)를 의미합니다.
분할 저장의 이유
데이터를 한곳에 모아두지 않고 여러 블록(노드)으로 분할하여 저장하는 이유는 오직 조회 속도를 높이기 위해서입니다.
전체 데이터를 처음부터 끝까지 훑는 대신, 필요한 구간만 골라내기 위함입니다.
단계적 탐색 과정
찾고자 하는 ID 값과 각 노드가 담고 있는 ID 값을 비교합니다.
루트 노드부터 시작해 노드 안에 있는 키들을 쭉 훑어보고, 해당 PK 구간을 관리하는 자식 노드로 내려갑니다.
예를 들어 ID=40을 찾는다면, 루트 노드(30)를 보고 "30보다 크니 오른쪽으로 가자"고 판단하여 범위를 좁힙니다.
중간 노드(50, 80)에 도달하면 "50보다 작으니 왼쪽 리프 노드로 가자"고 다시 한번 범위를 좁힙니다.
이처럼 단계적으로 범위를 좁혀가며 탐색하면, 수만 건의 데이터 사이에서도 단 몇 번의 이동만으로 원하는 데이터에 도달할 수 있습니다.
데이터는 마지막(리프) 노드에만 있나요?
아니요, B-Tree는 루트 노드와 중간 노드를 포함한 모든 노드에 데이터(또는 데이터를 가리키는 포인터)가 저장됩니다.
어떤 노드에서든 찾는 값이 있다면 바로 데이터를 반환할 수 있는 구조입니다.
항상 정렬되어 있어야 하나요?
💻 참고
실무에서는 B-Tree의 모든 노드에 데이터가 있는 특징 때문에 노드의 크기가 커지면 디스크 I/O 효율이 떨어지는 단점을 고민하게 됩니다.
그래서 현대의 많은 데이터베이스는 이를 개선해 데이터는 리프 노드에만 몰아두고 노드 간 연결성을 강화한 B+Tree를 실제 인덱스 구조로 더 많이 채택하고 있습니다.
B-Tree의 범위 검색 취약점을 보완하기 위해 데이터는 리프 노드에만 저장하고, 이들을 연결 리스트로 묶은 최적화된 자료구조입니다.
B-Tree는 모든 노드에 데이터를 품고 있어 특정 값을 찾는 데는 유리하지만, 여러 데이터를 찾는 범위 검색(Range Search)을 할 때는 효율이 떨어집니다.
데이터를 하나 찾을 때마다 다시 루트 노드부터 내려가야 하는 비효율이 발생하기 때문입니다.
이를 해결한 것이 바로 B+Tree입니다.

데이터 저장의 선택과 집중
길잡이 역할의 상위 노드
그렇다고 상위 노드들이 노는 건 아닙니다.
각각의 노드에 키 값은 존재하기 때문에, 검색 시 어디로 내려가야 할지 알려주는 길잡이 역할은 그대로 수행합니다.
검색 프로세스
찾고자 하는 값과 루트 노드의 키 값을 비교합니다.
값이 같거나 크면 오른쪽으로 이동하는 식으로 단계적으로 내려갑니다.
실제 데이터는 상위에 없으므로, 무조건 리프 노드까지만 이동해야만 원하는 데이터를 얻을 수 있습니다.
리프 노드의 연결성
상위 노드에는 데이터가 없으니 더 느린 것 아닌가요?
데이터를 얻으려면 리프까지 가야 하니 개별 탐색은 조금 더 걸릴 수 있지만, 상위 노드에 데이터를 안 담는 만큼 하나의 노드에 더 많은 키 값을 담을 수 있습니다.
이는 트리의 높이를 낮추는 결과로 이어져 전체적인 검색 성능을 안정화합니다.
B-Tree도 연결 리스트만 있으면 똑같은 것 아닌가요?
B-Tree는 데이터가 모든 노드에 흩어져 있어 연결 리스트만으로 해결되지 않습니다.
모든 데이터를 리프 노드로 몰아넣은 B+Tree의 구조적 특징이 전제되어야 범위 검색 최적화가 가능합니다.

클러스터형 인덱스는 데이터 자체가 인덱스의 리프 노드에 포함되어 정렬된 구조입니다.
클러스터형 인덱스는 데이터 저장 방식 그 자체를 결정합니다.
B+Tree 구조를 따라가면 리프 노드에 도달하게 되는데, 여기서 실제 데이터를 바로 만날 수 있는 것이 핵심입니다.
물리적 정렬
직접 접근
구조 예시

비클러스터형 인덱스는 실제 데이터가 아닌 '데이터의 주소(PK)'를 담고 있는 별도의 인덱스 테이블입니다.
비클러스터형 인덱스는 실제 데이터와 인덱스가 별도의 공간에 저장됩니다.
따라서 데이터를 찾는 과정이 한 단계 더 추가됩니다.
리프 노드의 정체
비클러스터형 인덱스의 리프 노드에서 만나는 것은 실제 데이터가 아니라 인덱스 테이블입니다.
이 테이블은 인덱스로 지정한 컬럼(예: name)과 해당 행의 PK 값으로 구성됩니다.
간접 접근 (2단계 탐색)
구조 예시
비클러스터형은 독립적으로 존재할 수 있나?
아닙니다. 비클러스터형 인덱스는 최종적으로 PK를 통해 원래 데이터를 찾아가야 하므로, 클러스터형 인덱스가 설정되어 있다는 전제하에 동작합니다.
그래서 이를 '보조 인덱스'라고 부르기도 합니다.
모든 인덱스는 리프 노드까지 가야 하나?
💻 참고
실무에서 비클러스터형 인덱스를 설계할 때, '커버링 인덱스(Covering Index)'라는 개념을 자주 활용합니다.
인덱스에 포함된 컬럼만으로 조회가 끝나서 실제 테이블을 다시 참조하지 않아도 되는 경우인데, 위에서 배운 2단계 탐색 과정을 1단계로 줄여주는 아주 중요한 성능 최적화 기법입니다.
💡 비유로 이해하기
클러스터형 인덱스는 사전과 같습니다.
단어(ID)를 찾으면 그 자리에 바로 뜻(데이터)이 적혀 있습니다.비클러스터형 인덱스는 전공 서적 맨 뒤의 색인(Index)과 같습니다.
키워드(name)를 찾으면 '몇 페이지(PK)'로 가라고 적혀 있고, 우리는 그 페이지를 다시 펼쳐야 실제 내용(데이터)을 볼 수 있습니다.
인덱스는 목적에 따라 클러스터형, 비클러스터형, 고유 인덱스로 구분하여 생성하며 각각 데이터 저장 방식이 다릅니다.
테이블의 실제 데이터 저장 순서를 결정하는 가장 강력한 인덱스입니다.
생성 방법
특징
코드 예시
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50)
); [cite: 1088-1091]
하나 또는 여러 컬럼에 대해 명시적으로 인덱스를 추가하는 가장 보편적인 방법입니다.
생성 방법
CREATE INDEX 구문을 사용하여 생성합니다.특징
코드 예시
CREATE INDEX idx_users_name ON users (name);
중복되지 않는 값을 보장해야 하는 컬럼(이메일, 주민번호 등)에 사용합니다.
생성 방법
CREATE UNIQUE INDEX 구문을 사용하거나 컬럼에 UNIQUE 제약 조건을 추가합니다.특징
중복 방지라는 제약 조건이 주요 목적이지만, 내부적으로는 인덱스가 생성되어 검색 속도도 향상됩니다.
보통은 비클러스터형으로 생성되지만, 만약 Primary Key로 지정된다면 클러스터형으로 생성됩니다.
코드 예시
CREATE UNIQUE INDEX idx_users_email ON users (email);
인덱스를 만들면 무조건 조회가 빨라지나요?
UNIQUE 인덱스는 검색용이 아닌가요?
WHERE 절에서 해당 컬럼을 검색할 때 매우 빠른 속도를 냅니다.CREATE INDEX는 비클러스터형 인덱스를 생성하며, 데이터의 중복을 막으면서 검색 속도를 높이고 싶을 때는 고유 인덱스를 활용합니다.
복합 인덱스는 두 개 이상의 컬럼을 하나의 인덱스로 묶어서 생성하는 자료구조입니다.
실무에서는 하나의 컬럼만으로 검색하기보다 WHERE name = '이수진' AND age = 30처럼 여러 조건을 동시에 만족하는 데이터를 찾을 때가 많습니다.
이때 복합 인덱스를 사용하면 데이터베이스가 여러 컬럼을 하나의 트리 구조 내에서 관리하므로 검색 효율을 비약적으로 높일 수 있습니다.
복합 인덱스를 설계할 때 가장 주의할 점은 컬럼의 배치 순서입니다.
왼쪽 컬럼 우선 정렬
(name, age)로 인덱스를 구성하면, 데이터는 우선 name을 기준으로 정렬되고, 그 안에서 같은 이름을 가진 데이터들끼리만 다시 age 순으로 정렬됩니다.인덱스 테이블의 민낯
name은 깔끔하게 정렬되어 있지만, age 컬럼만 떼어놓고 보면 값이 뒤죽박죽 섞여 있는 것을 볼 수 있습니다.사용 규칙
따라서 인덱스를 제대로 타려면 인덱스를 생성할 때 지정한 첫 번째 컬럼부터 순서대로 WHERE 절에 포함되어야 합니다.
인덱스 활용 가능
name과 age를 모두 조회하거나, 왼쪽 컬럼인 name만 단독으로 조회할 때인덱스 활용 불가
name 없이 age 조건만 사용하는 경우우리가 쿼리를 날리면 데이터베이스는 즉시 실행하는 것이 아니라 최적의 실행 계획을 세웁니다.
실행 계획 수립
Optimizer의 판단
WHERE 절에 age 조건만 있다면, DB는 "(name, age) 복합 인덱스를 타봤자 어차피 age가 정렬되어 있지 않아 이득이 없다"고 판단합니다.Full Scan 결정
이 경우 인덱스를 건너뛰고 테이블 전체를 하나씩 다 훑는 풀 스캔(Full Scan)을 선택하게 됩니다.
즉, 인덱스를 만들어 뒀어도 순서가 맞지 않으면 무용지물이 되는 것입니다.
컬럼 3개를 묶으면 3개 다 써야 인덱스가 타나요?
아닙니다. (col1, col2, col3) 인덱스라면 col1만 쓰거나 col1, col2만 써도 인덱스를 탈 수 있습니다.
오직 순서를 건너뛰거나 첫 번째 컬럼을 빼먹었을 때만 문제가 됩니다.
인덱스가 있으면 무조건 빨라진다?
💻 참고
프론트엔드에서도 다중 필터 기능을 구현할 때 백엔드의 복합 인덱스 설계를 알아야 합니다.
예를 들어 '카테고리'와 '태그'로 검색하는데 인덱스가
(카테고리, 태그)순이라면, 사용자가 태그만 선택했을 때 검색 속도가 급격히 느려질 수 있다는 점을 인지하고 기획이나 API 설계를 조율할 줄 알아야 합니다.
인덱스는 조회 성능(SELECT)을 비약적으로 향상시키지만, 데이터 변경(DML) 작업 시에는 유지 비용으로 인해 성능을 저하시키는 트레이드오프가 존재합니다.
도서관 분류표가 있으면 수만 권의 책 사이에서 원하는 책을 바로 찾듯, 인덱스가 있으면 대량의 데이터 검색이 매우 빨라집니다.
검색 최적화
예를 들어 수십만 명의 직원 중에서 부서명이 '개발팀'인 사람을 조회할 때, 인덱스가 없다면 모든 데이터를 다 훑는 풀 스캔(Full Scan)을 해야 합니다.
하지만 인덱스가 있다면 딱 해당 위치만 조회하면 되므로 속도가 압도적입니다.
연산 최적화
JOIN을 통해 테이블 여러 개를 비교하거나, 데이터를 순서대로 재배치하는 ORDER BY, GROUP BY 연산 시에도 미리 정렬된 인덱스 상태를 활용할 수 있어 쿼리 실행 시간을 크게 줄일 수 있습니다.많은 개발자들이 간과하는 부분이 바로 이 지점입니다.
인덱스는 공짜가 아닙니다.
유지 비용 발생
INSERT, UPDATE, DELETE 등 데이터가 변경될 때마다 데이터베이스는 인덱스 테이블도 함께 업데이트하고 정렬 상태를 유지해야 합니다.속도 저하
💻 참고
실무에서는 단순히
WHERE조건뿐만 아니라ORDER BY나GROUP BY가 자주 일어나는 컬럼에도 인덱스를 전략적으로 배치합니다.하지만 가장 중요한 건 주기적으로 사용되지 않는 인덱스를 제거하는 것입니다.
사용되지 않는 인덱스는 시스템의 쓰기 성능만 갉아먹는 '부채'와 같기 때문입니다.
하나의 큰 테이블에 모든 데이터를 담다 보면, 아무리 인덱스가 좋아도 서버 한 대가 감당해야 할 부하가 너무 커집니다.
이때 샤딩을 사용합니다.

동작 방식
데이터를 수평으로 잘라 각 조각(Shard)을 서로 다른 DB 서버에 분산시킵니다.
예를 들어, 사용자 ID 구간별로 별도의 샤드를 구성하는 방식입니다.
장점
핵심
샤딩이 서버를 나누는 것이라면, 파티셔닝은 집(DB)은 그대로 두고 방(Table)만 나누는 것입니다.

동작 방식
사용자 입장
핵심
파티셔닝은 하나의 DB 내에서의 '논리적인 분산'에 가깝습니다.
물론 파일 시스템 수준에서는 분리되지만, 샤딩처럼 서버 장비 자체가 나뉘는 것은 아닙니다.
샤딩과 파티셔닝의 가장 큰 차이는 무엇인가요?
구분 기준
근본적인 차이
인덱스의 두 얼굴 (Clustered vs Non-Clustered)
데이터 자체가 정렬된 사전(Clustered)과, 별도의 페이지 번호를 찾아가는 색인(Non-Clustered)의 차이를 명확히 해야 합니다.
특히 비클러스터형은 최종적으로 PK를 타고 실제 데이터로 가야 한다는 점(2단계 탐색)이 포인트입니다.
왜 B+Tree인가?
복합 인덱스의 황금률, '왼쪽부터'
확장 전략: 샤딩 vs 파티셔닝