혹시 '데이터 베이스를' 아십니까 ? #2 – 인덱스

전하윤·2025년 7월 2일
0

DB

목록 보기
2/7
post-thumbnail

목차



데이터 베이스 쓰세요. 두번 쓰세요.

앞선 글에서는 스프레드시트보다 데이터베이스(DBMS)가 훨씬 강력한 이유(데이터 무결성, 보안, 백업, 커뮤니티 지원)를 다뤘다.

이제 “왜 실무에서 DBMS가 필수가 되었는가?”에 대해 어느 정도 감이 잡힐 것이다.
하지만 한 가지 더 중요한 궁금증이 있다.

단순히 “신뢰성”이나 “보안” 때문만이 아니라,
과연 데이터베이스(DBMS)가 수십만, 수백만, 수천만 건의 데이터를 다룰 때
스프레드시트(엑셀, 구글시트)와 성능 면에서도 정말 차이가 날까?

정답은 ‘그렇다’다.
특히 “빠른 데이터 검색”에서 DBMS는 더욱 강점을 갖는다.

이 모든 차이의 핵심에는 바로 ‘인덱스(색인)’라는 개념이 있다.


인덱스란 무엇인가?


출처: https://ittrue.tistory.com/331

인덱스(색인)란,
데이터 검색 속도를 극적으로 높이기 위해
별도로 만들어 놓는 자료구조다.

스프레드시트/CSV는 ‘위에서부터 쭉’ 전체를 훑는 방식(Full Scan)만 지원한다.
반면, DBMS는 인덱스를 활용해
필요한 데이터 위치를 단 몇 번의 비교만에 찾는다.

인덱스를 만들기 위한 대표 자료구조는
B-Tree, B+Tree, Hash테이블가 있다.

Hash 테이블 구조


출처: https://goodgid.github.io/Hash-Table/

해시 테이블은 데이터를 (Key, Value) 쌍으로 저장하는 자료구조다.
특정 Key를 입력하면, 해시 함수를 거쳐 그에 해당하는 위치(Index)를 바로 찾아내기 때문에
검색 속도가 O(1)로, 사실상 ‘즉시’ 데이터를 꺼낼 수 있다.

DB 인덱스에 해시 테이블이 도입될 때는
“컬럼 값(데이터)” → “데이터의 실제 위치”
이렇게 맵핑하는 방식으로 활용되는데,
특정 값을 가진 데이터가 데이터 파일의 어디에 저장되어 있는지
해시 테이블에 기록해 두고,
이후 해당 값으로 검색할 때 아주 빠르게 찾을 수 있다.

하지만 아쉽게도 데이터 베이스에서 해시 기반 인덱스는 잘 쓰이지 않는다.
바로 “동등(=) 비교”에만 특화되어 있기 때문이다.

해시 함수의 원리는
Key 값이 조금이라도 다르면 완전히 다른 해시값을 만들어내기 때문에
“값이 딱 일치하는 경우”에는 매우 빠르지만,
“범위 검색”이나 “LIKE ‘가나다%’”
처럼 부등호/와일드카드/범위 연산이 들어가면
해시 인덱스는 효율이 떨어지고,
결국 모든 데이터를 다 뒤져야 하기 때문이다.

이런 한계 때문에
DBMS에서는 실제로 범위 탐색, 정렬 등 다양한 쿼리가 자주 쓰이는 현실을 고려해
B+Tree 구조가 표준 인덱스로 선택 되었다.

그래서 DBMS는 B+Tree 구조를 주로 사용한다.


인덱스의 자료구조: B-Tree와 B+Tree

B-Tree 구조


출처: 코딩애플

  • B-tree는 이진트리와 거의 유사하지만 한 노드에 여러 개의 데이터를 담을 수 있어
    index 구현 시 유사한 데이터를 한 노드에 담아 탐색 횟수를 줄여 효율적으로 쓸 수 있다.

다만, 범위 검색을 할 때는 자식 노드의 범위를 넘어가면 다시 부모노드로 갔다가 다른 범위 노드로 가야 하기 때문에, 데이터를 찾는 과정이 비효율적인 경우가 있다


B+Tree 구조

  • B-tree에서 진화한 형태
  • 리프 노드끼리는 Linked List로 연결되어 있어서
    범위 검색도 효율적이다.
  • 데이터베이스 인덱스에 가장 널리 쓰이는 구조


출처: 코딩애플

  • 최하단 노드(리프 노드)에만 실제 데이터(혹은 데이터 주소)가 저장된다.
  • 그 외의 노드는 인덱스(가이드) 역할만 한다.

