카테고리 : MySQL/기술노트

InnoDB 페이지 구조: page size, extent, segment, B+Tree 페이지의 구성

InnoDB의 page size, extent, segment, B+Tree 페이지 구조를 내부 저장 단위와 운영 관점에서 정리한다.

저자: MySQL 기술 노트 작성: 2026.05.26 약 13분 7,623자
다운로드

1. 왜 InnoDB 페이지 구조를 알아야 하는가

InnoDB를 운영하다 보면 Buffer Pool, 인덱스 크기, 테이블스페이스 증가, OPTIMIZE TABLE, ALTER TABLE, 랜덤 I/O, 페이지 분할 같은 용어가 반복해서 등장한다. 이 용어들은 모두 결국 하나의 저장 단위로 수렴한다. 바로 InnoDB page다.

MySQL 사용자는 SQL을 행(row)과 테이블(table) 단위로 생각하지만, InnoDB 스토리지 엔진은 데이터를 페이지(page), extent, segment, tablespace 단위로 배치하고 읽고 쓴다. SELECT 한 건을 실행하더라도 디스크에서는 행 하나가 아니라 페이지 하나가 읽히고, UPDATE 한 행을 변경하더라도 redo log와 buffer pool에서는 페이지 단위 변경이 추적된다.

이 구조를 이해하면 다음과 같은 운영 판단이 훨씬 명확해진다.

  • 왜 작은 행 하나를 읽어도 디스크 I/O가 페이지 단위로 발생하는가
  • 왜 인덱스가 커지면 Buffer Pool 적중률이 급격히 나빠지는가
  • 왜 Primary Key가 랜덤하면 페이지 분할과 단편화가 늘어나는가
  • innodb_page_size는 운영 중 쉽게 바꿀 수 없는 초기 설계 항목인가
  • 왜 대량 삭제 후 테이블 파일 크기가 바로 줄지 않는가
  • 왜 Aurora MySQL에서도 논리적인 InnoDB 페이지 구조는 중요하지만, 물리적인 내구성·복제·백업 계층은 Community MySQL과 다르게 해석해야 하는가

이 글은 InnoDB의 page size, extent, segment, 그리고 B+Tree 페이지 구성을 내부 메커니즘 중심으로 정리한다. 목표는 단순히 용어를 외우는 것이 아니라, 운영 중 보이는 현상을 페이지 구조로 설명할 수 있게 만드는 것이다.

2. InnoDB 저장 계층의 큰 그림

InnoDB의 저장 구조는 대략 다음과 같은 계층으로 볼 수 있다.

flowchart TD
  SQL[SQL 레이어<br/>테이블·행·인덱스 요청] --> SE[InnoDB 스토리지 엔진]
  SE --> BP[Buffer Pool<br/>페이지 캐시]
  BP --> PAGE[Page<br/>기본 읽기·쓰기 단위]
  PAGE --> EXTENT[Extent<br/>연속 페이지 묶음]
  EXTENT --> SEGMENT[Segment<br/>인덱스/LOB 등 용도별 공간]
  SEGMENT --> TS[Tablespace<br/>.ibd 또는 공유 테이블스페이스]
  SE --> REDO[Redo Log<br/>페이지 변경 내구성]
  SE --> UNDO[Undo Log<br/>MVCC·롤백]

개념적으로는 SQL이 테이블을 읽는 것처럼 보이지만, InnoDB 내부에서는 다음 순서가 중요하다.

  1. 옵티마이저가 사용할 인덱스와 접근 경로를 결정한다.
  2. InnoDB가 해당 인덱스의 B+Tree 루트 페이지부터 필요한 하위 페이지를 찾는다.
  3. 필요한 페이지가 Buffer Pool에 있으면 메모리에서 읽는다.
  4. 없으면 tablespace 파일에서 페이지 단위로 읽어 Buffer Pool에 올린다.
  5. 행 변경은 Buffer Pool 안의 페이지를 dirty page로 만들고, redo log에 변경 이력을 기록한다.
  6. 나중에 checkpoint와 page cleaner가 dirty page를 tablespace에 반영한다.

