콘텐츠로 이동

인덱스

Real MySQL 8.0 - 08. 인덱스

created: 2026.1.14

updated: 2026.1.26


디스크 읽기 방식

  • 데이터베이스의 성능 튜닝: 디스크 I/O 줄이기
  • 초당 처리 횟수: CPU > DRAM > SSD > HDD
  • 데이터베이스의 작업은 대부분 랜덤 I/O
  • 랜덤 I/O를 줄이는 것: 쿼리를 처리하는 데 꼭 필요한 데이터만 읽도록 쿼리 개선

인덱스 레인지 스캔: 랜덤 I/O

풀 테이블 스캔: 순차 I/O -> 데이터 웨어하우스나 통계 작업에 주로 사용

인덱스

  • 컬럼과 해당 레코드가 저장된 주소를 키와 값의 쌍(key-value pair)으로 삼아 인덱스 생성
  • 컬럼의 값을 주어진 순서로 미리 정렬하여 보관
  • SortedList: 인덱스와 같은 자료구조
    • 값을 항상 정렬된 상태로 유지
    • 저장될 때마다 정렬이 필요해 저장하는 과정이 복잡하고 느림
    • 원하는 값을 빠르게 찾을 수 있음
  • ArrayList: 데이터파일과 같은 자료구조
    • 값을 저장되는 순서대로 정렬 없이 유지
  • 인덱스는 저장 성능을 희생하고 데이터의 읽기 속도를 높이는 기능
  • 프라이머리 키(primary key)
    • 레코드를 대표하는 칼럼의 값으로 만들어진 인덱스, 식별자
    • not null, unique
  • 보조 키(secondary key)
    • pk를 제외한 모든 인덱스
    • 유니크 인덱스는 대체 키라고도 함
  • 데이터 저장 방식(알고리즘): B-Tree, Hash, Fractal-Tree, Merge-Tree
  • Hash
    • 컬럼의 값으로 해시값을 계산해서 인덱싱
    • 빠른 검색 지원
    • 값을 변형해서 인덱싱하여 prefix 일치와 값이 값의 일부만 검색하거나 범위를 검색할 때 사용 불가
    • 메모리 기반 데이터베이스에 주로 사용
  • 데이터 중복 허용 여부: unique index, non-unique index

B-Tree 인덱스

  • B+-Tree, B*-Tree 주로 사용
  • B: Balanced 의미
  • 가장 일반적으로 사용하며 컬럼의 원래 값을 변형하지 않고 인덱스 구조체 내에서 항상 정렬된 상태로 유지

구조

  • 트리 구조로 최상위에 루트 노드(Root node)가 존재하고 하위에 자식 노드가 붙어 있는 형태
  • 리프 노드(leaf node): 가장 하위에 있는 노드 - 실제 데이터 레코드의 주솟값을 가짐
  • 브랜치 노드(Branch node): 중간 노드

InnoDB 테이블에서 레코드는 클러스터되어 디스크에 저장되므로 프라이머리 키 순서로 정렬되어 저장. 디폴트로 클러스터링 테이블이 생성됨

클러스터링: 비슷한 값을 최대한 모아서 저장하는 방식

  • InnoDB 테이블에서는 프라이머리 키가 ROWID 역할
  • MyISAM - 물리적인 주소, InnoDB - 논리적인 주소

인덱스 키 추가/삭제

  • 추가: B-Tree상 저장 위치 검색 -> 레코드 키 값과 대상 레코드 주소 정보를 리프 노드에 저장 -> 리프 노드가 꽉 차면 분리
  • 삭제: 해당 키 값이 저장된 B-Tree의 리프 노드를 찾아 삭제 마크 - 마킹된 공간은 방치하거나 재활용
  • 변경: 키 값을 삭제한 후 새로운 키 값을 추가
  • 체인지 버퍼를 활용해 지연 처리 가능
  • 검색: 루트 노드 -> 브랜치 노드 -> 리프 노드까지 이동하며 비교 - 트리 탐색
  • 인덱스 키 값을 변형하여 비교되는 경우 B-Tree 빠른 검색이 불가능
  • update/delete 문을 사용할 때 인덱스를 잘 활용해야 모든 레코드가 잠기는 것을 막을 수 있음

