파티셔닝과 샤딩의 차이

파티셔닝과 샤딩의 차이

한눈에 보기

파티셔닝은 논리적으로 하나인 테이블을 보통 같은 DB 시스템 안의 여러 물리 조각으로 나누고 DB가 행 배치와 쿼리 프루닝을 담당한다. 샤딩은 데이터를 여러 독립 DB 노드에 분산하며 애플리케이션 또는 미들웨어가 shard routing, cross-shard 조회, 재분배와 장애를 다뤄야 한다. 둘 다 큰 데이터를 나누지만 해결하는 병목과 운영 복잡도가 다르다.

목차

문제가 되는 상황

이벤트 로그 테이블이 30억 행을 넘고 매달 수억 건씩 증가한다고 하자. 최근 7일 조회는 빨라야 하고 2년이 지난 데이터는 삭제해야 한다. 한편 쓰기 처리량도 단일 DB의 한계에 가까워지고 있다.

이때 “테이블이 크니 파티셔닝이나 샤딩을 하자”는 말이 나온다. 둘 다 데이터를 나누므로 비슷해 보이지만 실제로는 서로 다른 문제를 해결한다.

시간 기준 파티셔닝이 잘 푸는 문제
→ 최근 기간만 읽기
→ 오래된 기간 단위 제거
→ 큰 테이블의 관리 단위 분리

샤딩이 잘 푸는 문제
→ 단일 노드의 CPU·메모리·스토리지·쓰기 한계 분산
→ tenant 또는 user별 독립적인 데이터 배치

파티셔닝으로 단일 서버의 총 쓰기 용량이 자동으로 여러 서버에 분산되는 것은 아니다. 반대로 샤딩을 도입한다고 오래된 데이터의 보관과 삭제가 자연스럽게 해결되는 것도 아니다.

파티셔닝은 하나의 논리 테이블을 내부에서 나눈다

파티션 테이블은 애플리케이션에는 하나의 테이블처럼 보이지만 저장은 여러 조각으로 나뉜다.

events (logical table)
├── events_2026_07
├── events_2026_08
├── events_2026_09
└── events_default

PostgreSQL의 개념적 예시는 다음과 같다.

CREATE TABLE events (
  id BIGINT NOT NULL,
  tenant_id BIGINT NOT NULL,
  occurred_at TIMESTAMPTZ NOT NULL,
  payload JSONB NOT NULL
) PARTITION BY RANGE (occurred_at);
CREATE TABLE events_2026_09
PARTITION OF events
FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');

INSERT가 들어오면 DB는 partition key인 occurred_at을 보고 대상 파티션으로 라우팅한다. 애플리케이션은 일반적으로 논리 테이블 events에 질의한다.

INSERT INTO events(id, tenant_id, occurred_at, payload)
VALUES (:id, :tenant_id, :occurred_at, :payload);

제품에 따라 range, list, hash/key 같은 방식을 제공한다.

방식 분할 예 적합한 성격
Range 날짜·번호 구간 기간 조회와 보관 정책
List 지역·상태 목록 명확한 값 그룹
Hash/Key key hash 균등한 데이터 분산

파티션마다 인덱스와 제약이 물리적으로 어떻게 생성되는지, 전역 인덱스를 지원하는지 등은 DB마다 다르다. “논리 테이블 하나”라는 인터페이스만 보고 운영 세부까지 같다고 가정해서는 안 된다.

파티션 프루닝이 성능을 만드는 조건

파티셔닝의 대표적인 읽기 이점은 조건과 무관한 파티션을 계획 또는 실행 단계에서 제외하는 partition pruning이다.

SELECT tenant_id, COUNT(*)
FROM events
WHERE occurred_at >= '2026-09-01'
  AND occurred_at <  '2026-09-08'
GROUP BY tenant_id;

occurred_at으로 월별 range partition을 했다면 7월과 8월 파티션은 볼 필요가 없다.

flowchart LR
    Q[9월 1~7일 조회] --> P9[events_2026_09 읽기]
    Q -. pruning .-> P7[events_2026_07 제외]
    Q -. pruning .-> P8[events_2026_08 제외]

MySQL 공식 문서는 프루닝을 “일치할 수 없는 파티션은 스캔하지 않는다”는 개념으로 설명한다. PostgreSQL도 partition bounds를 이용해 불필요한 파티션을 제거한다.