따라서 InnoDB 성능을 볼 때는 “쿼리가 몇 행을 읽었는가”뿐 아니라 “몇 페이지를 접근했는가”, “그 페이지들이 Buffer Pool에 있었는가”, “페이지가 얼마나 조밀하게 채워져 있는가”를 함께 봐야 한다.

3. Page size: InnoDB의 기본 I/O 단위

3.1 기본값은 16KB

InnoDB page size의 기본값은 일반적으로 16KB다. 이는 innodb_page_size 시스템 변수로 확인할 수 있다.

SELECT @@version AS mysql_version,
       @@innodb_page_size AS innodb_page_size_bytes,
       @@innodb_page_size / 1024 AS innodb_page_size_kb;

실행 결과(MySQL 8.0.46):

mysql> SELECT @@version AS mysql_version,
    ->        @@innodb_page_size AS innodb_page_size_bytes,
    ->        @@innodb_page_size / 1024 AS innodb_page_size_kb;

+---------------+------------------------+---------------------+
| mysql_version | innodb_page_size_bytes | innodb_page_size_kb |
+---------------+------------------------+---------------------+
| 8.0.46        |                  16384 |             16.0000 |
+---------------+------------------------+---------------------+
1 row in set (0.00 sec)

innodb_page_size는 단순한 표시 값이 아니라 InnoDB tablespace 구조의 기본 전제다. 한 번 생성된 인스턴스의 page size는 운영 중 일반 파라미터처럼 변경할 수 없다. page size를 바꾸려면 별도 인스턴스를 초기화하고 논리 백업·복구 또는 덤프·적재 방식으로 이전하는 접근이 필요하다.

3.2 page size가 영향을 주는 영역

Page size는 다음 영역에 영향을 준다.

영역 영향
Buffer Pool 캐시 단위가 page size로 결정된다. 16KB 페이지를 하나 읽으면 Buffer Pool도 16KB 단위로 점유된다.
B+Tree fanout 한 페이지에 들어가는 index record 수가 달라져 B+Tree 높이에 영향을 준다.
I/O 패턴 랜덤 접근 시 최소 읽기 단위와 캐시 낭비 가능성이 달라진다.
압축/ROW_FORMAT 일부 page size 조합에서는 압축·최대 행 크기 제약이 달라진다.
운영 이관 page size가 다른 인스턴스 간에는 물리 tablespace 파일 호환을 기대하면 안 된다.

작은 page size는 작은 랜덤 접근에서 캐시 낭비를 줄일 수 있지만, B+Tree fanout이 줄어 트리 높이가 커질 수 있다. 큰 page size는 순차·범위 접근에 유리할 수 있지만, 작은 행을 드문드문 읽는 워크로드에서는 Buffer Pool 공간을 더 빨리 소모할 수 있다. 대부분의 운영 환경에서는 기본 16KB가 가장 넓게 검증된 선택이다.

4. Extent: 페이지를 묶어서 관리하는 공간 할당 단위

InnoDB는 모든 페이지를 완전히 개별적으로만 관리하지 않는다. 연속된 페이지 묶음인 extent를 사용해 공간을 할당하고 회수한다. 기본 16KB page size에서는 한 extent가 보통 1MB이며, 이는 64개 페이지에 해당한다.

16KB page × 64 pages = 1024KB = 1MB extent

Extent가 중요한 이유는 공간 관리와 단편화 때문이다. 테이블이나 인덱스가 커질 때 InnoDB는 필요한 페이지를 하나씩 임의 위치에 흩뿌리는 것이 아니라, extent 단위의 공간 관리 정책을 사용한다. 작은 객체는 일부 페이지 단위로 시작할 수 있지만, 커지면 extent 단위 할당이 더 중요해진다.

운영 관점에서는 다음 현상을 extent 구조와 연결해서 이해할 수 있다.

  • 대량 INSERT 후 .ibd 파일이 급격히 커진다.
  • 대량 DELETE 후에도 파일 크기가 즉시 줄지 않는다.
  • 삭제된 공간은 같은 tablespace 안에서 재사용될 수 있지만, OS 파일 크기 반환은 별도 재구성 없이는 제한적이다.
  • 인덱스가 단편화되면 논리적으로 같은 범위의 레코드가 여러 페이지와 extent에 흩어질 수 있다.