B+tree는 B-tree에서 최하단 노드(리프노드)에만 데이터를 보관한다.
그 외의 노드는 인덱스 노드라고 하며 데이터를 찾기 위한 가이드를 제공한다.
그리고, 리프노드 사이는 LinkedList로 연결한다.
이를 통해 범위 검색을 효율적으로 할 수 있다.


클러스터형 인덱스(Clustered Index)

  • 표로 만든 예시

  • 트리 형태로 바꿔본 예시

클러스터형 인덱스 핵심 요약

  • 영어 사전처럼 책 전체가 정렬되어 있는 구조 (PK 순서)
  • 기본키로 지정된 컬럼에 자동으로 생성되기 때문에 테이블당 하나만 생성 할 수 있다.
  • 생성될 때 클러스터 인덱스를 기준으로 데이터가 항상 정렬
  • 클러스터형 인덱스는 내부적으로 B+Tree 구조로 만들어진다.
  • 인덱스(상위 노드)는 참조 범위의 첫 번째 값,
    오른쪽은 그 범위의 데이터가 있는 페이지 주소
  • 검색할 때는 인덱스 노드를 따라가
    리프 노드(=데이터 페이지)에서 실제 데이터를 바로 찾는다.
  • 데이터가 추가될 때마다 데이터 페이지/루트 페이지 모두 정렬됨 (정렬 2번)
  • 리프 페이지 = 데이터 페이지
    (리프 페이지가 곧 데이터 그 자체!)

클러스터 인덱스 생성/변화 예시

처음에는 테이블에 데이터가 아무런 순서 없이 저장되어 있다.
하지만 클러스터 인덱스를 생성하는 순간, DBMS는 인덱스 기준이 되는 컬럼(PK 등)으로
모든 데이터를 자동으로 정렬한다. 이 과정에서 기존에 흩어져 있던 데이터들이
인덱스 기준에 따라 재배치되며, 여러 데이터 페이지로 나뉜다.

각 데이터 페이지는 정렬되고,
페이지마다 그 범위의 첫 번째 값을 대표로 가진다.
그리고 이 대표 값들과 페이지의 주소 정보가 별도의 루트 페이지(=인덱스 페이지)
정리되어 저장된다.


클러스터 인덱스 조회(단일)

  • 단일 검색:특정 값(PK 등)을 찾을 때는
    인덱스를 따라가서 해당 데이터가 들어 있는
    데이터 페이지(리프 노드)에 곧바로 접근할 수 있다.

클러스터 인덱스 조회(범위)

  • 범위 검색: 예를 들어, PK 1000~2000 구간의 데이터를 찾는다면
    인덱스에서 시작 위치만 빠르게 찾고,
    이후는 Linked List(페이지가 쭉 연결된 구조)로
    한 번에 이어서 데이터를 순차적으로 읽어올 수 있다.

클러스터 인덱스를 이용한 데이터 삽입

데이터를 삽입할 때는
단순히 마지막에 추가되는 게 아니라,
인덱스 정렬 기준에 따라 자동으로 위치가 조정된다.

새로운 데이터가 들어오면,

해당 데이터가 들어가야 할 위치를 찾아서 삽입,

만약 한 페이지가 꽉 차면,
페이지 분할이 일어나고,

인덱스(루트/상위 노드) 정보도 같이 조정된다.

즉, 데이터 정렬을 계속 유지해야 하므로
INSERT/UPDATE의 비용이 다소 늘어날 수 있다.


보조 인덱스(Secondary Index, Non-Clustered Index)

보조 인덱스 특징

  • 책의 찾아보기(색인)처럼
    인덱스 키 값 → 실제 데이터 주소(ROW POINTER or PK)만 보유한다.
  • 한 테이블에 여러 개 생성 가능
  • 실제 데이터는 클러스터 인덱스(PK) 순서로만 저장
  • 데이터의 추가/삭제/수정 시에는 인덱스만 갱신하고
    실제 테이블 데이터의 정렬에는 영향을 주지 않는다.
  • 리프 페이지는 데이터페이지의 값을 바로 참조한다.
  • 데이터페이지와 리프페이지가 나뉘어 있다.
  • 데이터는 테이블에 순서 없이 추가되지만,
  • 리프 페이지와 데이터 페이지는 서로 완전히 분리되어 있다.
    (리프 페이지에는 인덱스 값/주소만, 데이터 페이지에는 실제 레코드 전체)
  • 리프 페이지에서는 데이터 페이지의 주소를 정렬된 상태로 관리 (정렬 1번)

클러스터 인덱스 + 보조 인덱스 혼합 동작

실제로 쿼리문을 사용하면
대부분 두 인덱스를 혼합해서 쓰게 된다.

SELECT *
FROM 학생
WHERE 이름 = '홍길동';
  • 기존의 표로 예시

