

데이터를 정렬된 상태로 관리하는 별도 구조(B-Tree 등)
본 데이터는 순서 상관 없이 저장되고 Index는 설정한 순서에 따라 정렬된 key - value 쌍을 만들어둠
느린 I/O를 줄이기 위해 최소한의 정보만 모아둔 찾기 전용 구조
| 장점 | 단점 |
|---|---|
| SELECT가 매우 빨라짐 | INSERT/UPDATE/DELETE 성능 저하 |
| 정렬/범위 검색 성능 향상 | 인덱스 생성·유지 비용 발생 |
| 옵티마이저의 다양한 최적화 기법 사용 가능 | 인덱스를 너무 많이 만들면 전체 성능 망가짐 |
인덱스는 읽기 성능을 위해 쓰기 성능을 희생하는 구조이다.
웹 서비스에서는 조회가 압도적으로 많기 때문에 인덱스를 잘 설계하는 것이 필수
주의
유니크 인덱스 + 일반 인덱스를 같은 컬럼에 둘 다 만들면 중복이다
→ 하나만 쓰기
JSON_CONTAINS() 같은 함수와 함께 사용
B-Tree(Balanced Tree)는 이진 트리를 확장하여 N개의 자식을 가질 수 있도록 고안됨
좌우 자식 간 균형이 맞지 않으면 매우 비효율적이어서 항상 균형을 맞춰야 함
리프 노드만 실제 데이터 주소(PK or 물리주소)를 가진다.
이게 느려보이지만 장점도 존재한다 → 아래 클러스터링 인덱스에서 설명.
인덱스 사용 시 수행되는 동작은 단순히 두 가지이다.
이 두 단계는 B-Tree 깊이에 비례하여 매우 적은 수의 I/O로 처리된다.
DBMS는 옵티마이저의 판단을 위한 비용(cost) 모델을 갖고 있음
| 작업 | 비용 |
|---|---|
| 테이블에서 연속된 데이터 1건 읽기 | 비용 1 |
| 인덱스로 1건 읽기 | 비용 4~5 |
랜덤 I/O가 순차 I/O보다 훨씬 비싸기 때문
인덱스가 없다면
레코드가 수십만 건만 되어도 이 작업은 매우 느리다.
소수의 레코드를 찾는 경우 → 인덱스 압도적 승리
많은 레코드를 읽는 경우 → 테이블 스캔 승리
일반적으로 다음과 같은 기준을 사용
조회해야 하는 레코드가 전체의 20~25% 이상이면
인덱스를 타지 않고 테이블 스캔이 더 빠르다.

InnoDB는 클러스터링 인덱스를 사용하기 때문에
세컨더리 인덱스는 레코드 주소가 아닌 PK 값을 저장한다.
PK가 길면 생기는 문제
해답

다중 컬럼 인덱스는 다음 원칙을 반드시 기억해야 한다.
왼쪽 → 오른쪽 순서로 정렬되며, 오른쪽 컬럼은 왼쪽 컬럼에 의존한다
INDEX idx (dept_no, emp_no)
이 인덱스로 가능한 조건
이 인덱스로 불가능한 조건
다중 컬럼 인덱스 설계 = WHERE 조건의 등 값 조건 순서대로 배치
카디날리티 = 컬럼의 유니크한 값 개수
예)