파일 크기 축소만을 목표로 OPTIMIZE TABLE을 남용하면 잠금, 임시 디스크 사용량, redo/undo 증가, 복제 지연 같은 부작용이 더 클 수 있다. 공간 반환이 필요한지, 단순 내부 재사용이면 충분한지 먼저 판단해야 한다.

5. Segment: 인덱스와 객체별 공간의 논리적 소유자

Segment는 특정 목적을 위해 extent와 page를 소유하는 논리 단위다. InnoDB에서는 인덱스 B+Tree의 leaf/non-leaf 페이지, rollback segment, undo 관련 구조, LOB 저장 공간 등 여러 내부 구조가 segment 개념과 연결된다.

테이블을 하나 만들었다고 해서 물리적으로 “테이블 파일 안에 행만 순서대로 저장”되는 것이 아니다. InnoDB 테이블은 clustered index를 중심으로 구성되며, secondary index가 있으면 각 secondary index도 별도의 B+Tree 구조를 가진다. 각 B+Tree는 필요한 페이지를 할당받아 성장한다.

다음 예제는 논리적으로는 작은 테이블 하나지만, InnoDB가 clustered index와 secondary index를 별도 구조로 관리한다는 점을 관찰하기 위한 기본 재현이다.

DROP TABLE IF EXISTS page_structure_demo;

CREATE TABLE page_structure_demo (
  id BIGINT NOT NULL,
  account_id BIGINT NOT NULL,
  created_at DATETIME NOT NULL,
  amount DECIMAL(12,2) NOT NULL,
  memo VARCHAR(100) NOT NULL,
  PRIMARY KEY (id),
  KEY ix_account_created (account_id, created_at)
) ENGINE=InnoDB;

INSERT INTO page_structure_demo
VALUES
  (1, 100, '2026-05-26 09:00:00', 12000.00, '첫 번째 거래'),
  (2, 100, '2026-05-26 09:05:00',  8500.00, '두 번째 거래'),
  (3, 200, '2026-05-26 09:10:00', 30000.00, '세 번째 거래');

SELECT INDEX_NAME,
       SEQ_IN_INDEX,
       COLUMN_NAME,
       NON_UNIQUE
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
  AND TABLE_NAME = 'page_structure_demo'
ORDER BY INDEX_NAME, SEQ_IN_INDEX;

DROP TABLE page_structure_demo;

실행 결과(MySQL 8.0.46):

mysql> DROP TABLE IF EXISTS page_structure_demo;

Query OK, 0 rows affected (0.01 sec)

mysql> CREATE TABLE page_structure_demo (
    ->   id BIGINT NOT NULL,
    ->   account_id BIGINT NOT NULL,
    ->   created_at DATETIME NOT NULL,
    ->   amount DECIMAL(12,2) NOT NULL,
    ->   memo VARCHAR(100) NOT NULL,
    ->   PRIMARY KEY (id),
    ->   KEY ix_account_created (account_id, created_at)
    -> ) ENGINE=InnoDB;

Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO page_structure_demo
    -> VALUES
    ->   (1, 100, '2026-05-26 09:00:00', 12000.00, '첫 번째 거래'),
    ->   (2, 100, '2026-05-26 09:05:00',  8500.00, '두 번째 거래'),
    ->   (3, 200, '2026-05-26 09:10:00', 30000.00, '세 번째 거래');

Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> SELECT INDEX_NAME,
    ->        SEQ_IN_INDEX,
    ->        COLUMN_NAME,
    ->        NON_UNIQUE
    -> FROM information_schema.STATISTICS
    -> WHERE TABLE_SCHEMA = DATABASE()
    ->   AND TABLE_NAME = 'page_structure_demo'
    -> ORDER BY INDEX_NAME, SEQ_IN_INDEX;

+--------------------+--------------+-------------+------------+
| INDEX_NAME         | SEQ_IN_INDEX | COLUMN_NAME | NON_UNIQUE |
+--------------------+--------------+-------------+------------+
| ix_account_created |            1 | account_id  |          1 |
| ix_account_created |            2 | created_at  |          1 |
| PRIMARY            |            1 | id          |          0 |
+--------------------+--------------+-------------+------------+
3 rows in set (0.01 sec)

