테이블 파티셔닝
테이블 파티셔닝은 논리적으로 하나인 큰 테이블을 파티션 키 기준으로 더 작은 물리 조각(파티션)으로 나누어 저장하는 방법입니다. 응용 프로그램은 여전히 테이블 하나를 조회하지만, 데이터베이스는 행을 키 값에 맞는 파티션에 넣고, WHERE에 파티션 키 조건이 있으면 조회할 때 필요한 파티션만 읽습니다.
PostgreSQL 문서는 파티셔닝의 이점으로 자주 쓰는 행이 한두 파티션에 모여 있을 때의 조회 성능, 파티션을 붙이고 떼는 방식으로 하는 대량 적재와 삭제, 잘 쓰지 않는 데이터의 저렴한 저장소 이동을 듭니다. 이 토픽에서는 Range, List, Hash 세 분할 방식, 파티션 프루닝, 파티셔닝과 샤딩의 차이를 시각화로 확인합니다.
핵심 개념
파티션과 파티션 키
테이블을 나누는 기준 열이 파티션 키이고, 나뉜 조각 하나하나가 파티션입니다. 행은 키 값에 따라 어느 파티션에 들어갈지 정해집니다.
핵심 개념
Range 파티셔닝
키 값의 범위로 나눕니다. 범위끼리는 겹치지 않고, 날짜나 시간처럼 이어지는 값에 잘 맞습니다.
핵심 개념
List 파티셔닝
키 값이 어느 값 목록에 속하는지로 나눕니다. 국가나 상태 코드처럼 값이 몇 가지로 정해진 열에 맞습니다.
핵심 개념
Hash 파티셔닝
키 값의 해시를 modulus로 나눈 나머지로 나눕니다(보통 파티션 수와 같게 둡니다). 행이 비교적 고르게 흩어지지만 키의 범위로 파티션을 골라내기는 어렵습니다.
핵심 개념
파티션 프루닝
WHERE 조건을 보고 읽을 필요가 없는 파티션을 계획 수립 또는 실행 중에 제외하는 최적화입니다. 파티션 키 열을 단순하게 비교하는 조건이 있을 때 가장 잘 동작합니다.
핵심 개념
파티셔닝과 샤딩
파티셔닝은 보통 한 데이터베이스 안에서 테이블을 나누고, 샤딩은 데이터를 여러 서버에 나눠 저장합니다.
파티셔닝의 배경
테이블이 서버 메모리보다 클 정도로 매우 커지면 인덱스가 커지고 정리 작업이 오래 걸리며, 보관 기간이 지난 데이터를 지우는 일 자체가 부담이 됩니다. 한 테이블을 나눠서 다루면 이런 작업을 조각 단위로 할 수 있습니다.
PostgreSQL은 10 버전부터 PARTITION BY로 Range와 List를 선언하는 선언적 파티셔닝을 제공하고, 11 버전에서 Hash 파티셔닝이 추가되었습니다. MySQL도 RANGE, LIST, HASH, KEY 같은 파티셔닝을 지원합니다. 구체적인 문법과 제약은 데이터베이스마다 다르고, MySQL도 모든 유니크 키가 파티셔닝에 쓰는 열을 포함해야 합니다.
숫자로 확인하기
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
Range 파티셔닝의 대표적인 예는?
날짜 범위로 나누는 경우입니다. 월별이나 연도별 파티션으로 나누면 오래된 기간을 파티션 단위로 보관하거나 삭제하기 쉽습니다.
면접 질문
파티션 프루닝이란?
WHERE 조건으로 읽을 필요가 없는 파티션을 계획 수립 또는 실행 중에 제외하는 최적화입니다. 파티션 키 조건이 있어야 동작합니다.
면접 질문
파티셔닝과 샤딩은 어떻게 다른가요?
파티셔닝은 보통 한 데이터베이스 인스턴스 안에서 테이블을 나누고, 샤딩은 데이터를 여러 서버에 나눠 저장합니다. 샤딩은 서버를 늘려 용량과 처리량을 키울 수 있지만 라우팅과 샤드 간 조인 같은 부담이 따릅니다.
면접 질문
파티션 키는 어떻게 고르나요?
자주 쓰는 WHERE 조건과 데이터 보관 정책에 맞는 열을 고릅니다. 시간 기반 데이터는 날짜 열을 Range 키로 쓰는 경우가 많습니다.
활용 분야
함께 보면 좋은 토픽
참고 자료
SQL — PostgreSQL 선언적 파티셔닝
CREATE TABLE orders ( id bigint, ordered_at date NOT NULL, region text, amount int ) PARTITION BY RANGE (ordered_at); -- 파티션 키 = ordered_at CREATE TABLE orders_2024_01 PARTITION OF orders FOR VALUES FROM ('2024-01-01') TO ('2024-02-01'); -- 상한은 포함하지 않음 CREATE TABLE orders_2024_02 PARTITION OF orders FOR VALUES FROM ('2024-02-01') TO ('2024-03-01'); EXPLAIN SELECT * FROM orders WHERE ordered_at = '2024-02-15'; -- 계획에 orders_2024_02만 나온다 (프루닝) ALTER TABLE orders DETACH PARTITION orders_2024_01; -- 오래된 파티션을 떼어 내고 DROP TABLE orders_2024_01; -- 통째로 삭제 (대량 DELETE와 다름) -- List와 Hash도 같은 형태 -- PARTITION BY LIST (region) ... FOR VALUES IN ('KR') -- PARTITION BY HASH (id) ... FOR VALUES WITH (MODULUS 4, REMAINDER 0)
PostgreSQL의 선언적 파티셔닝은 PARTITION BY로 방식(RANGE, LIST, HASH)과 키를 정하고, 파티션마다 PARTITION OF와 FOR VALUES로 담당 범위를 지정합니다. 파티션을 나눈 부모 테이블 orders 자체에는 행이 저장되지 않고, 행은 키 값에 맞는 파티션에 들어갑니다. Range의 FROM은 포함하고 TO는 포함하지 않으므로 1월 파티션과 2월 파티션의 범위가 겹치지 않습니다. WHERE에 파티션 키 조건이 있으면 EXPLAIN 결과에 해당 파티션만 나오고, 시각화의 파티션 프루닝 탭이 보여 주는 동작과 같습니다. 오래된 기간의 데이터는 DETACH PARTITION으로 떼어 내 DROP TABLE로 지우면 대량 DELETE처럼 행마다 삭제하고 정리하는 부담이 없습니다. 다만 DROP이나 DETACH PARTITION은 부모 테이블에 강한 잠금이 필요하고(DETACH PARTITION ... CONCURRENTLY는 더 약한 잠금), 어느 파티션에도 맞지 않는 값의 INSERT는 오류이므로 필요하면 DEFAULT 파티션을 둡니다. 시각화의 month는 날짜의 월에 해당합니다.
Python — 행 배정과 프루닝 흉내
from collections import defaultdict rg = ('KR', 'KR', 'US', 'JP', 'KR', 'US') rows = [{'id': i + 1, 'month': 1 if i < 6 else 2 if i < 11 else 3 if i < 18 else 4, 'region': rg[i % 6]} for i in range(24)] def route(row, scheme): if scheme == 'range': return f"p_2024_{row['month']:02d}" # 값 범위 if scheme == 'list': return f"p_{row['region']}" # 값 목록 return f"p_{row['id'] % 4}" # 해시 (id % 4로 단순화) def partitions(scheme): parts = defaultdict(list) for r in rows: parts[route(r, scheme)].append(r) return parts def scan(scheme, key, value): # 키 조건이면 프루닝, 아니면 전부 읽음 parts = partitions(scheme) names = [f'p_2024_{value:02d}'] if (scheme, key) == ('range', 'month') else list(parts) return names, sum(len(parts[n]) for n in names) for s in ('range', 'list', 'hash'): print(s, {k: len(v) for k, v in sorted(partitions(s).items())}) print(scan('range', 'month', 3)) print(scan('range', 'region', 'KR')) # range {'p_2024_01': 6, 'p_2024_02': 5, 'p_2024_03': 7, 'p_2024_04': 6} # list {'p_JP': 4, 'p_KR': 12, 'p_US': 8} # hash {'p_0': 6, 'p_1': 6, 'p_2': 6, 'p_3': 6} # (['p_2024_03'], 7) # (['p_2024_01', 'p_2024_02', 'p_2024_03', 'p_2024_04'], 24)
같은 24행을 세 방식으로 배정하는 장난감 모델입니다. route가 행마다 담당 파티션 이름을 정하고, 출력은 Range가 6, 5, 7, 6행, List가 JP 4행, KR 12행, US 8행, Hash가 6행씩입니다. 시각화의 분할 방식 탭과 같은 결과이고, 해시는 설명을 위해 id % 4로 단순화했습니다. 실제 데이터베이스의 해시 함수와 다릅니다. scan은 프루닝을 흉내 냅니다. 파티션 키인 month 조건으로 찾으면 3월 파티션 하나에서 7행만 보지만, 키가 아닌 region으로 찾으면 파티션 4개 24행을 모두 훑습니다. 시각화의 파티션 프루닝 탭과 같은 결과입니다.
Java — TreeMap으로 Range 경계 찾기
TreeMap<Integer, String> bounds = new TreeMap<>(); // 키 = 파티션이 시작되는 month bounds.put(1, "p_2024_01"); bounds.put(2, "p_2024_02"); bounds.put(3, "p_2024_03"); bounds.put(4, "p_2024_04"); int[] months = new int[24]; for (int i = 0; i < 24; i++) months[i] = i < 6 ? 1 : i < 11 ? 2 : i < 18 ? 3 : 4; Map<String, Integer> count = new TreeMap<>(); for (int m : months) count.merge(bounds.floorEntry(m).getValue(), 1, Integer::sum); System.out.println(count); // {p_2024_01=6, p_2024_02=5, p_2024_03=7, p_2024_04=6} int[] hash = new int[4]; for (int id = 1; id <= 24; id++) hash[id % 4]++; System.out.println(Arrays.toString(hash)); // [6, 6, 6, 6]
자바에서는 TreeMap의 floorEntry가 값 이하에서 가장 큰 경계를 찾아 주므로 Range 파티션의 경계 검색을 간단히 표현할 수 있습니다. month 1부터 4까지를 경계로 두고 24행을 배정하면 6, 5, 7, 6행이 나옵니다. 경계가 많아져도 TreeMap은 트리 구조라 O(log n)에 찾으므로 비용이 크게 늘지 않습니다. 아래쪽은 Hash 배정을 id % 4로 단순화한 것으로 4개 파티션에 6행씩 들어갑니다. 실제 파티셔닝은 데이터베이스가 직접 처리하며 응용 프로그램이 이렇게 경계를 찾는 일은 거의 없습니다. 이 코드는 개념 설명용입니다. 상한을 검사하지 않아 month 5 이상도 마지막 파티션으로 배정되고 1보다 작으면 null이라 오류가 나는데, 실제 PostgreSQL은 이런 값을 오류 또는 DEFAULT 파티션으로 처리합니다.
Q1
Range 파티셔닝의 대표적인 예는?
Q2
파티션 프루닝(Partition Pruning)이란?
Q3
파티셔닝과 샤딩의 차이로 옳은 것은?