인덱스 생성 시
INDEX idx (dept_no ASC, emp_no DESC)
이렇게 방향을 지정해도
MySQL은 실행 계획에 따라 오름/내림차순으로 자유롭게 역순 스캔 가능하다.
정순 스캔 vs 역순 스캔
이유
B-Tree 인덱스는 항상 정렬된 상태를 유지해야 하는 구조
따라서 레코드의 추가(INSERT), 삭제(DELETE), 수정(UPDATE) 시 인덱스에도 즉각 반영되어야 하며, 이 과정에서 상당한 디스크 I/O가 발생
레코드가 추가되면 인덱스에서도 다음 작업 수행
(1) 인덱스는 항상 정렬 유지
즉시 B-Tree 내부에서 적절한 위치를 찾아 저장
그래서
인덱스가 3개면
레코드 작업 비용 = 1 + (1.5 × 3) = 5.5
→ 디스크 작업이므로 비용이 매우 크다.
(2) Insert Buffer(변경 버퍼: Change Buffer)를 통한 지연 처리
MyISAM / MEMORY 엔진은 즉시 인덱스를 업데이트
InnoDB는 일부 인덱스(유니크 X)에 대해 변경을 지연시키는 메커니즘
장점
단점
(3) 유니크 인덱스는 작업 비용이 훨씬 크다
유니크 인덱스는 저장 시:
그래서 유니크 인덱스는 정말 필요한 경우가 아닌 이상 남발하면 절대 안 된다.
(4) 페이지 분할(Page Split)
리프 노드(페이지)가 꽉 차 있으면
→ 이 과정은 디스크 I/O를 여러 번 발생시키므로 INSERT가 매우 느려질 수 있음
(1) 인덱스에선 실제 삭제가 아니라 삭제 마킹
레코드는 테이블에서 제거되지만,
인덱스에서는 리프 노드에 삭제 플래그만 붙인다.
삭제 마킹의 특징
(2) DELETE는 적절한 인덱스를 사용하지 않으면 매우 위험
InnoDB의 잠금 메커니즘 특성상
UPDATE/DELETE는 검색에 사용된 인덱스를 기준으로 잠금을 건다.
WHERE 절에서 인덱스를 사용하지 않으면
넥스트 키 락 / 갭 락이 광범위하게 걸리면서 불필요하게 많은 레코드를 잠그게 됨
심하면 테이블 전체 잠금 같은 상황이 발생
→ DELETE/UPDATE에는 반드시 적절한 인덱스가 필요함.
UPDATE는 사실상 다음과 동일
UPDATE = DELETE + INSERT
아래 3 종류의 컬럼에 따라 부담이 다름
(1) PK(Primary Key) 수정
절대로 하면 안 되는 작업
PK가 바뀌면
기존 PK 인덱스에서 삭제
새로운 PK 추가
세컨더리 인덱스에는 PK가 저장되므로,
모든 세컨더리 인덱스도 함께 업데이트해야 함
결과
PK를 변경하는 UPDATE는 최악의 성능을 가진다.
(2) 인덱스 컬럼 수정
이 경우도 DELETE + INSERT처럼 동작하므로 비용이 큼
가능하면 인덱스 컬럼은 변경을 최소화해야 한다.
(3) 일반 값(비인덱스 컬럼) 수정
가장 비용이 적음
(1) PK 검색 (가장 빠른 검색 방법)
InnoDB는 클러스터링 인덱스 구조를 사용한다.
PK가 곧 레코드의 실제 저장 위치를 결정하는 키
(리프 노드에 PK와 레코드가 함께 저장됨)
동작 과정
특징
예시
SELECT * FROM member WHERE id = 10;
(2) Secondary Index 검색 (빠르지만 PK보다 한 단계 추가 비용 존재)
Secondary index의 리프 노드에는 실제 레코드가 아닌 PK가 저장된다.
(인덱스 키, PK) 형태
따라서 레코드를 찾기 위해서는 두 단계가 필요하다.
동작 과정
→ 트리를 2번 탐색해야 하므로 PK보다 비용이 더 든다.
특징
유니크 인덱스는 더 빠르다?
(3) 인덱스를 사용하지 않는 검색 (Full Table Scan)
인덱스가 없다면 DB는 데이터가 어디 있는지 전혀 모르므로
테이블의 모든 레코드를 처음부터 끝까지 검사해야 한다.
이것을 테이블 풀 스캔이라고 한다.
발생 케이스
특징
(4) 어떤 검색은 인덱스를 사용할 수 없는가?
B-Tree는 왼쪽부터 정렬되어 있으므로 아래 방식은 인덱스를 사용할 수 없다.
뒷부분 검색
WHERE name LIKE '%park%'
컬럼에 연산 / 함수 적용
WHERE DATE(created_at) = '2024-01-01'
자료형 변환이 필요한 비교
WHERE phone = 01012341234 -- 문자열 vs 숫자 비교
다중 컬럼 인덱스에서 선행 컬럼 누락
인덱스 (A, B)에 대해
WHERE B = 10 -- 인덱스 미사용
가장 이상적인 인덱스 사용 방식
→ 인덱스 조건으로 범위가 명확하게 정해진 경우 사용
SELECT * FROM employee
WHERE name BETWEEN 'Lemon' AND 'Mango';