mysql> DROP TABLE page_structure_demo;

Query OK, 0 rows affected (0.00 sec)

위 예제에서 PRIMARY는 clustered index다. InnoDB에서는 테이블의 실제 행 데이터가 clustered index leaf page에 저장된다. 반면 ix_account_created는 secondary index이며, leaf page에 secondary key와 함께 primary key 값이 저장된다. Secondary index 탐색 후 실제 행 전체가 필요하면 primary key로 clustered index를 다시 찾아가는 구조가 된다.

이 구조 때문에 secondary index 설계에서는 단순히 “조건 컬럼을 넣었는가”뿐 아니라 다음 질문이 중요하다.

  • secondary index만으로 필요한 컬럼을 모두 읽을 수 있는가
  • 아니면 clustered index lookup이 대량으로 추가되는가
  • primary key가 너무 길어 secondary index leaf page가 불필요하게 커지지는 않는가
  • 인덱스 하나가 추가될 때 INSERT/UPDATE/DELETE가 몇 개의 B+Tree 페이지를 더 변경하는가

6. B+Tree 페이지의 구성

InnoDB 인덱스는 B+Tree 구조다. 여기서 중요한 점은 “인덱스 파일 안에 키가 정렬되어 있다” 정도가 아니라, B+Tree의 각 노드가 InnoDB page라는 점이다.

B+Tree는 크게 root page, internal page, leaf page로 볼 수 있다.

flowchart TD
  R[Root page<br/>상위 키 경계] --> I1[Internal page<br/>하위 페이지 포인터]
  R --> I2[Internal page<br/>하위 페이지 포인터]
  I1 --> L1[Leaf page<br/>key + row 또는 PK]
  I1 --> L2[Leaf page<br/>key + row 또는 PK]
  I2 --> L3[Leaf page<br/>key + row 또는 PK]
  I2 --> L4[Leaf page<br/>key + row 또는 PK]

Clustered index와 secondary index의 leaf page 내용은 다르다.

인덱스 종류 Leaf page에 저장되는 내용
Clustered index (PRIMARY) primary key와 실제 행 데이터
Secondary index secondary key와 해당 행의 primary key 값

따라서 InnoDB에서 primary key는 단순한 제약조건이 아니다. 테이블의 물리적 정렬 기준이자 secondary index가 행을 찾아가기 위해 들고 다니는 주소 역할을 한다. 너무 긴 primary key는 모든 secondary index를 비대하게 만들 수 있다.

6.1 페이지 내부에는 무엇이 있는가

InnoDB index page는 개념적으로 다음 요소를 포함한다.

  • page header: 페이지 번호, 이전/다음 페이지, LSN 등 페이지 관리 정보
  • infimum/supremum pseudo-record: 페이지 내 레코드 경계 표시
  • user records: 실제 인덱스 레코드
  • free space: 향후 삽입을 위한 빈 공간
  • page directory: 페이지 내부 이진 탐색을 돕는 슬롯 정보
  • file trailer: 체크섬 등 페이지 무결성 확인 정보

정확한 바이트 레이아웃은 InnoDB 버전과 페이지 유형에 따라 세부 차이가 있지만, 운영자가 기억해야 할 핵심은 다음이다.

InnoDB는 B+Tree를 “레코드 목록”으로만 관리하지 않고, 각 페이지 안에서 레코드 정렬·빈 공간·디렉터리·체크섬·LSN을 함께 관리한다.

이 때문에 한 행을 삽입할 때도 단순히 파일 끝에 append하는 것이 아니라, 대상 B+Tree leaf page를 찾아 그 페이지 안의 적절한 위치에 레코드를 넣는다. 공간이 부족하면 페이지 분할(page split)이 발생할 수 있다.

7. 페이지 분할과 Primary Key 설계

Primary Key 값이 증가형이면 INSERT는 대체로 B+Tree의 오른쪽 끝 leaf page로 몰린다. 반면 랜덤 UUID 같은 키는 트리 전체의 여러 leaf page에 분산 삽입된다. 분산 자체는 핫스팟을 줄일 수 있지만, InnoDB B+Tree 관점에서는 기존 페이지 중간에 새 레코드를 끼워 넣는 일이 늘어나고 페이지 분할 가능성이 커진다.

