423. 넌클러스터드 인덱스 (Non-Clustered Index)

⚠️ 이 문서는 한 테이블에 여러 개를 만들 수 있는 가장 일반적인 인덱스(책갈피) 형태인 '넌클러스터드 인덱스'의 작동 원리와, 인덱스를 탔는데도 테이블을 다시 뒤져야 하는 '포인터 참조(Key Lookup)' 구조를 다룹니다.

핵심 인사이트 (3줄 요약)

  1. 본질: 인덱스 트리(목차)와 실제 데이터(본문)가 물리적으로 완전히 분리되어 있는 구조다. 목차를 아무리 예쁘게 가나다순으로 정렬해도, 실제 데이터 파일의 위치나 순서는 1도 바뀌지 않는다.
  2. 가치: 한 테이블에 '이름 인덱스', '나이 인덱스' 등 여러 개의 목차를 만들 수 있어 다양한 검색 조건을 만족시킬 수 있다. (보통 한 테이블에 3~5개 정도 만든다.)
  3. 한계 (Key Lookup): 목차(인덱스)에서 원하는 값을 찾았다고 끝이 아니다. 목차에 적혀있는 '주소'를 보고 실제 데이터가 있는 디스크로 한 번 더 점프(Random Access)해야 하므로 클러스터드 인덱스보다 1단계 더 느리다.

Ⅰ. 개요: 책 뒤의 찾아보기 (Context & Necessity)

우리가 보는 전공서적 맨 뒤에는 보통 **'찾아보기(색인)'**가 있다.

  • "데이터베이스: 35페이지, 102페이지, 500페이지"

이 '찾아보기'가 전형적인 넌클러스터드 인덱스다. 찾아보기 종이 자체는 'ㄱㄴㄷ' 순서로 아주 예쁘게 정렬되어 있지만, 책의 본문 내용은 'ㄱㄴㄷ' 순서가 아니라 '1단원, 2단원' 시간순으로 되어 있다. 즉, 목차(인덱스)의 정렬 상태와 본문(데이터)의 정렬 상태가 완전히 **분리(Non-Clustered)**되어 있는 것이다.

📢 섹션 요약 비유: 넌클러스터드 인덱스는 **'지도 앱의 식당 즐겨찾기 목록'**과 같습니다. 내 폰 안의 즐겨찾기 목록은 '별점순'으로 예쁘게 정렬되어 있지만, 강남역에 있는 진짜 식당 건물의 위치가 별점순으로 재건축되거나 옮겨지는 건 아니죠. 식당 위치(데이터)는 그대로 둔 채, 목록(인덱스)만 따로 관리하는 방식입니다.


Ⅱ. 넌클러스터드 인덱스의 핵심 메커니즘 ★

인덱스의 리프 노드(422번 문서)에 도달했을 때 무슨 일이 벌어지는지가 핵심이다.

1. 리프 노드에는 무엇이 있는가?

  • 리프 노드에는 '진짜 데이터(예: 회원의 이름, 나이, 주소)'가 없다.
  • 대신 **'진짜 데이터가 있는 디스크의 주소 번지(ROWID / 주소 포인터)'**가 들어있다.

2. 추가적인 점프 (Key Lookup / Bookmark Lookup)

  • 인덱스를 타고 내려가서 "김철수"를 찾았다. (1단계)
  • 리프 노드를 보니 "김철수 데이터는 하드디스크 102번 방에 있음!"이라고 적혀있다.
  • 102번 방으로 **점프(Random Access)**해서 김철수의 전체 데이터를 꺼내온다. (2단계)

이 2단계 점프를 Key Lookup이라고 부르며, 넌클러스터드 인덱스가 느려지는 가장 큰 주범이다.


Ⅲ. 실무 팁: 커버링 인덱스 (Covering Index)

Key Lookup 때문에 쿼리가 느리다면, **"아예 점프를 안 하게 만들면 어떨까?"**라는 꼼수가 등장한다.

  • 쿼리: SELECT 나이 FROM 직원 WHERE 이름 = '김철수';
  • 기존 인덱스: [이름]으로만 만들면, '김철수'를 찾은 뒤 디스크로 점프해서 '나이'를 가져와야 한다.
  • 커버링 인덱스: 아예 인덱스를 [이름, 나이] 두 개를 합쳐서(복합 인덱스) 만들어버린다.
  • 결과: 인덱스를 타서 '김철수'를 찾는 순간, 그 옆에 '나이' 정보가 이미 붙어있다! 디스크로 점프할 필요 없이 바로 결과를 리턴한다. (속도 10배 상승)
┌──────────────────────────────────────────────────────────────┐
│           넌클러스터드 인덱스(Non-Clustered) 작동 구조 시각화            │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│ [ 📑 인덱스 트리 (이름 기준) ]        [ 💾 실제 데이터 블록 (입력순) ] │
│      ┌─────[김]─────┐                  │ 1번방: 박민수, 22세, 서울│  │
│      ▼              ▼                  │ 2번방: 이영희, 25세, 부산│  │
│ [김가루: 3번방]    [박민수: 1번방] ────▶ │ 3번방: 김가루, 20세, 대전│  │
│ [김철수: 4번방] ──┐[최보스: 5번방]        │ 4번방: 김철수, 30세, 제주│◀─┘│
│                 │                      │ 5번방: 최보스, 40세, 광주│  │
│                 └────────────────────▶                          │
│                                                              │
│ ★ 특징: 인덱스는 '김-박-최' 순서지만, 실제 데이터는 들어온 순서대로 박혀있다.│
└──────────────────────────────────────────────────────────────┘

Ⅳ. 결론

"넌클러스터드 인덱스는 비싼 지름길이다." 한테이블에 인덱스를 여러 개 달면 검색은 빨라질 수 있다. 하지만 데이터를 새로 넣거나 지울 때(INSERT, DELETE)마다 모든 인덱스 트리를 다 같이 수정해 줘야 하므로 쓰기 성능이 박살 나게 된다. 따라서 실무에서 넌클러스터드 인덱스는 보통 테이블당 3~4개를 넘지 않도록 신중하게(Selectivity가 높은, 즉 중복도가 낮은 컬럼에만) 생성해야 한다. 또한 Key Lookup 비용을 줄이기 위해 '커버링 인덱스' 설계 기법을 활용하는 것이 시니어 백엔드 개발자의 필수 소양이다.


📌 관련 개념 맵

  • 대척점 개념: Clustered Index (424번 문서 - 물리적 정렬을 수반하는 인덱스)
  • 내부 용어: Key Lookup (또는 Bookmark Lookup), ROWID, Secondary Index
  • 최적화 기법: Covering Index (커버링 인덱스 - 쿼리의 모든 컬럼이 인덱스 안에 다 있는 경우)
  • 생성 조건: 테이블 생성 시 UNIQUE 제약조건을 걸거나 CREATE INDEX를 치면 만들어짐.

👶 어린이를 위한 3줄 비유 설명

  1. 넌클러스터드 인덱스는 도서관에 있는 '도서 검색대 컴퓨터'와 같아요.
  2. 내가 검색대에 '해리포터'를 검색하면, 컴퓨터가 책을 주는 게 아니라 "그 책은 A구역 3층에 있어요"라는 '주소표(ROWID)'를 인쇄해 주죠.
  3. 나는 그 주소표를 들고 직접 A구역 3층까지 터벅터벅 걸어가서(Key Lookup 점프) 진짜 책을 꺼내와야 한답니다!