작동 흐름
인덱스 탐색(Search)
루트 → 브랜치 → 리프 노드를 따라
Lemon이 시작되는 위치까지 찾아감.
인덱스 스캔(Scan)
시작점부터 Mango까지 정렬된 순서대로 연속 읽기
(리프 노드는 정렬되어 있고, 페이지 간 링크 존재)
랜덤 I/O
인덱스가 PK를 가지고 있으므로
→ PK로 실제 레코드를 테이블에서 읽어옴.
커버링 인덱스면 더 빠름
특징
인덱스를 처음부터 끝까지 순차적으로 읽는 방식
- 조건절이 인덱스 선행 컬럼을 사용하지 않는 경우
(인덱스가 (A, B)인데 WHERE B=… 로 검색하는 경우)
SELECT COUNT(*) FROM employee;
특징
인덱스 중 필요한 부분만 건너뛰며 듬성듬성 읽는 방식
GROUP BY 최적화에 아주 자주 등장한다.
SELECT dept_no, MIN(emp_no)
FROM dept_emp
WHERE dept_no BETWEEN 'D002' AND 'D004'
GROUP BY dept_no;

왜 가능한가?
중간 값들은 불필요한 값이므로 건너뛰어도 됨
특징
선행 인덱스 컬럼이 WHERE 절에 없어도
자동으로 인덱스를 사용할 수 있게 하는 기능
기존에는 불가능했던 케이스
인덱스가 (gender, birth_date)일 때
SELECT * FROM employee
WHERE birth_date >= '1994-12-26';
→ gender 조건이 없으므로 인덱스 사용 불가
MySQL 8.0의 스킵 스캔 작동 방식