페이지 분할이 많아지면 다음 비용이 증가한다.

  • 변경해야 할 페이지 수 증가
  • redo log 기록량 증가
  • Buffer Pool dirty page 증가
  • leaf page 공간 사용률 저하
  • range scan 시 더 많은 페이지 접근
  • secondary index에서도 유사한 분할 비용 발생

그렇다고 모든 환경에서 증가형 숫자 key만 정답이라는 뜻은 아니다. 분산 ID, 샤딩 키, 외부 시스템 ID와의 정합성, 쓰기 핫스팟 회피 같은 요구도 있다. 다만 InnoDB 페이지 구조를 이해하면 key 설계의 비용을 명시적으로 평가할 수 있다.

운영 기준으로는 다음과 같이 판단하는 것이 좋다.

  • OLTP 테이블의 primary key는 가능하면 짧고 안정적인 값을 선택한다.
  • 랜덤 UUID를 primary key로 쓴다면 BINARY(16) 저장, 시간 순서성을 가진 UUID 계열, 별도 surrogate key 등을 검토한다.
  • secondary index가 많은 테이블에서 긴 primary key는 전체 인덱스 크기 증가로 이어진다.
  • 쓰기량이 큰 테이블은 인덱스 개수와 각 인덱스 leaf page 변경 비용을 함께 계산한다.

8. 운영 진단: 현재 인스턴스와 테이블 구조 확인

InnoDB 페이지 구조를 직접 덤프하지 않더라도, 운영자는 SQL만으로 중요한 전제를 확인할 수 있다.

SELECT @@innodb_page_size AS innodb_page_size_bytes,
       @@innodb_file_per_table AS innodb_file_per_table,
       @@version AS mysql_version;

SHOW TABLES FROM information_schema LIKE 'INNODB_TABLES';
SHOW TABLES FROM information_schema LIKE 'INNODB_INDEXES';

CREATE TABLE page_structure_size_demo (
  id BIGINT NOT NULL AUTO_INCREMENT,
  customer_id BIGINT NOT NULL,
  status VARCHAR(20) NOT NULL,
  created_at DATETIME NOT NULL,
  payload VARCHAR(200) NOT NULL,
  PRIMARY KEY (id),
  KEY ix_customer_created (customer_id, created_at),
  KEY ix_status (status)
) ENGINE=InnoDB;

INSERT INTO page_structure_size_demo (customer_id, status, created_at, payload)
VALUES
  (10, 'READY',   '2026-05-26 09:00:00', '작은 예제 데이터'),
  (10, 'DONE',    '2026-05-26 09:01:00', '작은 예제 데이터'),
  (20, 'READY',   '2026-05-26 09:02:00', '작은 예제 데이터'),
  (30, 'FAILED',  '2026-05-26 09:03:00', '작은 예제 데이터');

SELECT TABLE_NAME,
       ENGINE,
       ROW_FORMAT,
       TABLE_ROWS,
       DATA_LENGTH,
       INDEX_LENGTH
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
  AND TABLE_NAME = 'page_structure_size_demo';

DROP TABLE page_structure_size_demo;

실행 결과(MySQL 8.0.46):

mysql> SELECT @@innodb_page_size AS innodb_page_size_bytes,
    ->        @@innodb_file_per_table AS innodb_file_per_table,
    ->        @@version AS mysql_version;

+------------------------+-----------------------+---------------+
| innodb_page_size_bytes | innodb_file_per_table | mysql_version |
+------------------------+-----------------------+---------------+
|                  16384 |                     1 | 8.0.46        |
+------------------------+-----------------------+---------------+
1 row in set (0.00 sec)

mysql> SHOW TABLES FROM information_schema LIKE 'INNODB_TABLES';

+----------------------------------------------+
| Tables_in_information_schema (INNODB_TABLES) |
+----------------------------------------------+
| INNODB_TABLES                                |
+----------------------------------------------+
1 row in set (0.00 sec)

mysql> SHOW TABLES FROM information_schema LIKE 'INNODB_INDEXES';

