Real MySQL - 파티션
들어가며
이 글은 Real MySQL 8.0의 13장 파티션을 읽고 정리한 내용이다. 파티션의 개념, 내부 동작 원리, 주의사항, 그리고 각 파티션 종류별 특징을 다룬다.
파티션은 하나의 테이블을 물리적으로 여러 개로 분리하여 저장하는 기법 이다.
논리적으로는 하나의 테이블이지만, 내부적으로는 파티션 키를 기준으로 데이터가 나뉘어 저장된다. 사용자는 하나의 테이블처럼 쿼리하면 되고, MySQL이 적절한 파티션을 찾아 처리한다.
파티션을 사용하는 이유
단일 INSERT와 단일 또는 범위 SELECT의 빠른 처리
테이블의 데이터가 많아지면 인덱스의 크기도 커진다. 인덱스가 메모리에 다 올라가지 못하면 디스크 I/O가 발생하고, 이는 성능 저하로 이어진다.
파티셔닝을 하면 데이터와 인덱스가 파티션 단위로 분리된다. 특정 파티션만 조회하는 쿼리라면 해당 파티션의 인덱스만 사용하면 되므로, 워킹 셋(Working Set) 이 줄어들어 메모리 효율이 높아진다.
데이터의 물리적인 저장소를 분리
파티션별로 다른 디스크에 저장할 수 있다. 자주 조회되는 최신 데이터는 SSD에, 거의 조회되지 않는 과거 데이터는 HDD에 저장하는 식으로 비용을 최적화할 수 있다.
이력 데이터의 효율적인 관리
로그나 이력 데이터처럼 일정 기간이 지나면 삭제해야 하는 데이터에 파티셔닝이 특히 유용하다. DELETE로 수천만 건을 지우려면 시간도 오래 걸리고 서버 부하도 크다. 하지만 DROP PARTITION은 해당 파티션의 데이터 파일만 삭제하면 되므로 거의 즉시 완료 된다.
MySQL 파티션의 내부 처리
MySQL이 파티션 테이블을 어떻게 처리하는지 이해하면 쿼리 최적화에 도움이 된다.
파티션 테이블의 레코드 INSERT
새로운 레코드가 들어오면 MySQL은 파티션 키 값을 계산하여 어느 파티션에 저장할지 결정한다. 해당 파티션에만 레코드가 저장되고, 다른 파티션은 건드리지 않는다.
파티션 테이블의 UPDATE
UPDATE는 두 가지 경우로 나뉜다.
1. 파티션 키가 변경되지 않는 경우 : 일반 테이블과 동일하게 해당 파티션에서 레코드를 찾아 업데이트한다.
2. 파티션 키가 변경되는 경우 : 기존 파티션에서 레코드를 삭제하고, 새로운 파티션에 레코드를 삽입한다. 내부적으로 DELETE + INSERT가 발생하므로 비용이 크다.
파티션 테이블의 검색
파티션 테이블 검색에서 가장 중요한 것은 파티션 프루닝(Partition Pruning) 이다. WHERE 절의 조건에 따라 어떤 파티션을 읽어야 하는지 결정되고, 필요 없는 파티션은 아예 접근하지 않는다.
파티션 프루닝이 동작하려면 WHERE 절에 파티션 키 컬럼이 포함 되어야 한다. 파티션 키가 없으면 모든 파티션을 스캔해야 하므로 오히려 성능이 나빠질 수 있다.
검색 성능은 다음 두 가지 조건의 조합에 따라 달라진다:
| 파티션 선택 가능 | 인덱스 사용 가능 | 결과 |
|---|---|---|
| O | O | 가장 효율적 |
| O | X | 특정 파티션 풀 스캔 |
| X | O | 모든 파티션에서 인덱스 스캔 |
| X | X | 모든 파티션 풀 스캔 (최악) |
파티션 테이블의 인덱스 스캔과 정렬
파티션 테이블에서 인덱스를 스캔할 때는 각 파티션의 인덱스를 개별적으로 스캔 한다. 여러 파티션에 걸친 쿼리라면 각 파티션에서 정렬된 결과를 가져온 후 머지(Merge) 하는 방식으로 최종 결과를 만든다.
이 과정은 내부적으로 우선순위 큐(Priority Queue)를 사용한다. 각 파티션에서 가져온 결과 중 가장 작은(또는 큰) 값을 순서대로 꺼내는 방식이다. 추가적인 정렬 작업 없이도 정렬된 결과를 얻을 수 있지만, 파티션 개수가 많으면 머지 비용이 증가한다.
주의사항
파티셔닝은 강력하지만 제약사항과 주의할 점이 많다. 도입 전에 반드시 확인해야 한다.
파티션의 제약 사항
스토어드 루틴, UDF, 사용자 변수 등을 파티션 표현식에 사용할 수 없다. 파티션 키 계산은 MySQL이 내부적으로 수행하는데, 이런 동적 요소가 포함되면 일관성을 보장할 수 없기 때문이다.
파티션 표현식은 컬럼 자체 또는 MySQL 내장 함수만 사용할 수 있다. 그것도 일부 함수만 지원된다. TO_DAYS(), TO_SECONDS(), YEAR(), MONTH(), DAY() 등이 대표적이다.
PK와 UNIQUE 인덱스에는 파티션 키 컬럼이 반드시 포함되어야 한다. 이는 MySQL의 중요한 제약사항이다. 파티션 테이블에서 유니크 제약을 보장하려면 모든 파티션을 검사해야 하는데, 파티션 키가 포함되어 있으면 특정 파티션만 검사하면 되기 때문이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
-- 잘못된 예: 파티션 키(reg_date)가 PK에 없음
CREATE TABLE users (
id BIGINT PRIMARY KEY,
reg_date DATE
) PARTITION BY RANGE (YEAR(reg_date)) (...);
-- 올바른 예
CREATE TABLE users (
id BIGINT,
reg_date DATE,
PRIMARY KEY (id, reg_date)
) PARTITION BY RANGE (YEAR(reg_date)) (...);
파티션 테이블에서는 Foreign Key를 사용할 수 없다.
파티션 테이블에서는 Full-Text 인덱스를 생성할 수 없다.
공간 데이터 타입(POINT, GEOMETRY 등)의 컬럼에는 파티션 테이블을 사용할 수 없다.
임시 테이블(Temporary Table)은 파티션을 사용할 수 없다.
그 외에도 자세한 사항들은 이 공식문서를 참고해보자.
파티션 사용 시 주의사항
파티션과 유니크 키(PK 포함)
앞서 언급한 것처럼 PK와 UNIQUE 인덱스에는 파티션 키가 반드시 포함되어야 한다. 이 제약 때문에 기존 테이블에 파티셔닝을 적용하기 어려운 경우가 많다.
예를 들어 id만 PK로 사용하던 테이블을 created_at 기준으로 파티셔닝하려면, PK를 (id, created_at)으로 변경해야 한다. 이는 테이블 구조 변경이 필요하고, 애플리케이션 코드에도 영향을 줄 수 있다.
파티션과 open_files_limit
파티션 테이블은 파티션 개수만큼 파일 핸들을 사용한다. 파티션이 많은 테이블이 여러 개 있으면 open_files_limit 설정을 넉넉하게 잡아야 한다. 기본값으로는 부족할 수 있다.
파티션 테이블과 잠금
파티션 테이블에서 쿼리를 실행하면, 실제로 접근하는 파티션뿐 아니라 모든 파티션에 잠금이 걸릴 수 있다. MySQL 8.0에서는 파티션 프루닝이 먼저 수행되어 필요한 파티션에만 잠금이 걸리도록 개선되었지만, 버전에 따라 동작이 다를 수 있으므로 주의해야 한다.
MySQL 파티션의 종류
MySQL은 RANGE, LIST, HASH, KEY 네 가지 기본 파티션 타입을 지원한다. 각각의 특징과 사용 시나리오가 다르다.
레인지 파티션 (RANGE)
파티션 키 값의 연속된 범위 로 파티션을 정의한다. 날짜나 숫자처럼 순서가 있는 값에 적합하다.
1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE orders (
id BIGINT NOT NULL,
order_date DATE NOT NULL,
amount INT,
PRIMARY KEY (id, order_date)
)
PARTITION BY RANGE (YEAR(order_date)) (
PARTITION p2022 VALUES LESS THAN (2023),
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
사용하기 적합한 경우
- 날짜 기반으로 데이터가 누적되는 테이블 (로그, 주문, 이력 등)
- 특정 기간 데이터를 자주 삭제해야 하는 경우
- 범위 검색이 많은 경우
MAXVALUE 파티션 : 정의된 범위를 벗어나는 값이 들어오면 에러가 발생한다.
VALUES LESS THAN MAXVALUE로 마지막 파티션을 정의해두면 예상치 못한 값도 저장할 수 있다.
레인지 파티션의 관리
파티션 추가 : 가장 마지막 파티션 뒤에만 추가할 수 있다. MAXVALUE 파티션이 있으면 먼저 재구성해야 한다.
1
2
3
4
5
6
7
8
9
10
-- MAXVALUE 파티션이 없는 경우
ALTER TABLE orders ADD PARTITION (
PARTITION p2025 VALUES LESS THAN (2026)
);
-- MAXVALUE 파티션이 있는 경우, REORGANIZE로 분할
ALTER TABLE orders REORGANIZE PARTITION p_future INTO (
PARTITION p2025 VALUES LESS THAN (2026),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
파티션 삭제 : 오래된 데이터를 빠르게 삭제할 때 유용하다.
1
ALTER TABLE orders DROP PARTITION p2022;
파티션 병합 : 여러 파티션을 하나로 합칠 수 있다.
1
2
3
ALTER TABLE orders REORGANIZE PARTITION p2022, p2023 INTO (
PARTITION p_old VALUES LESS THAN (2024)
);
리스트 파티션 (LIST)
파티션 키 값을 개별적으로 나열 하여 파티션을 정의한다. 카테고리나 지역처럼 이산적인 값에 적합하다.
1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE users (
id BIGINT NOT NULL,
region VARCHAR(10) NOT NULL,
name VARCHAR(100),
PRIMARY KEY (id, region)
)
PARTITION BY LIST COLUMNS (region) (
PARTITION p_seoul VALUES IN ('서울', '경기', '인천'),
PARTITION p_busan VALUES IN ('부산', '울산', '경남'),
PARTITION p_etc VALUES IN ('대구', '광주', '대전', '강원', '제주')
);
사용하기 적합한 경우
- 코드 값이나 카테고리로 데이터를 분류하는 경우
- 특정 그룹의 데이터를 자주 조회하는 경우
LIST 파티션에서는 MAXVALUE 같은 기본 파티션이 없다. 정의되지 않은 값이 들어오면 에러가 발생하므로, 가능한 모든 값을 나열해야 한다.
리스트 파티션의 관리
레인지 파티션과 달리 파티션 추가에 순서 제약이 없다. 언제든 새로운 값 목록으로 파티션을 추가할 수 있다.
1
2
3
ALTER TABLE users ADD PARTITION (
PARTITION p_jeju VALUES IN ('제주')
);
파티션 삭제와 병합은 레인지 파티션과 동일하다.
해시 파티션 (HASH)
파티션 키 값을 해시 함수에 넣어 나머지 연산(MOD) 으로 파티션을 결정한다. 데이터를 균등하게 분산시키고 싶을 때 사용한다.
1
2
3
4
5
6
7
8
CREATE TABLE sessions (
id BIGINT NOT NULL,
user_id INT NOT NULL,
data TEXT,
PRIMARY KEY (id, user_id)
)
PARTITION BY HASH (user_id)
PARTITIONS 4;
user_id를 4로 나눈 나머지 값(0, 1, 2, 3)에 따라 파티션이 결정된다.
사용하기 적합한 경우
- 특정 컬럼에 데이터가 편중되지 않도록 균등 분산이 필요한 경우
- 범위 검색보다 특정 값 조회가 많은 경우
- 레인지나 리스트로 나누기 애매한 경우
주의할 점
- 범위 검색에는 파티션 프루닝이 동작하지 않는다
- 파티션 개수를 변경하면 모든 데이터가 재배치 된다
해시 파티션의 관리
파티션 추가 : 파티션 개수가 늘어나면 해시 결과가 달라지므로 데이터 재배치가 발생한다.
1
ALTER TABLE sessions ADD PARTITION PARTITIONS 2; -- 4개 → 6개
파티션 삭제 : 마찬가지로 데이터 재배치가 발생한다.
1
ALTER TABLE sessions COALESCE PARTITION 2; -- 6개 → 4개
해시 파티션에서 파티션 추가/삭제는 대량의 데이터 이동을 유발하므로 서비스 영향을 고려해야 한다.
키 파티션 (KEY)
해시 파티션과 유사하지만, MySQL이 내부적으로 해시 함수를 결정 한다. 사용자가 해시 함수를 지정할 수 없고, MySQL의 MD5() 함수 기반의 해시를 사용한다.
1
2
3
4
5
6
7
CREATE TABLE logs (
id BIGINT NOT NULL,
message TEXT,
PRIMARY KEY (id)
)
PARTITION BY KEY (id)
PARTITIONS 4;
해시 파티션과의 차이
- 해시 파티션: 정수 타입 컬럼만 사용 가능
- 키 파티션: 대부분의 타입 사용 가능 (문자열, 날짜 등)
파티션 키를 명시하지 않으면 PK를 사용하고, PK가 없으면 UNIQUE 키를 사용한다.
1
2
3
-- PK를 파티션 키로 사용
PARTITION BY KEY ()
PARTITIONS 4;
리니어 해시 파티션 / 리니어 키 파티션
일반 해시/키 파티션은 파티션 개수가 변경되면 모든 데이터가 재배치된다. 리니어(Linear) 파티션은 파워 오브 투(Power of Two) 알고리즘 을 사용하여 재배치되는 데이터 양을 줄인다.
1
2
3
4
5
6
7
CREATE TABLE sessions (
id BIGINT NOT NULL,
user_id INT NOT NULL,
PRIMARY KEY (id, user_id)
)
PARTITION BY LINEAR HASH (user_id)
PARTITIONS 4;
장점
- 파티션 추가/삭제 시 일부 파티션의 데이터만 재배치
- 대용량 테이블에서 파티션 관리가 용이
단점
- 데이터 분포가 일반 해시보다 덜 균등할 수 있음
파티션 변경이 잦고 데이터가 많다면 리니어 파티션을 고려해볼 만하다.
파티션 테이블의 쿼리 성능
파티션 테이블의 성능은 파티션 프루닝이 얼마나 잘 동작하느냐 에 달려있다.
파티션 프루닝이 잘 동작하는 경우
1
2
3
4
5
6
7
8
-- 레인지 파티션: 범위 조건에 파티션 키 사용
SELECT * FROM orders WHERE order_date BETWEEN '2024-01-01' AND '2024-12-31';
-- 리스트 파티션: 목록 조건에 파티션 키 사용
SELECT * FROM users WHERE region IN ('서울', '경기');
-- 해시 파티션: 등호 조건에 파티션 키 사용
SELECT * FROM sessions WHERE user_id = 12345;
파티션 프루닝이 동작하지 않는 경우
1
2
3
4
5
6
7
8
-- 파티션 키가 WHERE 절에 없음
SELECT * FROM orders WHERE amount > 10000;
-- 파티션 키에 함수를 적용 (해시 파티션)
SELECT * FROM sessions WHERE user_id + 1 = 12345;
-- 해시 파티션에서 범위 검색
SELECT * FROM sessions WHERE user_id BETWEEN 100 AND 200;
파티션 개수와 성능
파티션 개수가 너무 많으면 오히려 성능이 저하될 수 있다. MySQL 매뉴얼에서는 1000개 미만 을 권장한다. 파티션마다 파일 핸들을 열어야 하고, 머지 비용도 증가하기 때문이다.
파티션 프루닝으로 1~2개 파티션만 읽는다면 파티션이 많아도 괜찮지만, 여러 파티션에 걸친 쿼리가 많다면 파티션 개수를 적게 유지하는 것이 좋다.
마치며
MySQL 파티셔닝은 대용량 테이블을 효율적으로 관리하기 위한 강력한 도구다. 핵심 개념을 정리하면 다음과 같다.
파티션 프루닝 이 가장 중요하다. WHERE 절에 파티션 키가 포함되어야 필요한 파티션만 스캔한다. 파티션 키가 없는 쿼리가 많다면 파티셔닝의 의미가 없다.
PK/UNIQUE에 파티션 키 포함 필수 라는 제약사항을 기억하자. 기존 테이블에 파티셔닝을 적용할 때 가장 큰 걸림돌이 된다.
파티션 종류 선택 은 데이터 특성과 쿼리 패턴에 따라 결정한다.
- 날짜/숫자 범위 검색이 많으면 → RANGE
- 카테고리/코드 값으로 분류하면 → LIST
- 균등 분산이 필요하면 → HASH 또는 KEY
제한사항 을 확인하자. FK, Full-Text, 공간 타입을 사용하는 테이블은 파티셔닝할 수 없다.
References
- Real MySQL 8.0
- MySQL Partitioning Documentation