옵티마이저가 내부적으로 쿼리를
gender = 'M'
birth_date >= …
gender = 'F'
birth_date >= …
이렇게 두 번 실행하는 것처럼 최적화하여 인덱스를 사용
인덱스 스킵 스캔 조건
다음 3개를 반드시 만족해야 한다.
특징
그리고 UPDATE/DELETE에선 인덱스가 더 중요
검색에 사용된 인덱스 범위 전부 잠금
인덱스를 못 쓰면 불필요하게 많은 레코드를 잠그게 됨
심하면 테이블 전체 Lock 수준으로 번짐
변경 작업(UPDATE, DELETE)에는 반드시 적절한 인덱스가 필요
아래는 인덱스 타지 못함
SUBSTRING(name,1,3))LIKE '%abc')정렬된 인덱스의 순서(정렬 효과)를 활용할 수 없기 때문
MySQL B-Tree는 내부적으로
때문에
정순(Index Forward Scan)이 가장 빠르게 동작한다.
역순(Index Backward Scan)도 가능하지만
구조적 한계 때문에 상대적으로 느리다.
인덱스는 읽기를 빠르게 해주지만,
쓰기(INSERT, UPDATE, DELETE)는 더 비싸진다.
인덱스 1개 추가 = INSERT 비용 +1.5
인덱스 3개 있는 테이블 = INSERT 비용 약 5.5배 증가
실제로 사용되는 WHERE 조건, JOIN, ORDER/GROUP BY에만 인덱스 생성해야 한다.
쿼리가 인덱스 내부 정보만으로 처리되면(리프 노드에서 끝나면)
→ PK lookup(랜덤 I/O)이 필요 없음
SELECT emp_no FROM employee WHERE name = 'Kim';
인덱스가 (name, emp_no) 형태라면 테이블까지 안 가도 됨.
INDEX (A, B, C)
정렬 순서
A → A 같은 값에서 B → B 같은 값에서 C
INDEX (A, B) 인데
WHERE B = ? AND A = ? 로 검색한다면 → 비효율
A 정렬 기반 구조를 깨기 때문
InnoDB에서 테이블 자체가 PK 인덱스를 기준으로 B-Tree 구조로 저장
외래키(FK)는 부모 테이블의 PK를 참조하는 제약 조건이다.
FK 제약이 존재하면 DBMS는 반드시 FK 컬럼에 인덱스를 만든다.
이유 : FK 제약 검증이 매우 느려지는 것을 방지하기 위해
자식 테이블에서 부모 테이블을 참조할 때,
데이터 추가·수정·삭제 시 매번 참조 일관성을 검사해야 한다.
INSERT INTO orders (user_id, ...) VALUES (3, ...);
DB는 이렇게 체크
user_id = 3 이 parent 테이블에 존재하는지 검사여기서 user_id에 인덱스가 없으면?
→ 매번 테이블 풀 스캔
→ 성능 폭발적으로 저하
→ 수십~수백만건이면 사실상 불가능
그래서 DBMS는 보호 차원에서 자동으로 인덱스를 생성해준다.
외래키가 존재하면 부모 테이블의 레코드를 마음대로 삭제할 수 없다.
DELETE FROM user WHERE id = 3;
user_id = 3 을 참조하는 자식 레코드가 있다면?
→ DB는 무결성 깨짐을 막기 위해 삭제를 거부한다.
이는 외래키 설계 이유 자체가
참조 무결성을 깨뜨리는 연산을 차단하기 때문
해결 방법
ON DELETE CASCADE → 부모 삭제 시 자식 자동 삭제ON DELETE SET NULL → 부모 삭제 시 FK null로 변경외래키는 읽기 성능 문제가 아니라, 거의 100% 쓰기(write)에서 문제를 발생시킨다.
이유 : 부모/자식 테이블 모두에 락(Lock)이 걸림
INSERT, UPDATE, DELETE 같은 변경 작업이 일어나면
InnoDB는 FK 무결성을 보장하기 위해 양쪽 테이블에 잠금을 건다.
(1) INSERT 시 락이 걸리는 이유
INSERT INTO orders (user_id, ...) VALUES (3, ...)
DB는 다음 작업을 수행한다:
id = 3 검색만약 동시에 여러 INSERT가 parent를 조회하면
같은 레코드에 공유 락들이 몰리고
경합이 발생한다.
(2) DELETE/UPDATE는 훨씬 더 심각한 락 발생
부모 테이블을 수정하는 순간,
DB는 FK가 걸린 자식 테이블을 잠그거나 검사해야 한다.
DELETE FROM user WHERE id = 3;
user_id = 3 이 orders 테이블에서 참조 중이라면
특히 인덱스가 없는 FK는 지옥
외래키 컬럼에 인덱스가 없으면?
→ 그래서 MySQL은 FK에 인덱스를 강제하는 것이다.
DELETE parent 시 자식 존재 여부 검사하는 과정에서 경합 발생
부모 테이블에서 삭제가 거의 불가능해짐.
자식 INSERT 시 부모 키 유효성 검사로 인해 parent 락 경합
트래픽 높은 서비스는 parent 테이블에 요청이 몰림.
많은 FK가 걸린 스키마는 병목이 발생하기 쉽다
테이블 간 의존성이 많아질수록 업데이트/삭제가 전체 DB 락 트리를 흔들어댄다.
트위터, 페이스북, 우버 같은 초대형 서비스들은
DB 성능과 확장성 때문에 FK를 실제 운영에서는 거의 사용하지 않는다.
대신