영향을 미치는 요소

  • 인덱스 키 값의 크기
    • 페이지 단위로 관리
    • B-Tree는 자식 노드의 개수가 가변적 -> 인덱스의 페이지 크기와 키 값의 크기에 따라 결정
    • 인덱스를 구성하는 키 값의 크기가 커지면 디스크 읽기 횟수 증가 -> 느려짐
  • B-Tree 깊이(Depth)
    • 인덱스 키 값의 크기가 커질수록 페이지가 담을 수 있는 인덱스 키 값의 개수가 감소 -> Depth가 깊어져 디스크 읽기 증가
  • 선택도(Selectivity), 기수성(Cardinality)
    • 모든 인덱스 키 값 가운데 유니크한 값의 수
    • 인덱스에서 유니크한 값의 개수는 쿼리의 효율성에 영향
  • 레코드 건수
    • 인덱스를 이용한 읽기의 손익 분기점 판단 필요
    • 인덱스를 사용해 읽을 레코드의 건수가 전체 테이블 레코드의 20%~25%를 넘으면 테이블을 직접 읽어 필터링 방식으로 처리하는 것이 효율적

페이지(Page)/블록(Block)

  • 디스크에 데이터를 저장하는 가장 기본 단위

  • 디스크의 모든 읽기 및 쓰기 작업의 최소 작업 단위

  • 버퍼 풀에서 데이터를 버퍼링하는 기본 단위

B-Tree 인덱스로 데이터 읽기

  • 인덱스 레인지 스캔
    • 인덱스 접근 방법 중 가장 대표적인 방식 - 빠름
    • 검색해야 할 인덱스의 범위가 결정됐을 때 사용
    • 검색하려는 값의 수나 검색 결과 레코드 건수와 관계없이 레인지 스캔이라 표현
    • 루트 -> 브랜치 -> 리프 순으로 레코드의 시작 지점을 찾아 리프 노드의 레코드를 순서대로 스캔
    • 인덱스 자체의 정렬로 인덱스를 구성하는 컬럼의 정렬된 상태 레코드 조회

인덱스 레인지 스캔의 3단계 1. 인덱스에서 조건을 만족하는 값이 저장된 위치를 찾음(인덱스 탐색 index seek)
2. 탐색된 위치부터 필요한 만큼 인덱스를 차례대로 읽음(인덱스 스캔 index scan)
3. 스캔한 인덱스 키와 레코드 주소를 이용해 레코드가 저장된 페이지를 가져오고 최종 레코드 조회

  • 인덱스 풀 스캔
    • 인덱스의 처음부터 끝까지 모두 읽는 방식
    • 쿼리의 조건절에 사용된 컬럼이 인덱스의 첫 번째 컬럼이 아닌 경우
    • 쿼리가 인덱스에 명시된 컬럼만으로 조건을 처리할 수 있는 경우
    • 리프 노드의 맨 앞 혹은 맨 뒤에서부터 처음부터 끝까지 스캔
    • 테이블 풀 스캔보다 효율적
    • 비효율적인 인덱스 사용 방식
  • 루스 인덱스 스캔
    • 느슨하게 또는 듬성듬성하게 인덱스를 읽는 것
    • 인덱스 레인지 스캔과 비슷하지만 중간에 필요하지 않은 인덱스 키 값은 무시하고 다음으로 넘어가는 형태
    • GROUP BY 또는 집합 함수 MAX(), MIN() 함수에 대해 최적화 하는 경우 사용
    • 그룹별 첫 번째 레코드의 값만 읽고 이동
  • 인덱스 스킵 스캔

    • 선행 컬럼 없이도 후행 컬럼으로 인덱스 사용 가능 하게 함
      -- 인덱스 생성
      ALTER TABLE employees
        ADD INDEX ix_gender_birthdate (gender, birth_date);
      
      -- birth_date만 사용
      SELECT gender, birth_date
        FROM employees
       WHERE birth_date >= '1990-10-10';
      
      -- 인덱스 스킵 스캔의 실제 쿼리
      SELECT gender, birth_date FROM emplyees WHERE gender = 'M' AND birth_date >= '1990-10-10';
      SELECT gender, birth_date FROM emplyees WHERE gender = 'F' AND birth_date >= '1990-10-10';
      
  • 선행 컬럼이 어떤 타입이든 루스 인덱스 스캔과 동일한 방식으로 읽어 인덱스에 존재하는 모든 값을 먼저 추출

  • WHERE 조건절에 조건이 없는 인덱스의 선행 컬럼의 유니크한 값이 개수가 적어야 함
  • 쿼리가 인덱스에 존재하는 컬럼만으로 처리가 가능해야 함(커버링 인덱스)