DB 쿼리 속도 100배 향상

📌 이 글의 핵심 내용

  • 인덱스의 내부 자료구조 B-Tree의 작동 원리와 시간 복잡도
  • Clustered IndexNon-Clustered Index의 결정적 차이
  • 인덱스를 걸어도 효과가 없는 경우 (카디널리티의 중요성)
  • 복합 인덱스 설계 시 컬럼 순서 결정 법칙 (Leftmost Prefix)
  • 테이블 조회 없이 인덱스만으로 끝내는 커버링 인덱스(Covering Index)

서비스 초기에는 사용자가 적어 쿼리 튜닝의 필요성을 느끼지 못합니다. 하지만 데이터가 수십만, 수백만 건으로 늘어나는 순간, 0.1초 걸리던 조회 쿼리가 3초, 10초로 느려지며 서비스 전체의 병목(Bottleneck)이 됩니다. 이때 가장 먼저 살펴봐야 할 것이 바로 인덱스(Index)입니다.

하지만 무턱대고 모든 컬럼에 인덱스를 걸면 오히려 쓰기(Insert/Update/Delete) 성능이 심각하게 저하됩니다. 인덱스는 공짜가 아닙니다. 정확한 원리를 알고 필요한 곳에만 전략적으로 사용해야 합니다.

데이터베이스 B-트리

1. 인덱스의 기본 원리: B-Tree

데이터베이스 테이블은 기본적으로 데이터가 들어온 순서대로 저장됩니다(Heap Table). 책으로 치면 페이지 번호 없이 내용만 무작위로 적힌 공책과 같습니다. 여기서 특정 단어를 찾으려면 첫 페이지부터 끝까지 다 읽어야 합니다. 이를 Full Table Scan이라 하며, 시간 복잡도는 O(N)입니다.

인덱스를 생성하면 DB는 데이터를 정렬하여 별도의 자료구조인 B-Tree(Balanced Tree)를 구성합니다.

💡 B-Tree의 핵심 특징

루트(Root)에서 리프(Leaf) 노드까지의 거리가 항상 일정하여, 데이터 양이 늘어나도 검색 성능이 급격히 떨어지지 않습니다. 탐색 시간 복잡도는 O(log N)입니다. 데이터가 100만 건일 때, Full Scan은 100만 번 비교해야 하지만, B-Tree는 약 20번의 비교만으로 데이터를 찾을 수 있습니다.

2. Clustered vs Non-Clustered Index

MySQL(InnoDB) 기준으로 인덱스는 크게 두 가지로 나뉩니다. 이 둘의 차이를 아는 것이 튜닝의 첫걸음입니다.

① Clustered Index (군집 인덱스)

  • 책의 페이지 번호와 같습니다. 데이터 자체가 인덱스 순서대로 물리적으로 정렬되어 저장됩니다.
  • 테이블당 단 하나만 존재할 수 있습니다. (물리적 정렬은 하나만 가능하니까요)
  • 주로 Primary Key(PK)가 이 역할을 합니다.
  • 리프 노드에 실제 데이터(모든 컬럼)가 저장되어 있어 조회 속도가 가장 빠릅니다.

② Non-Clustered Index (보조 인덱스)

  • 책의 맨 뒤에 있는 '찾아보기(색인)'와 같습니다.
  • 테이블당 여러 개를 만들 수 있습니다.
  • 리프 노드에는 실제 데이터가 아니라, 데이터의 위치(PK 값)가 저장되어 있습니다.
  • 따라서 데이터를 찾으려면 인덱스 검색 → PK 획득 → PK로 다시 실제 테이블 조회라는 두 번의 과정을 거칩니다.

3. 인덱스를 걸어도 느린 이유: 카디널리티(Cardinality)

"모든 WHERE 조건 컬럼에 인덱스를 걸면 빨라질까요?" 절대 아닙니다. 인덱스 효율의 핵심 지표는 카디널리티(기수성)입니다. 카디널리티란 '데이터의 중복 수치'를 의미합니다.

컬럼 예시 중복도 카디널리티 인덱스 효율
주민번호, ID 없음 (Unique) 매우 높음 최상 👍
이름, 생일 가끔 중복 보통 좋음
성별 (남/여) 50% 중복 매우 낮음 최악 👎

