📚FOS Study
홈카테고리
홈카테고리

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • agents 페이지로 이동
    • BMAD Method — AI 에이전트로 애자일 개발하는 방법론
    • Claude Code의 Skill 시스템 - 개발자를 위한 AI 자동화의 새로운 차원
    • Claude Code 멀티 에이전트 — Teams
    • 멀티모달 LLM (Multimodal Large Language Model)
  • architecture 페이지로 이동
    • 캐시 설계 전략 총정리
    • 디자인 패턴
    • 분산 트랜잭션
  • css 페이지로 이동
    • FlexBox 페이지로 이동
  • database 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • redis 페이지로 이동
    • 김영한의-실전-데이터베이스-설계 페이지로 이동
    • 커넥션 풀 크기는 얼마나 조정해야할까?
    • 인덱스 - DB 성능 최적화의 핵심
    • 역정규화 (Denormalization)
    • 데이터 베이스 정규화
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • k8s-in-action 페이지로 이동
    • monitoring 페이지로 이동
  • go 페이지로 이동
    • Go 언어 기본 학습
  • http 페이지로 이동
    • HTTP Connection Pool
  • interview 페이지로 이동
    • 210812 페이지로 이동
    • 뱅크샐러드 AI Native Server Engineer
    • CJ 올리브영 지원 문항
    • CJ 올리브영 커머스플랫폼유닛 Back-End 개발 지원 자료
    • 마이리얼트립 - Platform Solutions실 회원주문개발 Product Engineer
    • NHN 서비스개발센터 AI서비스개발팀
    • nhn gameenvil console backend 직무 인터뷰 준비
    • 면접을 대비해봅시다
    • Tossplace Node.js Developer
    • 토스플레이스 Node.js 백엔드 컬처핏
  • java 페이지로 이동
    • jdbc 페이지로 이동
    • opentelemetry 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • 더_자바_코드를_조작하는_다양한_방법 페이지로 이동
    • Java의 로깅 환경
    • MDC (Mapped Diagnostic Context)
    • OpenTelemetry 란 무엇인가?
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • Data_Structures_and_Algorithms 페이지로 이동
    • Heap 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node.js
    • npm vs pnpm 선택기준은 무엇인가요?
    • `setImmediate()`
  • kafka 페이지로 이동
    • Kafka 기본
    • Kafka를 사용하여 **데이터 정합성**은 어떻게 유지해야 할까?
    • 메시지 전송 신뢰성
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • tmux — Terminal Multiplexer
  • network 페이지로 이동
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • react 페이지로 이동
    • JSX 페이지로 이동
    • VirtualDOM 페이지로 이동
    • v16 페이지로 이동
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • the-future-company 페이지로 이동
📚FOS Study

개발 학습 기록을 정리하는 블로그입니다.

바로가기

  • 홈
  • 카테고리

소셜

  • GitHub
  • Source Repository

© 2025 FOS Study. Built with Next.js & Tailwind CSS

목록으로 돌아가기
🗄️database/ mysql

B-Tree 인덱스

약 4분
2026년 3월 24일
GitHub에서 보기

B-Tree 인덱스

InnoDB가 인덱스를 어떻게 저장하고 탐색하는지, 개발자 입장에서 알아야 할 B-Tree 구조를 정리했다.


B-Tree란

Balanced Tree의 약자다. 모든 리프 노드가 같은 깊이에 위치해서 어떤 키를 검색해도 탐색 비용이 일정하다.

데이터베이스 인덱스에 B-Tree(정확히는 B+Tree)를 쓰는 이유는 두 가지다.

  • 범위 검색: 리프 노드가 링크드 리스트로 연결되어 있어서 BETWEEN, >, < 같은 범위 조건에 효율적이다
  • 디스크 I/O 최적화: 하나의 노드가 여러 키를 담아서 트리 깊이가 낮고, 한 번의 디스크 읽기(페이지)로 많은 키를 가져올 수 있다

MySQL InnoDB는 정확히는 B+Tree를 쓴다. B-Tree와 달리 내부 노드에는 키만 있고, 실제 데이터(또는 포인터)는 리프 노드에만 있다. 그래서 리프 노드끼리 연결 리스트로 이어진다.


구조

                    [ 루트 노드 ]
                   /             \
          [ 내부 노드 ]       [ 내부 노드 ]
          /     \               /     \
    [리프]  [리프]         [리프]  [리프]
      ↔                           ↔
  (리프 노드끼리 양방향 연결)
  • 루트 노드: 탐색 시작점. 항상 메모리에 캐시된다
  • 내부 노드: 탐색 방향을 결정하는 키만 포함
  • 리프 노드: 실제 인덱스 키 + 데이터 포인터. 순서대로 연결되어 있어 범위 스캔 가능

노드 하나 = InnoDB 페이지 1개 = 기본 16KB


클러스터링 인덱스 (Primary Key)

InnoDB의 가장 중요한 특징이다. Primary Key 자체가 테이블 데이터의 저장 구조다.

PK 인덱스 리프 노드에 실제 행 데이터 전체가 들어있다

리프 노드: [PK=1 | name="kim" | age=30 | ...]
           [PK=2 | name="lee" | age=25 | ...]
           [PK=3 | name="park"| age=40 | ...]