하지만 WHERE 조건이 partition key와 연결되지 않으면 모든 파티션을 확인할 수 있다.

SELECT *
FROM events
WHERE tenant_id = 8421;

날짜 조건이 없으므로 월별 파티션을 모두 탐색해야 할 수 있다. 각 파티션의 tenant_id 인덱스를 반복해서 찾는 계획이 된다.

Partition 2025-01 index lookup
Partition 2025-02 index lookup
...
Partition 2026-09 index lookup
파티션 수가 많다고 검색이 자동으로 빨라지지 않는다

주요 쿼리가 partition key를 조건으로 포함해 pruning할 수 있어야 한다. 파티션 안에서도 필요한 인덱스는 여전히 필요하다.

함수를 적용하거나 타입을 변환한 조건이 프루닝에 어떻게 해석되는지도 DB·버전별로 확인해야 한다.

WHERE DATE(occurred_at) = '2026-09-01'

원본 범위 조건으로 표현하면 partition bounds와 인덱스를 더 명확하게 활용할 수 있다.

WHERE occurred_at >= '2026-09-01 00:00:00'
  AND occurred_at <  '2026-09-02 00:00:00'

파티셔닝은 오래된 데이터 관리에도 유용하다

대형 테이블에서 1억 건을 DELETE하면 긴 트랜잭션, 많은 undo·redo, 복제 지연과 vacuum/purge 부담이 생길 수 있다.

DELETE FROM events
WHERE occurred_at < '2024-01-01';

기간별 파티션이라면 보관 기간이 지난 파티션을 detach 또는 drop하는 운영 방식을 검토할 수 있다.

events_2023_12 partition
→ 읽기 트래픽에서 detach
→ 필요 시 archive
→ drop

행을 하나씩 삭제하는 대신 저장 단위 전체를 제거하므로 훨씬 효율적일 수 있다. 다만 DDL lock, 외래 키, 복제, 백업과 복구 정책은 DB별로 확인한다.

새 월 파티션을 미리 만들어야 하는 운영 자동화도 필요하다.

async function ensureFuturePartitions(now: Date) {
  const months = nextMonths(now, 3);

  for (const month of months) {
    await partitionManager.createIfMissing(month);
  }
}

파티션이 없을 때 INSERT가 실패하는지 default partition으로 들어가는지, default 데이터를 나중에 옮길지 정책을 정한다. 파티션 수가 지나치게 많으면 계획과 메타데이터 비용이 커질 수 있어 시간 단위를 너무 작게 잡지 않는다.

샤딩은 여러 데이터베이스로 부하와 용량을 나눈다

샤딩은 데이터 집합을 여러 독립 DB 노드로 나눈다.

flowchart TD
    A[Application] --> R[Shard Router]
    R -->|tenant hash 0~31| S1[(Shard A)]
    R -->|tenant hash 32~63| S2[(Shard B)]
    R -->|tenant hash 64~95| S3[(Shard C)]
    R -->|tenant hash 96~127| S4[(Shard D)]

각 샤드는 자신의 CPU, 메모리, 스토리지와 커넥션 한도를 가진다. 요청이 균등하게 분산되면 단일 노드 한계를 넘어 수평 확장할 수 있다.

하지만 논리 테이블을 한 DB가 완전히 관리하던 책임이 분산된다.

Vitess 같은 미들웨어는 keyspace와 sharding key를 이용한 routing과 online resharding 기능을 제공하지만, cross-shard 쿼리와 데이터 모델의 비용 자체가 사라지는 것은 아니다.

Shard Key가 라우팅과 균형을 결정한다

멀티테넌트 서비스에서 tenant_id를 shard key로 사용할 수 있다.

function shardForTenant(tenantId: bigint): ShardId {
  const bucket = consistentHash(tenantId);
  return shardMap.lookup(bucket);
}

좋은 shard key는 다음 성질을 가능한 한 만족한다.

tenant_id는 locality가 좋지만 대형 tenant 하나가 shard 전체 부하를 독점할 수 있다.

tenant A: 10,000 rows
tenant B: 20,000 rows
tenant C: 4,000,000,000 rows  ← whale tenant

단순 tenant_id % 4는 tenant C를 여러 shard로 나누지 못한다. hash 기반 분산은 평균적인 균형에 유리하지만 범위 조회와 지역 배치를 어렵게 할 수 있다. 날짜를 shard key로 쓰면 최신 shard에 모든 쓰기가 몰리고 기간을 넘는 사용자 조회가 fan-out될 수 있다.

