#4. 자동으로 생기는 인덱스들
PK·UNIQUE·FK가 만드는 숨은 인덱스
# 자동으로 생기는 인덱스들
> 📘 **학습자료 4편 / 5편** | 연결 퀴즈: [**인덱스 기초2 풀러가기**](quiz.html?set=B)
> "내가 직접 만들지 않아도 InnoDB가 알아서 인덱스를 만들어주는 경우"를 다룹니다.
> PK가 없을 때, UNIQUE 제약을 걸 때, FK를 걸 때 — 인덱스는 어디선가 자동으로 생겨납니다.
> 📚 **이전 편**: [3편 - InnoDB의 두 가지 인덱스: 클러스터링 vs 보조]
> 3편에서 InnoDB의 클러스터링 인덱스와 보조 인덱스의 구조를 다뤘습니다. 이번 편은 그 위에서 시작합니다.
## 이 자료를 다 읽으면 알게 되는 것
- PK도 NOT NULL+UNIQUE 컬럼도 없을 때, InnoDB가 만드는 **숨겨진 클러스터링 인덱스**
- `UNIQUE` 제약을 걸면 **자동으로 생성되는 인덱스**
- `FOREIGN KEY` 제약을 걸면 **자동으로 생성되는 인덱스** (그리고 다른 RDBMS와의 차이)
- 인덱스를 직접 안 만든 테이블도 사실 인덱스가 **여러 개** 존재한다는 것
---
## 📑 목차
- [1. PK가 없으면 InnoDB는 무엇을 할까](#1-pk가-없으면-innodb는-무엇을-할까)
- [2. GEN_CLUST_INDEX: 숨겨진 클러스터링 인덱스](#2-gen_clust_index-숨겨진-클러스터링-인덱스)
- [3. 제약조건이 만드는 자동 인덱스](#3-제약조건이-만드는-자동-인덱스)
- [4. 실습: 자동 인덱스 개수 세보기](#4-실습-자동-인덱스-개수-세보기)
- [핵심 요약](#핵심-요약)
- [다음 편 예고](#다음-편-예고)
---
## 1. PK가 없으면 InnoDB는 무엇을 할까
3편에서 클러스터링 인덱스 선정 우선순위를 다뤘습니다. 다시 한번 정리하면:
1. 사용자가 명시적으로 지정한 **PRIMARY KEY**
2. **NOT NULL + UNIQUE** 조건을 모두 만족하는 첫 번째 컬럼
3. 그것도 없으면, InnoDB가 내부적으로 **숨겨진 Row ID**를 만들어 사용
3편에서는 1, 2번 케이스까지만 봤습니다. 이번 편은 **3번 케이스**부터 시작합니다.
다음과 같은 테이블이 있다고 해봅시다. 게시글 본문만 저장하는 단순한 테이블입니다.
```sql
CREATE TABLE articles (
title VARCHAR(200),
content TEXT
);
```
- PK 없음
- NOT NULL + UNIQUE인 컬럼도 없음
이 테이블의 **클러스터링 인덱스 키는 무엇일까요?**
직관적으로는 "그럼 클러스터링 인덱스가 없는 거 아니야?"라고 생각할 수 있습니다. 하지만 InnoDB의 모든 테이블은 **반드시 클러스터링 인덱스를 가져야** 합니다. 데이터의 물리적 저장 자체가 클러스터링 인덱스 위에 올라가는 구조이기 때문입니다.
그래서 InnoDB는 사용자가 키를 안 정해주면 **스스로 만듭니다.**
---
## 2. GEN_CLUST_INDEX: 숨겨진 클러스터링 인덱스
InnoDB는 1, 2번 후보가 모두 없을 때 **숨겨진 6바이트 Row ID** 컬럼을 내부적으로 추가하고, 이걸 클러스터링 인덱스 키로 씁니다. 이 자동 생성 인덱스의 이름이 **`GEN_CLUST_INDEX`** 입니다.
- 이름이 정해져 있음: `GEN_CLUST_INDEX` (Generated Clustered Index의 줄임)
- 키 컬럼은 사용자가 볼 수 없음 (숨겨짐)
- 새 행이 들어올 때마다 단조 증가하는 Row ID가 자동 부여됨
**실습으로 확인해봅시다.**
```sql
CREATE TABLE articles (
title VARCHAR(200),
content TEXT
);
SELECT * FROM INFORMATION_SCHEMA.INNODB_INDEXES;
```
```
+----------+-----------------+----------+------+----------+---------+-------+
| INDEX_ID | NAME | TABLE_ID | TYPE | N_FIELDS | PAGE_NO | SPACE |
+----------+-----------------+----------+------+----------+---------+-------+
| 163 | GEN_CLUST_INDEX | 1071 | 1 | 5 | 4 | 5 |
+----------+-----------------+----------+------+----------+---------+-------+
```
이름이 `GEN_CLUST_INDEX`로 표시됩니다. **사용자가 만든 적 없는 인덱스가 왜인지 모르게 존재**합니다.
여기서 `TYPE = 1`을 주목해주세요. 3편에서 봤던 `TYPE = 3` (사용자 지정 클러스터링 인덱스)과 다른 값입니다.
<details>
<summary>📎 INNODB_INDEXES의 TYPE 값 의미 (참고)</summary>
```
0 = 일반 보조 인덱스 (nonunique secondary index)
1 = 자동 생성된 클러스터링 인덱스 (GEN_CLUST_INDEX) ← 이번 케이스
2 = 유니크 보조 인덱스 (unique nonclustered)
3 = 사용자 지정 클러스터링 인덱스
32 = 풀텍스트 인덱스
64 = 공간 인덱스
128 = 가상 컬럼 보조 인덱스
```
`TYPE = 1`과 `TYPE = 3`은 둘 다 클러스터링 인덱스지만, **누가 만들었느냐**의 차이입니다.
</details>
> 💡 **그래서 PK는 항상 명시하는 게 좋다**
> InnoDB는 어떻게든 클러스터링 인덱스를 만들기 때문에, PK가 없어도 테이블은 동작합니다. 하지만:
> - 숨겨진 Row ID는 단조 증가만 할 뿐 의미가 없는 값입니다
> - 모든 보조 인덱스는 리프에 클러스터링 키를 들고 있어야 하는데(3편 참고), 그 키가 6바이트 Row ID가 됩니다
> - 의도치 않은 동작이 생길 수 있습니다 (예: 복제 환경에서 Row ID가 노드별로 다를 수 있음)
>
> 실무에서는 항상 명시적으로 PK를 지정하는 것을 권장합니다.
---
## 3. 제약조건이 만드는 자동 인덱스
GEN_CLUST_INDEX 외에도 InnoDB(정확히는 MySQL)는 **제약조건을 걸면 인덱스를 자동으로** 만들어줍니다. 두 가지 케이스를 봅시다.
### 3-1. UNIQUE 제약 → 자동 유니크 인덱스
```sql
CREATE TABLE posts (
post_id INT PRIMARY KEY,
slug VARCHAR(100) UNIQUE
);
```
`slug` 컬럼에 `UNIQUE` 제약을 걸면 (slug는 URL에 쓰이는 고유 식별자입니다 — 예: `my-first-post`), MySQL은 **자동으로 유니크 인덱스를 생성**합니다.
이건 사실 **필수에 가깝습니다.** UNIQUE 제약은 "이 컬럼의 값은 중복되면 안 된다"는 약속인데, 새 값이 들어올 때마다 중복인지 확인하려면 빠른 조회 수단이 필요합니다. 인덱스가 없으면 매 INSERT마다 **테이블 풀 스캔**으로 중복을 검사해야 하니까요.
### 3-2. FOREIGN KEY 제약 → 자동 인덱스
```sql
CREATE TABLE posts (
post_id INT PRIMARY KEY,
author_id INT,
FOREIGN KEY (author_id) REFERENCES users(user_id)
);
```
`author_id`에 외래키 제약을 걸면, MySQL은 **`author_id` 컬럼에도 자동으로 인덱스를 생성**합니다.
이유는 비슷합니다. FK는 부모 테이블의 행을 삭제·변경할 때 **자식 테이블에서 참조 무결성을 검사**해야 하는데, 인덱스가 없으면 매번 자식 테이블 풀 스캔이 일어나서 성능이 망가지기 때문입니다.
> ⚠️ **MySQL 한정 동작입니다**
> "FK 걸면 자동으로 인덱스 생성"은 **MySQL의 동작**이지, SQL 표준이나 모든 RDBMS의 공통 동작이 아닙니다.
> - **PostgreSQL**: FK를 걸어도 인덱스를 자동 생성하지 **않습니다.** 직접 만들어야 합니다.
> - **Oracle**: 마찬가지로 자동 생성되지 않습니다.
>
> 다른 DB로 이직하거나 작업할 때 헷갈리지 마세요. 신입 개발자가 잘 빠지는 함정입니다.
> 💡 여기까지 읽었다면 자동 인덱스의 개념을 충분히 이해한 것입니다. [**퀴즈로 확인해보세요!**](quiz.html?set=B)
---
## 4. 실습: 자동 인덱스 개수 세보기
이제 3-1, 3-2에서 부분적으로 봤던 `posts` 테이블을 모두 합쳐봅시다. 이 테이블에 인덱스가 **몇 개 있을지** 추측해봅시다.
```sql
CREATE TABLE posts (
post_id INT PRIMARY KEY,
slug VARCHAR(100) UNIQUE,
author_id INT,
FOREIGN KEY (author_id) REFERENCES users(user_id)
);
```
- `post_id`: 게시글의 PK
- `slug`: URL용 고유 식별 문자열 (3-1에서 본 UNIQUE)
- `author_id`: 글쓴이를 가리키는 외래키 (3-2에서 본 FK)
CREATE INDEX 같은 명시적인 인덱스 생성 구문은 **단 하나도** 없습니다. 그런데도 인덱스는 **3개**가 생깁니다.
1. **PRIMARY KEY (post_id)** → 클러스터링 인덱스
2. **UNIQUE (slug)** → 유니크 인덱스 자동 생성
3. **FOREIGN KEY (author_id)** → 일반 인덱스 자동 생성
확인 쿼리:
```sql
SELECT DISTINCT index_name
FROM information_schema.statistics
WHERE table_name = 'posts';
```
**결과:**
| index_name |
|------------|
| PRIMARY |
| slug |
| author_id |
> 💡 **"인덱스를 신중하게"라는 1편의 조언과 합쳐 생각해보면**
> 1편에서 "인덱스가 많을수록 INSERT/UPDATE/DELETE가 느려진다"고 했습니다.
> 그런데 우리가 모르는 사이에도 PK·UNIQUE·FK가 인덱스를 자동으로 만들고 있다면?
>
> 결론: **테이블을 설계할 때 자신도 모르게 인덱스를 늘리고 있을 수 있다는 것을 인지**하고 있어야 합니다.
> 특히 정규화 과정에서 FK가 늘어날수록 자동 인덱스도 함께 늘어나니까요.
---
## 핵심 요약
이번 편에서 꼭 가져가야 할 한 가지:
> 🎯 **인덱스는 명시적으로만 생기는 게 아니다**
> PK가 없어도 InnoDB가 `GEN_CLUST_INDEX`를 만들고, `UNIQUE`/`FOREIGN KEY` 제약조건이 또 자동 인덱스를 추가합니다.
> 테이블의 실제 인덱스 개수는 종종 우리 예상보다 많습니다.
체크리스트:
- [x] PK도 NOT NULL+UNIQUE 컬럼도 없을 때, InnoDB가 **GEN_CLUST_INDEX**를 만든다는 것을 안다
- [x] `UNIQUE` 제약이 자동으로 유니크 인덱스를 만드는 **이유**를 설명할 수 있다 (중복 검사 필요)
- [ ] `FOREIGN KEY` 자동 인덱스가 **MySQL 한정** 동작이며, PostgreSQL/Oracle은 그렇지 않다는 것을 안다
- [ ] 제약조건 3개가 걸린 테이블의 자동 인덱스 개수를 셀 수 있다
> 📝 체크리스트를 다 채울 자신이 있다면? [**인덱스 기초2 퀴즈 도전하기**](quiz.html?set=B)
---
## 다음 편 예고
이번 편에서 인덱스가 어떻게 "생겨나는지"를 봤습니다. 다음 편에서는 인덱스가 어떻게 "변경되는지"를 다룹니다.
> "INSERT 한 번 하면, 모든 인덱스가 그 즉시 갱신될까?"
> "사실 그렇지 않습니다. **PK는 즉시 반영, 일반 보조 인덱스는 지연 반영**됩니다."
InnoDB의 핵심 최적화 기법인 **Change Buffer**, 그리고 트랜잭션의 동시성을 위해 삭제마저 "논리적으로만" 일어나는 **MVCC**까지 — 인덱스 동작의 깊은 곳을 들여다봅니다.
---
## Reference
- MySQL 공식 문서: [InnoDB Index Types](https://dev.mysql.com/doc/refman/8.4/en/innodb-index-types.html)
- MySQL 공식 문서: [INNODB_INDEXES Table](https://dev.mysql.com/doc/refman/8.4/en/information-schema-innodb-indexes-table.html)
- MySQL 공식 문서: [CREATE TABLE](https://dev.mysql.com/doc/refman/8.4/en/create-table.html)
- MySQL 공식 문서: [FOREIGN KEY Constraints](https://dev.mysql.com/doc/refman/8.4/en/create-table-foreign-keys.html)