데이터베이스 인덱싱(Indexing)의 원리와 최적화: 쿼리 속도의 비밀
백엔드 개발자 면접 단골 질문이자, 서비스 성능 튜닝의 첫 번째 단추인 데이터베이스 인덱싱(Indexing). 우리는 흔히 "느리면 인덱스 걸어"라고 말하지만, 인덱스가 내부적으로 어떻게 동작하는지, 왜 B-Tree를 쓰는지, 많이 걸면 어떤 부작용이 있는지 깊이 있게 이해하는 개발자는 많지 않습니다. 이 글에서는 인덱스의 자료구조부터 실행 계획(Explain) 분석, 커버링 인덱스까지 약 3,000자 분량으로 상세히 파헤칩니다.
1. 인덱스란 무엇인가? (책의 색인)
데이터베이스 테이블은 책의 본문과 같습니다. 인덱스는 책의 맨 뒤에 있는 **'색인'**입니다. 특정 단어를 찾고 싶을 때 책을 첫 페이지부터 한 장씩 넘기며 찾는 것(Full Table Scan)과, 색인에서 단어를 찾아 페이지 번호로 바로 이동하는 것(Index Seek)의 속도 차이는 데이터가 많을수록 어마어마해집니다.
2. 인덱스의 내부 구조: B-Tree (Balanced Tree)
대부분의 RDBMS(MySQL, PostgreSQL, Oracle)는 인덱스 관리를 위해 B-Tree 계열의 자료구조를 사용합니다. 왜 하필 B-Tree일까요? 해시 테이블(Hash Table)은 O(1)로 더 빠르지 않을까요?
2.1 범위 검색(Range Scan)의 효율성
해시 테이블은 = 연산(단건 조회)에는 매우 빠르지만, >, <, BETWEEN 같은 범위 검색이나 정렬(ORDER BY)에는 사용할 수 없습니다. 반면 B-Tree는 데이터가 항상 정렬된 상태로 유지되므로, 특정 범위를 스캔하는 데 매우 유리합니다.
2.2 균형(Balanced) 유지
B-Tree는 데이터가 삽입/삭제되어도 트리의 높이(Depth)를 일정하게 유지합니다. 이는 최악의 경우에도 검색 성능 O(log N)을 보장한다는 뜻입니다. 데이터가 100만 건이 있어도 트리 높이가 34 정도라면, 디스크 I/O를 34번만 수행하면 데이터를 찾을 수 있습니다.
3. Clustered Index vs Non-Clustered Index
이 두 가지 개념을 구분하는 것은 매우 중요합니다.
- Clustered Index: 데이터 자체를 정렬해서 저장합니다. 테이블당 오직 하나만 존재할 수 있으며, 일반적으로 Primary Key가 이 역할을 합니다. 리프 노드에 실제 데이터 페이지가 들어있습니다. 검색 속도가 가장 빠릅니다.
- Non-Clustered Index (Secondary Index): 데이터는 그대로 두고, 별도의 인덱스 페이지를 만듭니다. 테이블당 여러 개 만들 수 있습니다. 리프 노드에는 실제 데이터가 아니라, 데이터의 위치를 가리키는 주소값(혹은 PK)이 들어있습니다.
4. 인덱스의 양날의 검: 쓰기 성능 저하
"그럼 모든 컬럼에 인덱스를 걸면 빨라지겠네요?" 이는 초보자가 가장 많이 하는 실수입니다. 인덱스는 공짜가 아닙니다.
- 저장 공간: 인덱스도 테이블이므로 디스크 공간을 차지합니다.
- 쓰기 속도 저하: 데이터를 INSERT, UPDATE, DELETE 할 때마다, 인덱스 테이블도 같이 정렬하고 갱신해야 합니다. 인덱스가 많을수록 쓰기 작업은 기하급수적으로 느려집니다. 따라서 **"읽기(Read)와 쓰기(Write)의 비율"**을 고려하여 전략적으로 인덱스를 생성해야 합니다.
5. 최적화 기법: 커버링 인덱스(Covering Index)
쿼리를 튜닝하는 가장 강력한 기법 중 하나입니다. 쿼리에 필요한 모든 컬럼이 인덱스에 포함되어 있어, 실제 데이터 테이블을 찌를(Random Access) 필요 없이 인덱스 스캔만으로 결과를 반환하는 경우를 말합니다.
-- (id, name, email)로 구성된 인덱스가 있다고 가정
SELECT id, name FROM users WHERE email = 'test@example.com';
위 쿼리는 테이블에 접근하지 않고 인덱스 페이지만 읽고 끝납니다. 이로 인해 불필요한 디스크 I/O가 줄어들고 비약적인 성능 향상이 일어납니다.
6. 결론
인덱스는 마법이 아닙니다. 데이터의 분포도(Cardinality)가 좋을수록(중복이 적을수록) 효과가 좋습니다. 예를 들어 '성별' 컬럼처럼 '남/여' 두 가지 값만 있는 컬럼에 인덱스를 거는 것은 효과가 거의 없습니다. 항상 EXPLAIN 명령어로 쿼리의 실행 계획을 확인하고, 병목 지점을 찾아 인덱스를 적재적소에 배치하는 습관을 들여야 합니다.