컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
db · 13

테이블 파티셔닝

설계성능파티셔닝

테이블 파티셔닝은 논리적으로 하나인 큰 테이블을 파티션 키 기준으로 더 작은 물리 조각(파티션)으로 나누어 저장하는 방법입니다. 응용 프로그램은 여전히 테이블 하나를 조회하지만, 데이터베이스는 행을 키 값에 맞는 파티션에 넣고, WHERE에 파티션 키 조건이 있으면 조회할 때 필요한 파티션만 읽습니다.

PostgreSQL 문서는 파티셔닝의 이점으로 자주 쓰는 행이 한두 파티션에 모여 있을 때의 조회 성능, 파티션을 붙이고 떼는 방식으로 하는 대량 적재와 삭제, 잘 쓰지 않는 데이터의 저렴한 저장소 이동을 듭니다. 이 토픽에서는 Range, List, Hash 세 분할 방식, 파티션 프루닝, 파티셔닝과 샤딩의 차이를 시각화로 확인합니다.

핵심 개념

파티션과 파티션 키

테이블을 나누는 기준 열이 파티션 키이고, 나뉜 조각 하나하나가 파티션입니다. 행은 키 값에 따라 어느 파티션에 들어갈지 정해집니다.

핵심 개념

Range 파티셔닝

키 값의 범위로 나눕니다. 범위끼리는 겹치지 않고, 날짜나 시간처럼 이어지는 값에 잘 맞습니다.

핵심 개념

List 파티셔닝

키 값이 어느 값 목록에 속하는지로 나눕니다. 국가나 상태 코드처럼 값이 몇 가지로 정해진 열에 맞습니다.

핵심 개념

Hash 파티셔닝

키 값의 해시를 modulus로 나눈 나머지로 나눕니다(보통 파티션 수와 같게 둡니다). 행이 비교적 고르게 흩어지지만 키의 범위로 파티션을 골라내기는 어렵습니다.

핵심 개념

파티션 프루닝

WHERE 조건을 보고 읽을 필요가 없는 파티션을 계획 수립 또는 실행 중에 제외하는 최적화입니다. 파티션 키 열을 단순하게 비교하는 조건이 있을 때 가장 잘 동작합니다.

핵심 개념

파티셔닝과 샤딩

파티셔닝은 보통 한 데이터베이스 안에서 테이블을 나누고, 샤딩은 데이터를 여러 서버에 나눠 저장합니다.

조회가 필요한 파티션만 읽습니다 — WHERE에 파티션 키 조건이 있으면 나머지 파티션은 계획에서 빠져 읽을 데이터가 줄어듭니다.
오래된 데이터를 통째로 지웁니다 — 기간별 Range 파티션이면 보관 기간이 지난 파티션을 떼어 내 삭제할 수 있어, 행마다 지우는 대량 DELETE의 부담을 피합니다.
키 선택이 성능을 좌우합니다 — 자주 쓰는 WHERE 조건과 맞지 않는 열로 나누면 모든 파티션을 훑게 되어 프루닝의 이점을 얻지 못합니다.
제약 조건에 제한이 있습니다 — PostgreSQL에서 파티션된 테이블의 기본 키와 유니크 제약에는 파티션 키 열이 모두 들어 있어야 합니다. 각 파티션의 인덱스는 자기 파티션 안에서만 유일성을 강제하기 때문입니다.
이 시각화에서 확인할 것 — "분할 방식" 탭에서 같은 24행이 Range, List, Hash로 달리 나뉘는 모습을, "파티션 프루닝" 탭에서 month 조건이면 파티션 하나만 읽고 region 조건이면 4개를 모두 읽는 모습을, "파티셔닝과 샤딩" 탭에서 한 서버 안의 파티션과 여러 서버의 샤드가 어떻게 다른지 STEP으로 확인하세요.

파티셔닝의 배경

테이블이 서버 메모리보다 클 정도로 매우 커지면 인덱스가 커지고 정리 작업이 오래 걸리며, 보관 기간이 지난 데이터를 지우는 일 자체가 부담이 됩니다. 한 테이블을 나눠서 다루면 이런 작업을 조각 단위로 할 수 있습니다.

PostgreSQL은 10 버전부터 PARTITION BY로 Range와 List를 선언하는 선언적 파티셔닝을 제공하고, 11 버전에서 Hash 파티셔닝이 추가되었습니다. MySQL도 RANGE, LIST, HASH, KEY 같은 파티셔닝을 지원합니다. 구체적인 문법과 제약은 데이터베이스마다 다르고, MySQL도 모든 유니크 키가 파티셔닝에 쓰는 열을 포함해야 합니다.

숫자로 확인하기

Range 4개 — 주문 24행을 month로 나누면 1월부터 4월까지 6, 5, 7, 6행입니다. 파티션 크기가 달라도 합은 항상 24행입니다.
List와 Hash — region으로 나누면 KR 12행, US 8행, JP 4행으로 고르지 않고, id % 4로 나누면 6행씩 고릅니다.
프루닝 효과 — month = 3으로 찾으면 파티션 1개 7행만 검사하고, month가 2에서 3 사이면 파티션 2개 12행을 검사합니다. 파티션 키가 아닌 region으로 찾으면 파티션 4개 24행을 모두 검사합니다.
코드 확인 — 파이썬과 자바 모델 모두 Range 배정이 6, 5, 7, 6행이고 해시 배정이 6행씩이며, 파이썬 scan은 키 조건이면 7행, 키가 아니면 24행을 반환합니다.