+-----------------------------------------------+
| Tables_in_information_schema (INNODB_INDEXES) |
+-----------------------------------------------+
| INNODB_INDEXES                                |
+-----------------------------------------------+
1 row in set (0.00 sec)

mysql> CREATE TABLE page_structure_size_demo (...);

Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO page_structure_size_demo (customer_id, status, created_at, payload) VALUES (...);

Query OK, 4 rows affected (0.00 sec)
Records: 4  Duplicates: 0  Warnings: 0

mysql> SELECT TABLE_NAME,
    ->        ENGINE,
    ->        ROW_FORMAT,
    ->        TABLE_ROWS,
    ->        DATA_LENGTH,
    ->        INDEX_LENGTH
    -> FROM information_schema.TABLES
    -> WHERE TABLE_SCHEMA = DATABASE()
    ->   AND TABLE_NAME = 'page_structure_size_demo';

+--------------------------+--------+------------+------------+-------------+--------------+
| TABLE_NAME               | ENGINE | ROW_FORMAT | TABLE_ROWS | DATA_LENGTH | INDEX_LENGTH |
+--------------------------+--------+------------+------------+-------------+--------------+
| page_structure_size_demo | InnoDB | Dynamic    |          4 |       16384 |        32768 |
+--------------------------+--------+------------+------------+-------------+--------------+
1 row in set (0.00 sec)

mysql> DROP TABLE page_structure_size_demo;

Query OK, 0 rows affected (0.00 sec)

이 예제의 DATA_LENGTHINDEX_LENGTH는 작은 테스트 테이블에서는 통계 추정과 최소 할당 단위의 영향을 크게 받는다. 운영 환경에서도 이 값은 바이트 단위의 절대 진실이라기보다, 테이블·인덱스 크기 추세를 보는 지표로 사용하는 편이 안전하다.

대형 운영 테이블에서는 다음과 같은 질문을 함께 던져야 한다.

  • DATA_LENGTH보다 INDEX_LENGTH 증가가 훨씬 빠르지 않은가
  • 사용하지 않는 secondary index가 쓰기 비용과 Buffer Pool 점유를 늘리고 있지 않은가
  • TABLE_ROWS 추정치와 실제 카디널리티가 크게 어긋나 옵티마이저 판단에 영향을 주지 않는가
  • 대량 삭제 후 공간이 내부적으로 재사용 가능한 상태인지, OS 반환이 필요한 상태인지 구분했는가

9. Buffer Pool과 페이지 구조의 관계

Buffer Pool은 InnoDB 페이지 캐시다. 즉, Buffer Pool이 충분하다는 말은 “자주 접근하는 페이지를 메모리에 유지할 수 있다”는 뜻이다. 행 수가 아니라 페이지 수가 핵심이다.

다음과 같은 상황에서는 Buffer Pool 효율이 떨어질 수 있다.

  • primary key 또는 secondary index가 불필요하게 커서 한 페이지에 들어가는 레코드 수가 줄어든다.
  • 쿼리가 covering index를 사용하지 못해 secondary index와 clustered index 페이지를 모두 대량 접근한다.
  • 랜덤 접근 패턴이 강해 read-ahead 효과가 제한된다.
  • 대량 스캔이 자주 접근하는 OLTP 페이지를 밀어낸다.
  • 너무 많은 인덱스가 쓰기 시 dirty page를 과도하게 만든다.

운영자는 단순히 Buffer Pool 크기만 늘리는 대신, “같은 메모리에 더 많은 유효 레코드를 담을 수 있는 구조인가”를 함께 봐야 한다. 짧고 선택도 높은 인덱스, 불필요한 컬럼을 줄인 covering index, 적절한 primary key 설계는 모두 페이지 효율 개선과 연결된다.

10. Aurora MySQL에서의 해석 차이

Aurora MySQL은 MySQL 호환 SQL 계층과 InnoDB 기반 실행 특성을 제공하지만, 저장소 계층은 Community MySQL과 다르다. Aurora는 분산 스토리지 계층에 redo 중심으로 변경을 전파하고, 여러 AZ에 복제된 스토리지 볼륨을 사용한다. 따라서 .ibd 파일과 로컬 디스크 I/O만으로 모든 내구성·복제 동작을 설명하는 것은 부정확하다.