shard key는 인덱스 열 하나를 고르는 수준의 결정이 아니다

API가 key를 항상 전달하는지, 데이터 locality와 트랜잭션 경계, 대형 고객의 성장까지 데이터 모델 전체에 영향을 준다.

Cross-shard 쿼리가 복잡해지는 이유

shard key가 없는 전역 최신 주문 100건을 조회하면 모든 shard에 질의해야 한다.

Shard A → 최신 100건
Shard B → 최신 100건
Shard C → 최신 100건
Shard D → 최신 100건
Router  → 400건 merge-sort → 최종 100건
const shardResults = await Promise.all(
  allShards.map((shard) => shard.findLatestOrders(100)),
);

return mergeByCreatedAt(shardResults).slice(0, 100);

한 shard가 느리거나 실패하면 전체 응답이 지연된다. pagination 커서에는 shard별 진행 위치가 필요할 수 있고 shard가 늘면 fan-out 비용도 늘어난다.

전역 집계도 어렵다.

SELECT status, COUNT(*)
FROM orders
GROUP BY status;

각 shard에서 부분 집계를 수행한 뒤 합쳐야 한다. 실시간 정확성이 필요 없다면 분석계나 별도 read model로 이벤트를 모으는 편이 낫다.

같은 shard key를 가진 테이블을 co-location하면 JOIN을 로컬로 유지할 수 있다.

orders(tenant_id, ...)
order_items(tenant_id, order_id, ...)

두 테이블을 tenant_id 기준 같은 shard에 배치

반대로 사용자와 주문이 서로 다른 key로 분산되면 일반적인 FK와 JOIN 보장을 유지하기 어려워진다.

트랜잭션과 유일성의 범위가 달라진다

한 shard 안의 트랜잭션은 기존 단일 DB와 비슷하게 처리할 수 있다. 두 shard를 동시에 변경하면 분산 트랜잭션 문제가 생긴다.

Shard A: 사용자 포인트 차감
Shard B: 판매자 포인트 증가

한쪽만 커밋되면 금액이 사라지거나 늘어난다. 2-phase commit을 지원하는 플랫폼도 있지만 가용성, 지연과 운영 복잡도가 커질 수 있다. saga와 보상 트랜잭션을 사용하면 중간 상태와 재시도·멱등성을 설계해야 한다.

전역 유일성도 자동으로 보장되지 않는다.

UNIQUE (email)

각 shard가 email을 따로 검사하면 같은 이메일이 서로 다른 shard에 존재할 수 있다. 선택지는 다음과 같다.

ID도 shard 간 충돌 없이 생성해야 한다. shard prefix, snowflake 계열 ID, UUID 같은 방식을 검토한다. 시간 정렬 ID의 성질은 UUID v4와 시간 정렬 UUID 비교와 연결된다.

재파티셔닝과 리샤딩의 운영 차이

파티션 경계를 바꾸는 일도 쉬운 것은 아니다. 잘못된 월 단위를 일 단위로 나누거나 partition key를 바꾸면 대량 데이터 이동과 DDL이 필요할 수 있다. 그래도 같은 DB 시스템이 메타데이터와 쿼리 인터페이스를 관리하는 경우가 많다.

리샤딩은 여러 노드 사이에서 온라인 데이터를 복사하면서 읽기·쓰기 routing을 전환해야 한다.

sequenceDiagram
    participant App
    participant Router
    participant Old as Old Shard
    participant New as New Shards
    Router->>Old: 기존 read/write
    Old->>New: snapshot copy
    Old->>New: 변경분 지속 복제
    Router->>New: shadow read 및 검증
    Router->>New: read 전환
    Router->>New: write 전환
    Note over Old: 검증 후 폐기

필요한 단계는 보통 다음과 같다.

  1. 새 shard와 schema 준비
  2. 기존 데이터 snapshot 복사
  3. 복사 중 변경분 동기화
  4. row count·checksum·샘플 검증
  5. 일부 read를 새 shard에 보내 비교
  6. read와 write routing 전환
  7. rollback 관찰 기간 후 기존 shard 제거

dual write를 애플리케이션에서 직접 구현하면 한쪽 실패와 순서 문제가 생긴다. CDC 또는 검증된 resharding 도구를 사용하는 이유다. 도구가 있어도 cutover 기준과 rollback 책임은 남는다.