동작 절차
1. 보조 인덱스(B+Tree, 이름 인덱스)에서 이름을 찾는다
2. 해당 이름에 연결된 학번(Primary Key, row pointer)을 찾는다
3. 클러스터형 인덱스(Primary Index, 학번 인덱스)에서 실제 레코드를 찾는다
4. 실제 데이터(행 전체)를 읽어 결과로 반환한다

참고: 보조 인덱스는 데이터의 주소값을 갖고 있다.
클러스터형 인덱스에 데이터가 늘어나면 보조 인덱스 구조도 계속 바뀐다.
인덱스를 혼합해서 쓸 때에는 보조 인덱스의 구조가 바뀌어야 할 수 있고,
데이터 검색 시 손해를 볼 수도 있지만
데이터를 수정/삭제할 때 효율적으로 관리할 수 있다.

모두 제거해야 할 때 보조 인덱스부터 삭제해야 하는 이유:
보조 인덱스는 클러스터형 인덱스를 참조하는 구조이므로
클러스터형 인덱스를 먼저 지우면
보조 인덱스의 구조가 깨진다.


클러스터 인덱스와 보조 인덱스, 한눈에 비교


인덱스의 성능과 고려사항

  • 검색 속도
    인덱스 덕분에 수십~수억 건 데이터에서도 logN의 속도로 매우 빠르게 원하는 레코드를 찾을 수 있다.
  • 범위 검색/정렬/집계에도 매우 효율적이다.
  • 데이터 무결성, 검색 최적화 등 실무에서는 필수다.

그러나 단점도 있다! (인덱스 유형별 성능 차이)

  • 인덱스가 많아질수록

    • INSERT/UPDATE/DELETE(쓰기 연산) 시 모든 인덱스를 함께 갱신해야 하므로 쓰기 성능이 저하된다.
    • 클러스터 인덱스(Primary/PK 기반)의 경우
      데이터 자체가 인덱스 구조에 맞게 정렬되어 저장되기 때문에
      삽입/삭제/수정 시 더 많은 비용이 든다.
    • 보조 인덱스(Secondary/Non-Clustered)
      실제 데이터(테이블)와 별도로 인덱스만 정렬하므로
      데이터는 추가/삭제가 빠르지만,
      인덱스 갱신(정렬, 포인터 변경) 오버헤드는 여전히 존재한다.
  • 조회(SELECT) 성능은 반대로

    • 클러스터 인덱스
      인덱스를 타고 곧장 실제 데이터 페이지(리프 노드)까지 바로 접근
      조회가 아주 빠르다 (특히 PK/순차적 범위 조회)
    • 보조 인덱스
      인덱스에서 PK(ROW_POINTER)만 찾고
      한 번 더 PK(클러스터 인덱스)를 타고 데이터를 읽으러 가야 해서
      조금 더 느릴 수 있다 (특히 랜덤/비순차 조회)
  • 인덱스 자체가 별도의 파일/디스크 공간을 차지
    → 저장 공간 관리 필요

  • 불필요한 인덱스를 많이 만들면 오히려 성능이 저하
    정기적으로 인덱스 점검, 적정 개수 유지 필요

실무에서는 이렇게 활용한다!

  • 쓰기(INSERT/UPDATE/DELETE)가 아주 잦은 테이블
    불필요한 인덱스를 최소화 (특히 보조 인덱스 남발 X)
  • 읽기(SELECT/조회)가 압도적으로 많은 테이블
    클러스터 인덱스, 필요한 보조 인덱스 적극 활용
  • 인덱스 컬럼은 WHERE/조인/정렬/그룹핑 등에서
    자주 쓰이는 컬럼에만 설정
  • Composite Index(복합 인덱스)도 효율적이나,
    “왼쪽부터 연속적으로 조건”이 붙을 때만 100% 효과가 있으니
    조건 순서/사용 패턴을 고려해서 설계해야 한다

마치며

인덱스는 단순히 성능 향상을 넘어, 대규모 데이터베이스 운영에서 필수적인 요소임을 새삼 느꼈습니다.
이번 글을 준비하면서 저 역시 “인덱스가 실제로 어느 정도 성능 차이를 만들어낼까?”가 궁금해졌는데요.

실제로는 단순히 인덱스를 건다고 무조건 빠른 건 아니고,
테이블 구조·데이터 양·쿼리 패턴에 따라 인덱스 효과가 천차만별이라고 합니다.

그래서 직접 샘플 데이터를 mysql 공홈에서 받아서
인덱스의 유무, 인덱스 종류(B+Tree, 해시 등),
여러 쿼리로 실험해보고 싶습니다.
가능하면 진행하고 벨로그에 올리겠습니닷. (가능하면..)


참고 레퍼런스

profile
개발에 대한 고민과 성장의 기록을 일기장처럼 성찰하며 남기는 공간

0개의 댓글