목차
주문 테이블이 10억 건이다. 인덱스가 메모리에 안 들어간 지 오래고, 2년 지난 데이터를 지우려고 DELETE를 돌렸더니 세 시간째 안 끝난다.
한 덩어리로는 감당이 안 되니 나눠야 한다. 그런데 나누는 방법이 둘이고, 둘은 이름만 비슷하지 완전히 다른 결정이다.
경계는 “누가 조각을 아는가”
SELECT * FROM orders
같은 조각 셋이다. 다른 것은 하나뿐 - 조각을 둘러싼 벽이 있느냐. 벽이 없으면 그 앎이 애플리케이션 몫이 된다.
파티셔닝은 DB가 감춰준다. 테이블은 여전히 하나로 보이고, SELECT도 그대로다. 안에서 조각으로 나눠 저장할 뿐이다.
샤딩은 못 감춘다. DB가 여러 대라 서로를 모른다. 어디에 있는지는 애플리케이션(또는 앞단의 프록시)이 알아야 한다.
이 차이가 전부다. 하나는 저장 방식의 변경이고, 다른 하나는 아키텍처의 변경이다.
파티셔닝은 조회를 빠르게 하려는 게 아니다
가장 흔한 오해다. 나누면 각 조각이 작아지니 빨라질 것 같다.
조건이 맞을 때만 그렇다.
날짜로 나눈 테이블에 WHERE 주문일 = '2026-07-15'로 물으면, DB는 7월 조각 하나만 열면 된다. 나머지는 열어볼 필요조차 없다. 이걸 파티션 프루닝이라고 한다.
그런데 WHERE 회원ID = 'U7'로 물으면? 회원ID는 나눈 축이 아니다. U7의 주문이 어느 조각에 있는지 알 수 없으니 전부 열어야 한다. 안 나눴을 때보다 오히려 느리다. 조각마다 따로 뒤져야 하니까.
나눈 축이 조회 조건에 없으면 파티셔닝은 손해다. 그리고 이건 실행 계획에 그대로 나온다. 몇 개 조각을 읽었는지가 찍힌다. 나누기 전에 어떤 조건으로 조회하는지부터 세어봐야 한다. 축은 조회가 정하는 것이지 데이터가 정하는 게 아니다.
그럼 왜 나누나
관리 때문이다. 이게 파티셔닝의 진짜 효용이다.
맨 앞의 그 DELETE를 생각해보자. 2년 지난 주문 3억 건을 지운다. DB는 3억 줄을 하나씩 찾아 지우고, 인덱스에서도 빼고, MVCC라면 옛 버전까지 쌓아둔다. 세 시간이 걸리고 그동안 테이블이 부푼다.
날짜로 파티션했다면? 그 조각을 통째로 버린다. 파일을 지우는 것과 같아서 1초면 끝난다. 지울 행을 찾을 필요도, 인덱스를 고칠 필요도 없다.
주기적으로 오래된 데이터를 버리는 시스템이라면 이것만으로 파티셔닝의 값어치가 있다.
나누는 축
| 어떻게 | 언제 | |
|---|---|---|
| 범위 | 날짜·숫자 구간으로 | 가장 흔하다. 로그·주문처럼 시간이 축인 데이터 |
| 목록 | 값을 나열해서 (지역, 상태) | 값의 종류가 적고 잘 안 변할 때 |
| 해시 | 키를 해싱해 고르게 | 고르게 흩는 게 목적일 때. 범위 조회는 못 한다 |
파티션 테이블에는 함정이 하나 있다. 전역 유니크 제약을 걸기 어렵다. 대부분의 DB에서 유니크 제약에 파티션 키가 포함돼야 한다. 각 조각이 자기 인덱스만 갖고 있어서, 다른 조각에 같은 값이 있는지 알 수가 없기 때문이다. 키 설계에서 email에 UNIQUE를 걸라고 했는데, 파티션 테이블이면 그게 안 될 수 있다.
샤딩 - 여기서부터 다른 세계다
한 대로 감당이 안 될 때 여러 대로 흩는다. 용량이든 쓰기 처리량이든 한 대의 한계에 닿았을 때다.
그리고 여기서 지금까지의 이야기 대부분이 무효가 된다.
잃는 것 하나: ACID
트랜잭션 글에서 트랜잭션이 뭘 약속하는지 봤다. 격리 수준도, 락도, MVCC도 다뤘다.
그게 전부 한 DB 안에서의 약속이었다.
A가 1번 샤드에 있고 B가 2번 샤드에 있는데 A에서 B로 이체한다면? 두 DB는 서로를 모른다. 각자 자기 트랜잭션만 안다. 한쪽은 커밋되고 한쪽이 실패하면 원자성이 깨진다. 트랜잭션 글에 나왔던 그 “만 원이 증발한” 상황이 그대로 돌아온다. 트랜잭션으로 막았다고 생각했던 바로 그 문제다.
분산 트랜잭션(2단계 커밋)이라는 게 있긴 하다. 여러 DB에게 “준비됐냐”를 물어보고 다 됐다고 하면 커밋하는 방식이다. 그런데 느리고, 조율자가 죽으면 참가자들이 락을 쥔 채 멈춘다. 한 대 안에서 공짜로 받던 걸 아주 비싸게 사는 셈이다.
그래서 실무에서는 대개 분산 트랜잭션을 피하도록 설계한다. “같이 바뀌어야 하는 것들은 같은 샤드에 두자”는 원칙이 나온다. 그게 안 되면 트랜잭션을 포기하고 다른 방법(보상 트랜잭션 같은)으로 푼다.
잃는 것 둘: 조인
조인 알고리즘에서 본 셋은 한 DB 안에서 두 테이블을 붙이는 이야기였다. 옵티마이저가 통계를 보고 알아서 골라줬다.
샤드를 넘으면 그런 게 없다. 회원은 1번 샤드에, 주문은 2번 샤드에 있으면 DB가 붙여줄 방법이 없다. 애플리케이션이 양쪽에서 각각 읽어와 메모리에서 붙여야 한다. 옵티마이저도, 해시 조인도, 통계도 없다.
그래서 샤드 키를 고를 때 같이 조회되는 것들이 같은 샤드에 모이게 해야 한다. 회원과 그 회원의 주문을 user_id로 같이 샤딩하면 한 샤드 안에서 조인된다.
샤드 키 - 되돌리기 가장 어려운 결정
키 설계에서 기본키는 안 바뀌어야 한다고 했다. 샤드 키는 그보다 더하다. 바꾸려면 데이터를 전부 옮겨야 한다.
고를 때 세 가지를 본다.
고르게 흩어지나. 한쪽에 쏠리면(핫스팟) 나눈 의미가 없다. 시간을 샤드 키로 쓰면 최신 데이터가 전부 한 샤드로 몰린다. 나머지 샤드는 놀고 한 대만 죽어난다.
대부분의 조회에 그 키가 들어가나. 안 들어가면 모든 샤드에 물어봐야 한다. 파티션 프루닝이 실패한 것과 같은데, 이번엔 네트워크를 타고 나간다.
나중에 늘릴 수 있나. 샤드를 3대에서 4대로 늘릴 때 user_id % 3이었다면 거의 모든 데이터가 자리를 옮긴다. 이걸 줄이려고 일관 해싱 같은 걸 쓴다.
그래서 순서
샤딩은 마지막 수단이다. 위에서 본 것들을 다 포기하고 시작하는 것이다. 그 전에 인덱스·쿼리·통계를 다 손봤는지, 읽기가 문제면 복제로 풀리는지, 오래된 데이터를 파티셔닝으로 덜어낼 수 있는지부터 본다. 샤딩은 되돌리기가 가장 어렵다.
- 파티셔닝은 저장 방식만 바꾼다. 애플리케이션이 몰라도 된다. 되돌리기도 상대적으로 쉽다.
- 샤딩은 아키텍처를 바꾼다. ACID도, 조인도, 옵티마이저도 잃는다.
정리
- 둘의 경계는 누가 조각을 아는가다. 파티셔닝은 DB가 감춰주고, 샤딩은 못 감춘다.
- 파티셔닝은 조회를 빠르게 하려는 게 아니다. 나눈 축이 조회 조건에 없으면 오히려 느리다. 진짜 효용은 오래된 조각을 통째로 버리는 것이다.
- 샤딩하면 지금까지의 트랜잭션 이야기가 대부분 무효가 된다. ACID는 한 DB 안의 약속이었다.
- 샤드를 넘는 조인은 DB가 안 해준다. 같이 조회되는 건 같은 샤드에.
- 샤드 키는 되돌리기 가장 어려운 결정이다. 고르게 흩어지고, 조회에 들어가고, 늘릴 수 있어야 한다.
다음 글은 복제다. 쓰기가 아니라 읽기가 문제라면 나누기 전에 복제로 풀 수 있다. 대신 “방금 쓴 걸 바로 못 읽는” 새로운 문제가 생긴다.