그러나 다음 이유로 InnoDB page, B+Tree, Buffer Pool 이해는 Aurora에서도 여전히 중요하다.

  • SQL 실행은 여전히 인덱스 B+Tree 접근 비용의 영향을 받는다.
  • Buffer Pool 적중률과 working set 크기는 Aurora에서도 성능에 중요하다.
  • primary key와 secondary index 설계가 페이지 접근 수와 쓰기 증폭에 영향을 준다.
  • 페이지 분할과 인덱스 비대화는 Aurora 스토리지 I/O 비용 및 지연에도 영향을 줄 수 있다.
  • 장애조치 후 새 writer/reader의 캐시 워밍 상태에 따라 지연이 달라질 수 있다.

다만 Aurora에서 파일 크기, 백업, 복제 지연, 스토리지 반환을 해석할 때는 Community MySQL의 로컬 파일 관점만 적용하지 말아야 한다. Aurora의 백업·스토리지·복제 메커니즘은 별도 계층으로 이해해야 한다.

11. 흔한 오해와 주의사항

11.1 “행 하나만 읽으니 I/O도 행 하나만 발생한다”

InnoDB는 페이지 단위로 읽고 쓴다. 행 하나를 찾더라도 해당 행이 들어 있는 페이지가 필요하다. 인덱스 탐색 과정에서는 root, internal, leaf 페이지가 필요할 수 있고, secondary index lookup 후 clustered index 페이지까지 추가될 수 있다.

11.2 “대량 DELETE 후 파일이 줄지 않았으니 삭제가 실패했다”

삭제는 레코드를 제거하거나 purge 대상 상태로 만들고, 공간은 tablespace 내부에서 재사용될 수 있다. OS 파일 크기 반환은 별도의 테이블 재구성이 필요할 수 있다. 파일 크기만 보고 실패로 판단하면 불필요한 OPTIMIZE TABLE을 수행할 위험이 있다.

11.3 “인덱스는 많을수록 안전하다”

각 secondary index는 별도의 B+Tree이며, 쓰기 작업마다 유지 비용이 발생한다. 사용하지 않는 인덱스는 Buffer Pool 점유, redo log 증가, 페이지 분할, 통계 관리 비용을 만든다.

11.4 “Primary Key는 아무 값이나 유일하면 된다”

InnoDB의 primary key는 clustered index다. 실제 행의 물리적 정렬 기준이며, secondary index leaf page에도 포함된다. 너무 길거나 랜덤성이 강한 primary key는 전체 저장 구조에 영향을 준다.

11.5 “테이블 크기 지표는 항상 정확한 행 단위 측정이다”

information_schema.TABLESTABLE_ROWS, DATA_LENGTH, INDEX_LENGTH는 InnoDB에서 추정과 통계에 기반할 수 있다. 추세와 비교 지표로는 유용하지만, 단일 시점의 절대값처럼 과신하면 안 된다.

12. 운영 체크리스트

  • 신규 MySQL 인스턴스 생성 시 innodb_page_size
  • DATA_LENGTH, INDEX_LENGTH

13. 결론

InnoDB의 page, extent, segment, B+Tree는 추상적인 내부 구현 세부사항이 아니다. 운영자가 매일 마주치는 인덱스 크기, Buffer Pool 효율, 대량 삭제 후 공간 문제, primary key 설계, 랜덤 쓰기 비용, Aurora 성능 해석의 기반이다.

핵심은 단순하다. InnoDB는 SQL의 행과 테이블을 페이지 기반 B+Tree와 tablespace 구조로 구현한다. 따라서 좋은 스키마와 인덱스 설계는 단순히 쿼리 조건에 맞는 컬럼을 고르는 일이 아니라, 제한된 페이지와 Buffer Pool 안에 어떤 데이터를 얼마나 조밀하고 예측 가능하게 배치할지 결정하는 일이다.

다음 글에서는 이 페이지 구조를 바탕으로 Buffer Pool의 LRU, dirty page, checkpoint, page cleaner가 실제 운영 성능과 장애 복구 시간에 어떤 영향을 주는지 더 구체적으로 살펴볼 수 있다.