DB 인덱스 — 1억 건에서 0.01초 만에 찾는 법
이 토픽을 마치면
인덱스가 왜 빠른지 원리를 설명할 수 있고, 언제 인덱스를 걸어야 하는지 판단할 수 있게 됩니다.
1부터 100까지 숫자 맞추기
게임을 하나 해봅시다. 상대방이 1~100 중에 숫자 하나를 골랐습니다. 맞춰야 합니다.
방법 A: "1이야?" "2야?" "3이야?" — 하나씩 물어보기. 최악의 경우 100번 물어봐야 합니다.
방법 B: "50보다 커?" → "75보다 커?" → "62보다 커?" — 절반씩 줄이기. 최대 7번이면 됩니다.
데이터베이스에서 인덱스가 없으면 방법 A, 인덱스가 있으면 방법 B입니다. 데이터가 100건이면 차이를 못 느끼지만, 1억 건이면 1억 번 vs 27번입니다.
인덱스 없이 검색하면
SELECT * FROM users WHERE age = 20;인덱스가 없으면 데이터베이스는 users 테이블의 모든 행을 하나씩 확인합니다. 이걸 Full Scan이라고 합니다. 1억 건이면 1억 개를 다 뒤져야 합니다.
인덱스를 걸면
CREATE INDEX idx_age ON users(age);이 한 줄을 실행하면, 데이터베이스가 age 컬럼의 값을 미리 정렬해서 별도 구조로 저장합니다. 그다음부터 WHERE age = 20을 실행하면 정렬된 데이터에서 절반씩 소거하며 찾습니다. 1억 건이어도 27번만 비교하면 됩니다.
B+ Tree — 실제로 쓰이는 구조
"절반씩 소거"의 기본 아이디어는 이진 탐색입니다. 근데 실제 데이터베이스는 이진 탐색 트리를 그대로 쓰지 않고, B+ Tree라는 구조를 씁니다.
B-Tree: 노드 하나에 데이터를 여러 개 담습니다. 이진 탐색 트리가 매번 반으로 갈랐다면, B-Tree는 3분의 1, 4분의 1씩 갈라냅니다. 더 빠릅니다.
B+ Tree: B-Tree에서 한 단계 더 진화한 겁니다.
- 실제 데이터는 맨 아래(리프 노드)에만 저장
- 위쪽 노드는 "어디로 가야 하는지" 안내판 역할만
- 리프 노드끼리 서로 연결되어 있어서 범위 검색이 빠름
"age 20~30인 사용자 찾아줘" 같은 범위 검색에서
B+ Tree는 20을 찾은 다음, 연결된 리프를 따라 30까지 쭉 읽습니다.현대 데이터베이스(MySQL, PostgreSQL)의 기본 인덱스가 B+ Tree입니다.
인덱스의 대가
공짜는 아닙니다. 트레이드오프가 있습니다:
저장 공간: 인덱스 = 정렬된 데이터의 복사본. 인덱스를 많이 걸수록 DB 용량이 늘어납니다.
쓰기 성능: 데이터를 INSERT하거나 UPDATE하면 인덱스도 같이 갱신해야 합니다. 인덱스가 10개면 데이터 하나 넣을 때 10개를 다 손봐야 합니다.
그래서 "읽기가 많은 컬럼"에 인덱스를 걸고, "쓰기가 많은 테이블"에는 인덱스를 남발하지 않습니다.
한 가지 알아둘 점 — Primary Key는 자동으로 인덱스가 걸려 있습니다. 이걸 Clustered Index라고 합니다. PK로 검색하면 따로 인덱스를 만들지 않아도 이미 빠릅니다.
인덱스를 걸지 말아야 할 때
인덱스가 오히려 해가 되는 경우도 있습니다:
- 데이터가 적을 때 — 100건짜리 테이블에 인덱스를 걸면 Full Scan보다 느릴 수 있습니다. 정렬된 구조를 탐색하는 것보다 100건 쭉 읽는 게 더 빠릅니다.
- 값의 종류가 적을 때 —
gender컬럼처럼 값이 "M" 아니면 "F"인 컬럼은 인덱스를 걸어도 효과가 없습니다. 어차피 절반을 읽어야 하니까요. 이걸 "카디널리티(Cardinality)가 낮다"고 합니다. - INSERT가 대부분인 테이블 — 로그 테이블처럼 계속 쌓기만 하고 거의 안 읽는 테이블은 인덱스가 쓰기 성능만 깎아먹습니다.
면접에서 "인덱스를 언제 거냐"보다 **"인덱스를 언제 걸지 말아야 하냐"**를 물어보는 경우가 더 많습니다. 읽기가 잦고, 카디널리티가 높고, WHERE 절에 자주 등장하는 컬럼 — 이 세 조건에 해당할 때 거세요.
핵심
인덱스는 데이터를 미리 정렬해놓은 목차입니다. 없으면 1억 건을 전부 뒤지고(Full Scan), 있으면 27번 비교로 끝납니다. 대신 저장 공간을 먹고, 쓰기가 느려집니다. 읽기가 잦은 컬럼에만 거세요.