자주 하는 오해

파티션을 나누면 모든 조회가 빨라진다 — WHERE에 파티션 키 조건이 있어 프루닝이 될 때 이점이 큽니다. 키 조건이 없으면 모든 파티션을 읽고, 파티션 수가 많으면 계획 수립 비용이 늘 수 있습니다.
파티션은 많을수록 좋다 — 파티션이 지나치게 많으면 계획 수립 시간과 메모리 사용이 늘어납니다. 필요한 만큼만 나누며, PostgreSQL 문서는 테이블이 아주 클 때에만 보통 이득이 있다고 설명합니다.
파티셔닝과 샤딩은 같은 말이다 — 파티셔닝은 보통 한 데이터베이스 안에서 테이블을 나누는 것이고, 샤딩은 데이터를 여러 서버에 나눠 저장하는 것입니다.
인덱스를 대신해 준다 — 파티셔닝은 읽을 범위를 줄일 뿐이어서, 파티션 안에서 일부 행만 읽는 쿼리에는 인덱스가 여전히 도움이 됩니다. 파티셔닝이 인덱스를 완전히 대체하지는 않습니다.
키 열에 함수를 씌워도 프루닝이 된다 — 프루닝은 키 열을 상수나 파라미터와 단순 비교할 때 가장 확실합니다. 키 열에 함수를 씌우거나 복잡한 식과 비교하면 파티션을 거르지 못할 수 있습니다.
어떤 열이든 키로 쓸 수 있다 — 키는 자주 쓰는 WHERE 조건과 보관 정책에 맞춰 골라야 하고, 파티션된 테이블의 기본 키와 유니크 제약은 키 열을 포함해야 하는 제약도 있습니다.

면접에서 이렇게 물어요

면접 질문

Range 파티셔닝의 대표적인 예는?

날짜 범위로 나누는 경우입니다. 월별이나 연도별 파티션으로 나누면 오래된 기간을 파티션 단위로 보관하거나 삭제하기 쉽습니다.

면접 질문

파티션 프루닝이란?

WHERE 조건으로 읽을 필요가 없는 파티션을 계획 수립 또는 실행 중에 제외하는 최적화입니다. 파티션 키 조건이 있어야 동작합니다.

면접 질문

파티셔닝과 샤딩은 어떻게 다른가요?

파티셔닝은 보통 한 데이터베이스 인스턴스 안에서 테이블을 나누고, 샤딩은 데이터를 여러 서버에 나눠 저장합니다. 샤딩은 서버를 늘려 용량과 처리량을 키울 수 있지만 라우팅과 샤드 간 조인 같은 부담이 따릅니다.

면접 질문

파티션 키는 어떻게 고르나요?

자주 쓰는 WHERE 조건과 데이터 보관 정책에 맞는 열을 고릅니다. 시간 기반 데이터는 날짜 열을 Range 키로 쓰는 경우가 많습니다.

활용 분야

로그와 이벤트 테이블 — 시간 순으로 쌓이는 데이터를 월별이나 일별 Range 파티션으로 나누고, 보관 기간이 지난 파티션을 삭제합니다.
주문과 거래 이력 — 최근 기간 조회가 대부분이면 기간 키로 나눠 최근 파티션만 읽게 합니다.
지역이나 상태별 분리 — 국가 코드처럼 값이 정해진 열은 List 파티셔닝으로 나눠 해당 지역 조회와 관리를 분리합니다.

함께 보면 좋은 토픽

샤딩 — 데이터를 여러 서버에 나누는 방식과 파티셔닝의 차이를 봅니다.
인덱스 — 파티션 안에서 행을 빨리 찾는 방법을 봅니다.
쿼리 실행 계획 — EXPLAIN으로 프루닝된 파티션을 확인하는 방법을 봅니다.
복제 — 데이터를 다른 서버에 복사하는 또 다른 확장 방식입니다.

참고 자료

공식 문서 — PostgreSQL: Table Partitioning
공식 문서 — PostgreSQL: CREATE TABLE
공식 문서 — MySQL: Partitioning

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 파티셔닝의 대표적인 예는?

값의 "범위"로 나눕니다.
Range 파티셔닝은 컬럼 값의 범위에 따라 파티션을 나눕니다. 로그나 주문 테이블을 월별/연도별 파티션으로 나누는 것이 흔한 예입니다.

Q2

파티션 프루닝(Partition Pruning)이란?

2024년 데이터만 찾는데 2020년 파티션까지 읽을 필요는 없습니다.
옵티마이저가 WHERE 조건을 분석해 관련 없는 파티션은 아예 읽지 않고 제외하는 최적화입니다. 대용량 테이블의 조회 성능을 크게 높일 수 있습니다.

Q3

파티셔닝과 샤딩의 차이로 옳은 것은?

"어디에" 나누는지가 다릅니다.
파티셔닝은 보통 하나의 DB 인스턴스 안에서 큰 테이블을 논리적으로 분할하는 것이고, 샤딩은 데이터를 여러 DB 서버에 분산 저장해 수평 확장하는 방식입니다.