예를 들어, 성별 컬럼에 인덱스를 걸고 '남'을 조회하면 전체 데이터의 50%를 가져와야 합니다. DB 옵티마이저는 "이럴 바엔 그냥 인덱스 안 타고 전체를 훑는 게 더 빠르겠다"라고 판단하여 인덱스를 무시(Full Scan)해 버립니다. 즉, 인덱스를 건 의미가 사라집니다.

따라서 전체 데이터의 10~15% 이하를 걸러낼 수 있는 컬럼에만 인덱스를 거는 것이 유리합니다.

4. 복합 인덱스(Composite Index) 설계 공식

실무에서는 하나의 컬럼으로 조회하기보다 WHERE A = ? AND B = ? 처럼 여러 조건을 조합하는 경우가 많습니다. 이때 사용하는 것이 복합 인덱스입니다. 복합 인덱스는 순서가 생명입니다.

공식 1: Leftmost Prefix Rule (좌측 접두사 규칙)

인덱스를 (A, B, C) 순서로 생성했다고 가정합시다.

-- 인덱스 사용 가능 (O) WHERE A = ? WHERE A = ? AND B = ? WHERE A = ? AND B = ? AND C = ? -- 인덱스 사용 불가 (X) - A가 없어서 시작점을 못 찾음 WHERE B = ? WHERE C = ? WHERE B = ? AND C = ? -- 일부만 사용 가능 (△) - A만 인덱스를 타고 C는 필터링됨 WHERE A = ? AND C = ?

공식 2: 카디널리티가 높은 순서대로

여러 컬럼 중 중복이 적은(더 많이 걸러내는) 컬럼을 앞쪽에 배치하는 것이 성능상 유리합니다. 예를 들어 지역(서울/부산...)주민번호가 있다면, 당연히 (주민번호, 지역) 순으로 거는 것이 압도적으로 빠릅니다.

5. 고급 기법: 커버링 인덱스 (Covering Index)

인덱스 튜닝의 꽃입니다. 쿼리에 필요한 모든 컬럼이 인덱스에 포함되어 있어서, 실제 데이터 테이블을 찌르지(Random Access) 않고 인덱스만 읽고 끝내는 방식입니다.

-- 인덱스: (email, username) -- 커버링 인덱스 성공! (인덱스에 email과 username이 다 있음) SELECT email, username FROM users WHERE email = 'test@example.com'; -- 커버링 인덱스 실패 (address는 인덱스에 없음 -> 실제 테이블 조회 발생) SELECT email, address FROM users WHERE email = 'test@example.com';

페이징 쿼리 등에서 성능을 극한으로 끌어올려야 할 때, 일부러 조회하려는 컬럼들을 모두 포함하는 복합 인덱스를 만들기도 합니다.

6. 주의사항: 인덱스를 망치는 쿼리 패턴

아무리 인덱스를 잘 걸어도 쿼리를 잘못 짜면 무용지물입니다.

  • 가공된 컬럼: WHERE YEAR(date) = 2024 처럼 컬럼을 함수로 감싸면 인덱스가 깨집니다. WHERE date >= '2024-01-01'로 써야 합니다.
  • 부정 연산: !=, NOT IN 등은 인덱스를 타지 못할 확률이 높습니다.
  • LIKE 앞쪽 와일드카드: LIKE '%abc'는 시작점을 알 수 없어 Full Scan을 합니다. LIKE 'abc%'는 인덱스를 탑니다.
  • 데이터 타입 불일치: 문자열 컬럼을 숫자로 조회하면 내부 형변환이 일어나 인덱스가 깨집니다.

7. 결론

인덱스는 조회 속도를 높여주는 강력한 무기지만, 쓰기 성능과 저장 공간(디스크)을 담보로 하는 거래입니다. 무조건 많이 거는 것이 능사가 아니며, EXPLAIN 명령어를 통해 쿼리 실행 계획을 확인하는 습관을 들여야 합니다.

데이터베이스 성능 최적화는 "디스크 I/O를 얼마나 줄이느냐"의 싸움임을 기억하십시오.

이 블로그의 인기 게시물

Docker 컨테이너 'Connection Refused' (Errno 111) 오류 해결 가이드

Redis 캐싱 전략 완벽 가이드: Look Aside부터 Write Back까지 (DB 부하 줄이기)

브라우저 렌더링 원리: Reflow와 Repaint 최적화 가이드 (CRP 심층 분석)