PK 순서로 데이터가 물리적으로 정렬되어 저장된다. 그래서 PK 범위 조건 조회는 디스크 순차 읽기가 된다.

Auto Increment PK를 권장하는 이유: 새 레코드를 항상 마지막에 추가할 수 있어서 페이지 분할이 거의 없다. UUID나 랜덤 값을 PK로 쓰면 중간 삽입이 빈번히 발생해서 페이지 분할이 잦고 인덱스가 단편화된다.


세컨더리 인덱스

PK가 아닌 컬럼에 생성한 인덱스.

세컨더리 인덱스 리프 노드: [인덱스 키 | PK 값]

name 인덱스:
  리프 노드: ["kim" | PK=1]
             ["lee" | PK=2]
             ["park"| PK=3]

세컨더리 인덱스 리프에는 실제 데이터가 아닌 PK 값이 들어있다. 세컨더리 인덱스로 조회하면:

  1. 세컨더리 인덱스 탐색 → PK 값 획득
  2. PK(클러스터링 인덱스) 탐색 → 실제 데이터 획득

이 2번 과정을 테이블 액세스(또는 북마크 룩업) 라고 한다. 세컨더리 인덱스 조회는 내부적으로 인덱스를 두 번 탄다.


커버링 인덱스

세컨더리 인덱스만으로 쿼리를 완전히 처리할 수 있는 경우. 테이블 액세스가 발생하지 않아서 빠르다.

-- name, age 복합 인덱스가 있을 때
SELECT age FROM users WHERE name = 'kim';
-- name으로 인덱스 탐색, age도 인덱스 리프에 있음 → 테이블 액세스 불필요

EXPLAIN 결과에서 Extra: Using index가 보이면 커버링 인덱스가 적용된 것이다.


인덱스 탐색 방식

Index Range Scan

인덱스를 범위로 읽는 방식. =, <, >, BETWEEN, LIKE 'prefix%'

SELECT * FROM users WHERE age BETWEEN 20 AND 30;
-- 인덱스에서 age=20 위치 찾기 → 리프 연결 리스트 따라 age=30까지 순서대로 읽기

Index Full Scan

인덱스 전체를 처음부터 끝까지 읽는 방식. 테이블 풀스캔보다는 가볍지만 비효율적이다.

인덱스를 못 타는 경우

-- 1. 컬럼 가공
WHERE YEAR(created_at) = 2024      -- ❌ 함수 적용
WHERE created_at + 0 = ...          -- ❌ 연산

-- 올바른 방법
WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31'  -- ✅

-- 2. 묵시적 형변환
WHERE user_id = '123'  -- user_id가 INT인데 문자열 비교  -- ❌ (경우에 따라)

-- 3. LIKE 앞 와일드카드
WHERE name LIKE '%kim'   -- ❌ 인덱스 못 탐 (B-Tree는 앞에서부터 정렬)
WHERE name LIKE 'kim%'   -- ✅

-- 4. OR 조건 (인덱스가 각각 있어도 하나만 타는 경우)
WHERE name = 'kim' OR age = 30

복합 인덱스와 좌측 접두사 규칙

(a, b, c) 복합 인덱스가 있을 때:

WHERE a = 1                    -- ✅ a 인덱스 사용
WHERE a = 1 AND b = 2          -- ✅ a, b 인덱스 사용
WHERE a = 1 AND b = 2 AND c=3  -- ✅ a, b, c 인덱스 사용
WHERE b = 2                    -- ❌ a 없이 b만으로 시작 불가
WHERE a = 1 AND c = 3          -- ✅ a는 사용, c는 스킵 (b를 건너뜀)

인덱스는 왼쪽부터 순서대로 사용된다. 중간 컬럼이 빠지면 거기서 인덱스 사용이 끊긴다.

선택도(Cardinality)가 높은 컬럼을 앞에 두는 것이 원칙이지만, 실제 쿼리 패턴에 따라 달라진다. = 조건 컬럼을 앞에, 범위 조건 컬럼을 뒤에 두는 것이 일반적이다.


페이지 분할

B-Tree 노드(페이지)가 꽉 차면 두 개로 나뉜다. 이 과정이 페이지 분할이다.

[1][2][3][4]  ← 꽉 참
    ↓ 5 삽입
[1][2]  [3][4][5]  ← 분할 발생

페이지 분할이 자주 일어나면:

  • 쓰기 성능 저하 (분할 + 부모 노드 업데이트)
  • 인덱스 단편화 → 범위 스캔 시 논리적 연속이지만 물리적 비연속

OPTIMIZE TABLE로 단편화를 해소할 수 있지만 테이블 잠금이 발생한다.


관련 문서

  • InnoDB 트랜잭션과 잠금 (MVCC, Lock)
  • Redo Log
database 카테고리의 다른 글 보기수정 제안하기

댓글

댓글을 불러오는 중...
목차
  • B-Tree 인덱스
  • B-Tree란
  • 구조
  • 클러스터링 인덱스 (Primary Key)
  • 세컨더리 인덱스
  • 커버링 인덱스
  • 인덱스 탐색 방식
  • Index Range Scan
  • Index Full Scan
  • 인덱스를 못 타는 경우
  • 복합 인덱스와 좌측 접두사 규칙
  • 페이지 분할
  • 관련 문서