파티셔닝과 샤딩을 함께 사용할 수도 있다

둘은 배타적인 선택이 아니다. tenant hash로 shard를 나누고 각 shard 안에서 이벤트를 월별 partition할 수 있다.

Shard A
├── events_2026_08
└── events_2026_09

Shard B
├── events_2026_08
└── events_2026_09

이 구조는 노드 간 용량·쓰기 분산과 기간 단위 보관을 함께 지원한다. 대신 운영 단위가 곱해진다.

8 shards × 24 monthly partitions = 192 physical pieces

모든 shard에 다음 달 partition을 생성하고, schema migration을 순차 적용하며, 오래된 partition을 동일한 정책으로 제거해야 한다. 충분한 자동화와 관측 없이 두 복잡성을 동시에 도입하면 장애 가능성이 커진다.

먼저 해결해야 할 더 단순한 문제

테이블이 크다는 이유만으로 바로 분할하지 않는다. 먼저 현재 병목이 무엇인지 측정한다.

쿼리와 인덱스

불필요한 전체 스캔, N+1, 큰 OFFSET, 잘못된 복합 인덱스를 고친다. 30억 행이어도 특정 키 단건 조회는 적절한 인덱스로 빠를 수 있다.

데이터 보관

필요 없는 원본 데이터를 무기한 보관하고 있지 않은지 확인한다. 집계 결과만 남기거나 object storage로 archive할 수 있다.

읽기 분리와 캐시

읽기 부하라면 replica, cache, read model로 단일 primary의 부담을 줄일 수 있다. 쓰기 한계에는 도움이 제한적이다.

수직 확장과 설정

버퍼 풀, 스토리지, 커넥션과 쿼리 동시성을 점검한다. 수직 확장은 영원한 해결책은 아니지만 샤딩의 운영 비용보다 훨씬 저렴할 수 있다.

비동기 처리

동기 요청에서 필요 없는 집계와 후속 작업을 큐로 옮긴다.

분할은 병목을 이동시킨다

한 테이블의 I/O 병목은 줄지만 router, cross-shard merge, migration과 관측이 새 병목이 된다. 현재 문제가 이 비용을 정당화할 정도인지 수치로 답한다.

선택 기준을 표로 정리하기

질문 파티셔닝 샤딩
주된 목적 큰 테이블의 조회·관리 단위 분리 단일 노드의 용량·처리량 한계 분산
논리 DB 보통 하나 여러 독립 노드
라우팅 책임 DB 엔진 미들웨어 또는 애플리케이션
핵심 키 partition key shard key
성능 조건 WHERE로 partition pruning 대부분 한 shard로 라우팅
JOIN·트랜잭션 일반 DB 범위 유지 cross-shard에서 복잡
유일성 DB 제약 범위 전역 유일성 별도 설계 가능
데이터 이동 partition attach/detach/reorganize online copy·동기화·routing 전환
장애 범위 한 DB 장애가 전체에 영향 가능 shard 장애가 일부 데이터에 영향, 운영은 복잡
대표 사용 기간 로그·보관 초대형 multi-tenant·쓰기 확장

결정 전에 다음 지표를 기록한다.

결론

파티셔닝과 샤딩은 모두 데이터를 나누지만 경계와 책임이 다르다. 파티셔닝은 하나의 논리 테이블을 DB 내부 조각으로 나눠 partition key 조건으로 불필요한 조각을 pruning하고 기간 단위 데이터를 관리하는 데 강하다. 샤딩은 여러 DB 노드로 용량과 처리량을 분산하지만 routing, cross-shard 조회, 분산 트랜잭션과 resharding 책임을 추가한다.

파티션 키가 주요 WHERE 조건에 없으면 모든 파티션을 읽을 수 있고, shard key가 API와 데이터 locality에 맞지 않으면 fan-out과 hot shard가 생긴다. 따라서 “데이터가 크다”가 아니라 현재 병목이 기간 관리인지 단일 노드 한계인지 먼저 구분한다. 인덱스·보존·캐시·비동기화 같은 단순한 수단을 검토한 뒤에도 한계가 명확할 때, 실제 쿼리와 성장 분포를 기준으로 분할 키와 이전 계획을 설계해야 한다.

관련 노트

참고 자료