단일 테이블의 데이터가 수천만 건을 넘어 수억 건에 도달하면, 아무리 인덱스를 정교하게 설계하더라도 물리적인 I/O 한계에 부딪히게 됩니다. B-Tree 인덱스의 깊이(Depth)가 깊어지고, 워킹 셋(Working Set)의 크기가 innodb_buffer_pool_size를 초과하면서 디스크 스왑(Swap)이 빈번해지기 때문입니다.
이때 샤딩(Sharding)이라는 무거운 아키텍처 변경 전에 반드시 검토해야 할 엔지니어링 기법이 바로 MySQL 파티셔닝(Partitioning)입니다. 이번 포스팅에서는 InnoDB 스토리지 엔진 관점에서의 파티셔닝 동작 원리와 각 파티션의 종류, 특히 데이터 균등 분산의 핵심인 HASH 파티션의 장단점과 실무 관리 방법을 심도 있게 다루어 보겠습니다.
1. MySQL 파티셔닝의 내부 작동 원리 (Architecture)
파티셔닝은 "논리적으로는 하나의 테이블이지만, 물리적으로는 여러 개의 독립된 테이블스페이스(.ibd)로 분할하여 저장하는 기법"입니다.
과거 MySQL 5.7 이전에는 스토리지 엔진 상위의 핸들러(ha_partition)가 파티셔닝을 제어했지만, MySQL 8.0부터는 네이티브 파티셔닝(Native Partitioning)이 완전히 정착되어 InnoDB 스토리지 엔진 자체가 파티션을 직접 제어합니다.
핵심 메커니즘 2가지
- 독립적인 B-Tree 구조: 파티션을 4개로 나누면, 클러스터드 인덱스(PK)와 세컨더리 인덱스 모두 4개의 독립된 B-Tree로 쪼개집니다. 인덱스 트리의 Depth가 낮아져 탐색 비용이 감소하고, 자주 쓰이는 파티션의 인덱스만 버퍼 풀(Buffer Pool)에 적재하여 메모리 효율을 극대화할 수 있습니다.
- 파티션 프루닝 (Partition Pruning): 옵티마이저가 쿼리의 WHERE 절을 분석하여, 읽지 않아도 되는 물리적 파티션을 실행 계획 수립 단계에서 아예 배제해 버리는 최적화 기법입니다.
실무 제약사항 (Primary Key와 파티션 키의 관계)
MySQL 파티셔닝에서 가장 중요한 규칙은 "파티션 키는 반드시 테이블의 모든 Unique Key(Primary Key 포함)의 일부여야 한다"는 점입니다. InnoDB는 유니크 제약을 검증할 때 단일 파티션 내부만 스캔하도록 설계되어 있습니다. 만약 파티션 키가 PK에 포함되지 않으면, 데이터 INSERT 시 중복 검사를 위해 모든 파티션을 뒤져야 하므로 아키텍처 레벨에서 이를 금지하고 있습니다.
2. MySQL 4대 파티션 종류 및 특징 비교
데이터의 분포 특성과 애플리케이션의 조회 패턴(Access Pattern)에 따라 아래 4가지 파티션 중 하나를 선택해야 합니다.
| 파티션 종류 | 분할 기준 | 주요 사용 사례 (Use Case) | 파티션 추가/삭제 방식 |
| RANGE | 연속된 값의 범위 (VALUES LESS THAN) | 일별/월별 로그, 주문 이력 등 시계열 데이터 | ADD / DROP PARTITION (매우 빠름) |
| LIST | 특정 값의 목록 (VALUES IN) | 국가 코드, 카테고리, 지역별 지점 데이터 등 고정된 값 | ADD / DROP PARTITION |
| HASH | 해시 함수 결과값 (MOD 연산 기반) | 범위나 고정 목록으로 나누기 모호한 회원 테이블 등 균등 분산이 필요할 때 | COALESCE / ADD PARTITION (데이터 재배치 발생) |
| KEY | MySQL 내부 해시 함수 (MD5/자체 알고리즘) | 파티션 키가 정수형이 아닌 문자열(VARCHAR) 등이거나 복합 키일 때 | COALESCE / ADD PARTITION (데이터 재배치 발생) |
① RANGE 파티션
실무에서 가장 많이 사용되는 형태입니다. 날짜 컬럼을 기준으로 월별 또는 연도별로 파티션을 나누며, 보관 주기가 지난 과거 데이터를 삭제할 때 DELETE 대신 ALTER TABLE ... DROP PARTITION을 사용하여 Undo Log 발생 없이 즉각적으로 디스크 공간을 회수할 수 있습니다.
② LIST 파티션
특정 코드값이나 지역 구분처럼 이산적인(Discrete) 값들을 그룹핑할 때 사용합니다. 단, 정의되지 않은 값이 INSERT 될 경우 에러가 발생하므로(MySQL 8.0 기준 DEFAULT 파티션 미지원), 애플리케이션 레벨에서 들어오는 값의 종류가 명확히 통제되어야 합니다.
③ KEY 파티션
HASH 파티션과 목적은 같지만, 사용자가 해시 함수를 정의하지 않고 MySQL 서버가 내부적으로 해싱 알고리즘을 수행합니다. 정수형 표현식만 허용하는 일반 HASH와 달리 문자열이나 다중 컬럼을 그대로 지정할 수 있다는 장점이 있습니다.
3. HASH 파티션 심층 분석: 작동 원리와 장단점
시간의 흐름에 따라 쌓이는 로그성 데이터가 아니라, user_id나 device_id처럼 특정 범위로 묶기 애매하지만 데이터 크기가 비대해지는 마스터성 테이블에는 HASH 파티션이 사용됩니다.
작동 원리: 일반 HASH vs LINEAR HASH
- 일반 HASH 파티션: N = MOD(expr, num_partitions) 공식을 사용합니다. 예를 들어 파티션이 4개이고 user_id가 10이면 10 % 4 = 2번 파티션에 저장됩니다. 데이터 분산도가 매우 균일하지만, 파티션 개수를 늘리거나 줄일 때 전체 데이터의 나머지 연산 결과가 바뀌므로 거의 모든 파티션의 데이터를 재구축(Rebuild)해야 합니다.
- LINEAR HASH 파티션: 거듭제곱 기반의 리니어 해싱(Linear Hashing) 알고리즘(비트 연산)을 사용합니다. 파티션 개수를 변경할 때 전체가 아닌 일부 파티션만 병합/분할하므로 관리 속도가 빠르지만, 파티션 개수가 2의 거듭제곱($2^n$)이 아닐 경우 데이터 쏠림(Skewness) 현상이 발생할 수 있습니다.
HASH 파티션의 장점
- 데이터의 완벽한 균등 분산: RANGE 파티션은 특정 시기(예: 블랙프라이데이)에 데이터가 몰리면 특정 파티션만 비대해지는 현상이 발생하지만, HASH 파티션은 모든 파티션의 크기를 거의 동일하게 유지합니다.
- 특정 파티션의 I/O 병목(Hotspot) 방지: 최근 데이터에만 쓰기가 집중되는 시계열 파티션과 달리, INSERT/UPDATE 트랜잭션이 모든 파티션 파일에 고르게 분산되어 버퍼 풀 경합을 줄여줍니다.
- 파티션 관리 자동화 불필요: RANGE 파티션처럼 매달 새로운 파티션을 미리 생성(ADD PARTITION)해 주는 배치 스크립트를 운영할 필요가 없습니다.
HASH 파티션의 단점 - 실무 주의사항
- 범위 검색(Range Scan) 시 파티션 프루닝 불가: 해시 함수 특성상 연속된 값들이 서로 다른 파티션으로 흩어집니다. 따라서 WHERE user_id BETWEEN 100 AND 200 같은 범위 조건 쿼리를 실행하면 옵티마이저는 어느 파티션에 데이터가 있는지 알 수 없어 모든 파티션을 뒤지는 Full Partition Scan을 수행합니다.
- 오래된 데이터 일괄 삭제 불가: 데이터가 전 파티션에 골고루 섞여 있으므로 DROP PARTITION을 통한 빠른 데이터 아카이빙이 불가능합니다.
- 파티션 리사이징 비용: 데이터가 커져서 파티션을 4개에서 8개로 늘릴 때, 기존 데이터의 해시 버킷 위치가 재계산되면서 막대한 디스크 I/O와 테이블 잠금(또는 Online DDL 부하)이 발생합니다.
4. HASH 파티션 설정 및 실무 관리 방법
실무에서 HASH 파티션을 생성하고, 실행 계획을 검증하며, 운영 중 파티션을 확장/축소하는 구체적인 방법을 살펴보겠습니다.
① HASH 파티션 테이블 생성 (복합 PK 구성)
파티션 키(user_id)를 반드시 Primary Key에 포함시켜야 합니다. MySQL 8.0부터는 UNIX_TIMESTAMP() 등 일부 함수를 제외한 복잡한 표현식보다 순수 정수형 컬럼 자체를 해시 키로 쓰는 것이 프루닝에 유리합니다.
CREATE TABLE tb_user_activity (
activity_id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
action_type VARCHAR(50) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- 제약사항: 파티션 키인 user_id가 반드시 PK에 포함되어야 함
PRIMARY KEY (activity_id, user_id),
KEY idx_user_created (user_id, created_at)
) ENGINE=InnoDB
PARTITION BY HASH(user_id)
PARTITIONS 8;
② 실행 계획을 통한 파티션 프루닝(Pruning) 검증
쿼리가 의도한 대로 단일 파티션만 스캔하는지 EXPLAIN의 partitions 항목을 통해 반드시 확인해야 합니다.
-- 1. 동등 비교 (=) 또는 IN 조건: 파티션 프루닝 정상 작동
EXPLAIN SELECT * FROM tb_user_activity WHERE user_id = 1024;
| id | select_type | table | partitions | type | key | rows |
| 1 | SIMPLE | tb_user_activity | p0 | ref | idx_user_created | 15 |
1024 % 8 = 0이므로 옵티마이저가 정확히 p0 파티션 하나만 접근한 것을 확인할 수 있습니다.
-- 2. 범위 비교 (BETWEEN, >, <): 파티션 프루닝 실패 (주의!)
EXPLAIN SELECT * FROM tb_user_activity WHERE user_id BETWEEN 1000 AND 1010;
| id | select_type | table | partitions | type | key | rows |
| 1 | SIMPLE | tb_user_activity | p0,p1,p2,p3,p4,p5,p6,p7 | range | idx_user_created | 120 |
범위는 해시 결과를 특정할 수 없으므로 p0부터 p7까지 8개 파티션의 인덱스 트리를 모두 탐색합니다.
이 경우 파티셔닝을 하지 않은 단일 테이블보다 오히려 성능이 저하됩니다.
③ 파티션 개수 조정 (확장 및 축소)
RANGE 파티션과 달리 HASH 파티션은 DROP PARTITION 구문을 사용할 수 없으며, 아래의 전용 구문을 통해 크기를 조절하고 데이터를 재배치해야 합니다.
-- 1. 파티션 개수 늘리기 (8개 -> 12개로 4개 추가)
ALTER TABLE tb_user_activity ADD PARTITION PARTITIONS 4;
-- 2. 파티션 개수 줄이기 (12개 -> 8개로 4개 병합/축소)
ALTER TABLE tb_user_activity COALESCE PARTITION 4;
운영 중 DDL 실행 시 주의점
ADD PARTITION이나 COALESCE PARTITION은 내부적으로 데이터를 다시 읽고 쓰는 리빌드(Rebuild) 작업을 동반합니다. 수억 건의 테이블에 바로 실행하면 마스터-슬레이브 복제 지연(Replication Lag)과 디스크 I/O 포화를 유발하므로, 실무에서는 트래픽이 적은 새벽 시간대에 수행하거나 pt-online-schema-change 또는 gh-ost 같은 Online Schema Change 도구를 활용하여 무중단으로 마이그레이션해야 합니다.
④ 파티션별 데이터 분포도 모니터링
HASH 파티션이 실제로 데이터를 고르게 분산시키고 있는지 확인하려면 information_schema.PARTITIONS 뷰를 조회합니다.
SELECT
PARTITION_NAME,
TABLE_ROWS,
ROUND(DATA_LENGTH / (1024 * 1024), 2) AS DATA_SIZE_MB,
ROUND(INDEX_LENGTH / (1024 * 1024), 2) AS INDEX_SIZE_MB
FROM information_schema.PARTITIONS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'tb_user_activity';
5. 마무리 :실무에서 마주하는 함정과 해결책
파티셔닝은 무조건 정답은 아닙니다. 잘못 적용하면 오히려 쿼리 응답 속도가 수 배 느려지는 장애를 겪게 됩니다. 도입 전 아래 두 가지를 반드시 점검하시기 바랍니다.
- 파티션 키가 없는 세컨더리 인덱스 조회의 비효율성 (Scatter-Gather) 위 예시 테이블에서 user_id 조건 없이 WHERE action_type = 'LOGIN'으로 조회한다면 어떻게 될까요? 옵티마이저는 파티션 프루닝을 포기하고 8개의 파티션을 모두 열어 각각의 B-Tree를 탐색한 뒤 결과를 병합합니다. 파티션 개수가 100개라면 1번의 인덱스 탐색으로 끝날 일을 100번의 인덱스 루트-리프 탐색으로 늘리는 셈입니다. 전체 조회 쿼리의 90% 이상이 파티션 키를 동반할 때만 파티셔닝을 도입해야 합니다.
- open_files_limit 및 테이블 캐시 관리 파티션 1개는 물리적으로 1개의 .ibd 파일입니다. 테이블 100개에 각각 100개의 파티션을 만들면 MySQL 프로세스가 열어야 하는 파일 디스크립터 수가 순식간에 10,000개로 늘어납니다. 따라서 파티션을 적극적으로 사용하는 환경에서는 my.cnf의 open_files_limit과 table_open_cache, innodb_open_files 파라미터를 충분히 높여주어야 파일 핸들 부족으로 인한 에러를 예방할 수 있습니다.
