> ## Documentation Index
> Fetch the complete documentation index at: https://villagesql.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# InnoDB가 데이터를 저장하는 방식

> MySQL의 InnoDB 스토리지 엔진이 디스크에 데이터를 구성하는 방식: 클러스터형 인덱스, 페이지, 버퍼 풀, 그리고 이것이 쿼리 성능에 의미하는 바.

> [card] [VillageSQL is a drop-in replacement for MySQL with extensions.](https://villagesql.com/docs/en/8.4/stable/install-server.md)
>
> All examples on this page work on VillageSQL. Install Now →

InnoDB는 MySQL의 기본 스토리지 엔진입니다. InnoDB가 데이터를 저장하는 방식을 이해하면 어떤 쿼리는 빠르고 어떤 쿼리는 그렇지 않은 이유가 설명됩니다 — 특히 기본 키 선택이 중요한 이유, 범위 스캔이 무작위 조회보다 나은 이유, 큰 테이블의 인덱스가 쓰기 성능을 해칠 수 있는 이유가 그렇습니다.

## 페이지: 저장의 단위

InnoDB는 모든 것을 페이지에 저장합니다 — 기본적으로 16KB의 고정 크기 덩어리입니다. 페이지는 InnoDB가 디스크에서 읽거나 디스크에 쓰는 가장 작은 단위입니다. 행 하나를 읽을 때 InnoDB는 그 행이 들어 있는 16KB 페이지 전체를 로드합니다.

이것은 실질적인 결과를 낳습니다:

* 연속된 행을 읽는 쿼리(범위 스캔, 정렬된 데이터에 대한 전체 테이블 스캔)는 각 페이지를 한 번 로드하고 그 페이지에서 많은 행을 처리합니다
* 무작위 행을 읽는 쿼리(큰 테이블에 대한 보조 인덱스 사용)는 행마다 새 페이지를 로드하며, 각각이 별도의 디스크 읽기일 가능성이 큽니다

## 클러스터형 인덱스

InnoDB는 테이블 행을 **클러스터형 인덱스**에 저장합니다 — 리프 노드에 실제 행 데이터가 들어 있고 기본 키로 물리적으로 정렬된 B-트리입니다. 별도의 "테이블 파일"과 "기본 키 인덱스 파일"은 없습니다 — 둘은 같은 구조입니다.

```sql theme={null}
CREATE TABLE orders (
    id          INT PRIMARY KEY AUTO_INCREMENT,
    customer_id INT NOT NULL,
    amount      DECIMAL(10,2) NOT NULL,
    created_at  DATETIME NOT NULL
);
```

이 테이블의 행은 디스크에 `id` 순서로 물리적으로 저장됩니다. `WHERE id BETWEEN 1000 AND 2000` 같은 쿼리는 연속된 페이지 범위를 읽습니다 — 빠릅니다. `id = 1`, 그다음 `id = 500000`, 그다음 `id = 250`인 행을 가져오는 쿼리는 물리적으로 흩어진 세 페이지를 읽습니다.

**기본 키를 정의하지 않으면** InnoDB는 숨겨진 6바이트 행 ID를 만들어 클러스터형 인덱스 키로 사용합니다. 이것은 최적화 기회를 낭비합니다.

## 보조 인덱스

모든 보조 인덱스는 별도의 B-트리입니다. 리프 노드는 전체 행을 저장하지 않습니다 — 인덱싱된 열 값과 기본 키를 저장합니다. 보조 인덱스에 없는 열을 가져오기 위해 InnoDB는 **행 조회**를 합니다: 보조 인덱스 리프에서 기본 키를 가져와 클러스터형 인덱스에서 전체 행을 가져옵니다.

```sql theme={null}
CREATE INDEX idx_customer ON orders (customer_id);

-- This query uses idx_customer, then does a row lookup for each match
SELECT amount FROM orders WHERE customer_id = 42;
```

이 두 단계 과정이 커버링 인덱스가 중요한 이유입니다: 보조 인덱스가 쿼리에 필요한 모든 열을 포함하면 행 조회를 건너뜁니다. [MySQL의 커버링 인덱스](https://villagesql.com/docs/ko/guides/covering-indexes.md)를 참조하세요.

행 조회 비용은 큰 기본 키가 비싼 이유이기도 합니다. 기본 키가 INT(4바이트) 대신 UUID 문자열(36바이트)이면, 모든 보조 인덱스가 항목당 4바이트 대신 36바이트를 저장하고, 행 조회는 더 깊은 B-트리를 탐색합니다.

## 버퍼 풀

InnoDB는 페이지를 **버퍼 풀**이라는 메모리에 캐시합니다. 디스크에서 읽은 페이지는 버퍼 풀에 로드되어 다른 것에 메모리가 필요해질 때까지 거기에 머뭅니다. 같은 페이지에 대한 이후 읽기는 RAM에서 처리됩니다.

```sql theme={null}
-- Check buffer pool size
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';

-- Check hit rate (should be > 99% on a healthy server)
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
-- Innodb_buffer_pool_reads: pages read from disk
-- Innodb_buffer_pool_read_requests: total page read requests
-- Hit rate = 1 - (reads / read_requests)
```

전용 데이터베이스 서버에서는 `innodb_buffer_pool_size`를 사용 가능한 RAM의 70–80%로 설정하세요. 이것은 읽기 위주 워크로드에서 가장 영향력이 큰 단일 구성 변경입니다 — 작업 집합이 메모리에 많이 들어갈수록 디스크 읽기가 줄어듭니다.

## 행 형식

InnoDB는 네 가지 행 형식을 지원합니다. 기본값은 `DYNAMIC`입니다.

| 형식 | 가변 길이 열 | 오버플로 전 인라인 저장 | 비고 |
| :- | :- | :- | :- |
| `REDUNDANT` | 인라인 또는 페이지 외부에 저장 | \~8KB | 레거시 형식 |
| `COMPACT` | 인라인 또는 페이지 외부에 저장 | \~8KB | 오래된 형식 |
| `DYNAMIC` | 클 때 페이지 외부에 저장 | \~8KB | 기본값; TEXT/BLOB에 더 적합 |
| `COMPRESSED` | 압축하여 페이지 외부에 저장 | \~8KB | 디스크 사용량 감소; CPU 오버헤드 추가 |

기본 16KB 페이지의 대략 절반인 \~8KB 수치는 InnoDB가 가변 길이 열 데이터를 페이지 외부로 옮기는 지점이므로, 큰 `VARCHAR`/`TEXT`/`BLOB` 열이 몇 개뿐인 행은 상한에 부딪히지 않고 그것을 훨씬 넘어 커질 수 있습니다. 그러나 페이지 외부로 옮겨지지 않고 인라인으로 저장되는 열 — 예를 들어 많은 고정 폭 `CHAR` 열 — 은 여전히 InnoDB 자체의 행 크기 제한에 전부 계산되며, 그 제한에 도달하면 `ERROR 1118 (42000): Row size too large (> 8126). Changing some columns to TEXT or BLOB may help.`가 발생합니다. MySQL의 별도이고 더 높은 행 크기 제한은 BLOB/TEXT 오버플로 페이지를 제외하고 65,535바이트이며, `ROW_FORMAT`과 관계없이 적용됩니다. 두 제한 모두 실제이며, 어떤 열이 크고 그것이 페이지 외부로 옮겨질 수 있는지에 따라 어느 쪽이든 먼저 부딪히는 제한이 될 수 있습니다.

`DYNAMIC` 형식에서 `VARCHAR`, `TEXT` 또는 `BLOB` 열이 페이지를 넘치면 전체 값이 오버플로 페이지에 저장됩니다 — 메인 행에는 20바이트 포인터만 남습니다. `COMPACT`는 대신 처음 768바이트를 인라인으로 저장합니다. `DYNAMIC`의 접근 방식은 긴 값이 있을 때 메인 행을 더 작게 유지하므로, 페이지당 더 많은 행이 들어가고 B-트리가 더 얕게 유지됩니다.

## 자주 묻는 질문

#### 기본 키 선택이 왜 성능에 그렇게 큰 영향을 주나요?

클러스터형 인덱스가 행을 기본 키로 물리적으로 정렬하기 때문입니다. 무작위 위치에 삽입(UUID 기본 키 같은)하면 **페이지 분할**이 일어납니다 — InnoDB는 순서를 유지하기 위해 가득 찬 페이지를 분할하고 데이터를 옮겨야 합니다. 순차적 기본 키(AUTO\_INCREMENT, UUID v7)는 끝에 삽입되어 분할을 피합니다. 페이지 분할은 테이블스페이스를 단편화하고 시간이 지남에 따라 쓰기와 읽기를 모두 느리게 합니다.

#### 버퍼 풀이 너무 작으면 어떻게 되나요?

InnoDB는 디스크에서 페이지를 자주 읽어야 합니다. `Innodb_buffer_pool_reads`가 올라가는 것을 보게 됩니다. 서버는 I/O 바운드가 됩니다 — RAM에서라면 마이크로초가 걸릴 쿼리가 디스크에서는 밀리초가 걸립니다. 읽기 위주 워크로드에서는 작업 집합을 담는 버퍼 풀이 거의 어떤 인덱스 최적화보다 가치 있습니다.

#### InnoDB와 MyISAM의 차이는 무엇인가요?

InnoDB는 트랜잭션, 외래 키, 행 수준 잠금, 충돌 복구를 지원합니다. MyISAM은 이 중 어느 것도 지원하지 않습니다 — 특정 읽기 전용 대량 워크로드에는 더 빠르지만 대부분의 애플리케이션에는 적합하지 않습니다. InnoDB가 MySQL 기본값이며, 특별한 이유가 없다면 그것을 사용하세요.

## 문제 해결

| 문제 | 해결 방법 |
| :- | :- |
| 큰 테이블에서 범위 스캔이 느림 | 버퍼 풀 적중률을 확인하세요. `innodb_buffer_pool_size`를 늘리는 것을 고려하세요 |
| 시간이 지남에 따라 삽입이 느려짐 | 순차적 기본 키인가요? `SHOW ENGINE INNODB STATUS`로 페이지 분할을 확인하세요 |
| RAM이 충분한데도 디스크 I/O가 높음 | 버퍼 풀이 작업 집합에 비해 작을 수 있습니다. 늘리세요 |
| 대부분 전문 읽기인 큰 테이블 | 큰 텍스트를 메인 페이지 밖에 두는 `DYNAMIC` 행 형식을 고려하세요 |

VillageSQL은 같은 InnoDB 엔진을 탑재하므로, 이 페이지의 모든 설정이 그대로 적용됩니다. [VillageSQL을 설치](https://villagesql.com/install)하여 InnoDB를 유지하면서 사용자 정의 타입, 함수, 인덱스를 위한 확장 프레임워크를 얻으세요.

## 참고

* [MySQL 인덱스의 동작 방식](https://villagesql.com/docs/ko/guides/mysql-indexes.md) — InnoDB 페이지 안에서 B-트리 인덱스가 배치되는 방식
* [MySQL의 커버링 인덱스](https://villagesql.com/docs/ko/guides/covering-indexes.md) — 인덱스 안에 머무는 것이 비싼 페이지 읽기를 피하는 이유
* [MySQL의 테이블 파티셔닝](https://villagesql.com/docs/ko/guides/table-partitioning.md) — 여러 InnoDB 세그먼트에 데이터 분할하기


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.