Change Buffer — 세컨더리 인덱스 쓰기 최적화
인덱스가 많은 테이블에 INSERT를 하면 왜 느려질까요? 인덱스 페이지를 매번 디스크에서 읽어서 업데이트해야 하기 때문입니다. 그런데 "나중에 몰아서 반영"하면 어떨까요?
Change Buffer란
Change Buffer는 InnoDB가 비유니크 세컨더리 인덱스 의 변경 사항을 즉시 디스크에 반영하지 않고, 메모리에 버퍼링해두었다가 나중에 한꺼번에 반영하는 최적화 기법입니다.
핵심 아이디어는 단순합니다. 세컨더리 인덱스 페이지가 Buffer Pool에 없으면 디스크에서 읽지 않고, 변경 내용만 기록해두는 것입니다.
일반적인 INSERT 흐름 (Change Buffer 없이):
1. 새 행을 클러스터드 인덱스에 삽입
2. 세컨더리 인덱스 페이지를 디스크에서 읽기 (랜덤 I/O!)
3. 세컨더리 인덱스 페이지에 엔트리 추가
Change Buffer를 사용한 흐름:
1. 새 행을 클러스터드 인덱스에 삽입
2. 세컨더리 인덱스 페이지가 Buffer Pool에 없으면
→ 변경 내용을 Change Buffer에 기록 (디스크 읽기 없음!)
3. 나중에 해당 페이지가 읽힐 때 Change Buffer의 내용을 반영(머지)
왜 비유니크 인덱스에만 적용되는가
유니크 인덱스에 INSERT할 때는 반드시 중복 체크 가 필요합니다.
CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(100) UNIQUE, -- 유니크 인덱스
name VARCHAR(100),
INDEX idx_name (name) -- 비유니크 인덱스
);
INSERT INTO users VALUES (1, 'a@test.com', '홍길동');
email인덱스(유니크): 같은 email이 이미 있는지 확인해야 합니다 → 반드시 인덱스 페이지를 읽어야 합니다idx_name인덱스(비유니크): 중복 허용이므로 확인 없이 삽입 가능합니다 → Change Buffer 사용 가능
이 차이가 유니크 인덱스와 비유니크 인덱스의 INSERT 성능 차이를 만들기도 합니다.
Change Buffer의 내부 구조
Change Buffer는 Buffer Pool의 일부 공간 을 사용합니다. 별도의 메모리 영역이 아닙니다.
Buffer Pool
┌────────────────────────────────────┐
│ 데이터 페이지 / 인덱스 페이지 │
│ (LRU 리스트로 관리) │
├────────────────────────────────────┤
│ Change Buffer │
│ (최대 innodb_change_buffer_max_size%)│
└────────────────────────────────────┘
Change Buffer에 기록되는 변경 유형은 다음과 같습니다.
-- innodb_change_buffering 옵션으로 제어
SHOW VARIABLES LIKE 'innodb_change_buffering';
| 값 | 버퍼링 대상 |
|---|---|
| none | 비활성화 |
| inserts | INSERT만 |
| deletes | DELETE marking만 |
| changes | INSERT + DELETE marking |
| purges | Purge(물리적 삭제)만 |
| all (기본) | 모든 변경 |
머지(Merge) 과정
Change Buffer에 쌓인 변경 사항은 언젠가 실제 인덱스 페이지에 반영되어야 합니다. 이를 머지 라고 합니다.
머지가 트리거되는 시점
- **해당 페이지가 읽힐 때 **: SELECT 등으로 인덱스 페이지가 Buffer Pool에 올라오면, 해당 페이지에 대한 Change Buffer 엔트리가 함께 머지됩니다
- ** 백그라운드 퍼지 스레드 **: 서버가 유휴 상태일 때 주기적으로 머지합니다
- ** 서버 종료 시 **: 정상 종료(slow shutdown) 시 모든 Change Buffer를 머지합니다
머지 과정의 상세 흐름
1. 인덱스 페이지가 Buffer Pool에 로드됨
2. Change Buffer에서 해당 페이지에 대한 엔트리 검색
3. 엔트리를 시간순으로 인덱스 페이지에 적용
4. 머지된 엔트리를 Change Buffer에서 제거
5. 머지된 페이지는 dirty page로 표시 → 나중에 디스크에 기록
머지 상태 모니터링
-- Change Buffer 관련 상태 변수
SHOW STATUS LIKE 'Innodb_ibuf%';
주요 지표는 다음과 같습니다.
Innodb_ibuf_merges: 머지가 수행된 횟수Innodb_ibuf_merged_inserts: 머지된 INSERT 엔트리 수Innodb_ibuf_merged_delete_marks: 머지된 DELETE marking 수Innodb_ibuf_merged_deletes: 머지된 물리적 삭제 수Innodb_ibuf_size: 현재 Change Buffer 크기 (페이지 단위)
튜닝 옵션
innodb_change_buffer_max_size
Change Buffer가 사용할 수 있는 Buffer Pool의 최대 비율입니다.
-- 기본값: 25 (Buffer Pool의 25%)
SHOW VARIABLES LIKE 'innodb_change_buffer_max_size';
-- 변경
SET GLOBAL innodb_change_buffer_max_size = 50; -- 50%까지 허용
- **INSERT가 많은 워크로드 **: 값을 높여서 더 많은 변경을 버퍼링합니다
- ** 읽기 중심 워크로드 **: 값을 낮춰서 Buffer Pool을 데이터 캐싱에 더 많이 활용합니다
Change Buffer 비활성화
-- 완전 비활성화
SET GLOBAL innodb_change_buffering = none;
비활성화를 고려하는 상황은 다음과 같습니다.
- **SSD 사용 **: 랜덤 I/O 비용이 낮으므로 Change Buffer의 이점이 줄어듭니다
- ** 읽기 중심 **: 변경이 적어 버퍼링할 것이 없습니다
- ** 유니크 인덱스가 대부분 **: 어차피 Change Buffer를 사용하지 못합니다
성능 영향 예시
세컨더리 인덱스가 3개 있는 테이블에 대량 INSERT를 하는 경우를 비교해보겠습니다.
CREATE TABLE logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
action VARCHAR(50),
created_at DATETIME,
INDEX idx_user (user_id),
INDEX idx_action (action),
INDEX idx_created (created_at)
);
**Change Buffer가 없을 때 **:
- 행 1개 INSERT 시 최대 3번의 랜덤 디스크 I/O 발생 (각 세컨더리 인덱스 페이지 읽기)
- 10만 행 INSERT 시 최대 30만 번의 랜덤 I/O
**Change Buffer가 있을 때 **:
- 인덱스 페이지가 Buffer Pool에 없으면 Change Buffer에 기록 (랜덤 I/O 없음)
- 머지 시점에 한꺼번에 반영 (순차 I/O에 가까움)
- I/O 횟수가 크게 감소
Change Buffer와 Redo Log의 관계
Change Buffer의 변경 사항은 Redo Log에도 기록 됩니다. 서버가 비정상 종료되더라도 Redo Log를 통해 Change Buffer를 복구할 수 있습니다.
INSERT 실행
├─ 클러스터드 인덱스 변경 → Redo Log 기록
├─ Change Buffer에 세컨더리 인덱스 변경 기록 → Redo Log 기록
└─ 나중에 머지 시 → 실제 인덱스 페이지 변경 → Redo Log 기록
이중으로 로그가 기록되는 것처럼 보이지만, 디스크 랜덤 I/O를 순차적인 로그 I/O로 대체하는 것이므로 전체적으로는 더 빠릅니다.
주의할 점
유니크 인덱스와 비유니크 인덱스의 INSERT 성능이 다른 이유가 여기에 있다
유니크 인덱스는 Change Buffer를 사용할 수 없으므로 매 INSERT마다 인덱스 페이지를 디스크에서 읽어야 합니다. 유니크 제약이 꼭 필요하지 않다면 비유니크 인덱스를 사용하는 것이 INSERT 성능에 유리합니다.
대량 INSERT 후 첫 SELECT가 느려질 수 있다
Change Buffer에 쌓인 변경 사항은 해당 페이지가 읽힐 때 머지됩니다. 대량 INSERT 직후 처음으로 해당 인덱스를 사용하는 SELECT가 실행되면 머지 비용이 추가되어 느려질 수 있습니다.
SSD에서는 비활성화를 고려할 수 있다
SSD의 랜덤 I/O 성능은 HDD보다 훨씬 좋으므로 Change Buffer의 이점이 줄어듭니다. 읽기 중심 워크로드라면 비활성화하여 Buffer Pool을 데이터 캐싱에 더 활용하는 것이 나을 수 있습니다.
정리
| 항목 | 설명 |
|---|---|
| Change Buffer | 비유니크 세컨더리 인덱스 변경을 버퍼링, 랜덤 I/O 감소 |
| 적용 제외 | 유니크 인덱스 (중복 체크 필요), 프라이머리 키 |
| 머지 시점 | 페이지 읽힐 때, 백그라운드 스레드, 서버 종료 시 |
| 최대 크기 | Buffer Pool의 25% (innodb_change_buffer_max_size) |
| 비활성화 고려 | SSD + 읽기 중심 + 유니크 인덱스 위주 |