📦 파티셔닝이란?
하나의 큰 테이블을 여러 개로 나눠서 저장하는 기술
하지만 중요한 건 : 논리적으로 1개의 테이블, 물리적으로는 여러 조각
즉, 개발자는 그냥 SELECT * FROM ORDERS 하면 되는데
DB 내부에서 알아서 나눠서 처리해줌
왜 쓰는가?
테이블이 너무 커지면 생기는 문제 :
🔹 조회 느려짐
🔹인덱스 비대해짐
🔹 I/O 증가
🔹 백업 느림
예를 들어 ORDERS 테이블에 3억 건 있음
➡ 매번 3억 건 중에서 찾으면 느림, 그래서 나누는 것
🍏 파티셔닝 종류 (MySQL 기준)
🔹 RANGE 파티셔닝 (제일 많이 사용)
값의 범위로 나눔
CREATE TABLE ORDERS (
ORDER_ID INT,
ORDER_DATE DATE
)
PARTITION BY RANGE (YEAR(ORDER_DATE)) (
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025)
);
2023 데이터는 P2023
2024 데이터는 P2024
주로 날짜 기준으로 많이 씀
🍏 LIST 파티셔닝
PARTITION BY LIST (ACCOUNT_ROLE) (
PARTITION p_admin VALUES IN ('ADMIN'),
PARTITION p_user VALUES IN ('USER')
);
특정 값 기준
🍏 HASH 파티셔닝
PARTITION BY HASH (ACCOUNT_PK)
PARTITIONS 4;
자동 분산
PK값 기준으로 자동 4등분
🍏 KEY 파티셔닝
HASH랑 비슷하지만 MySQL이 내부 해시 사용
성능이 왜 빨라질까?
예를 들어 WHERE ORDER_DATE = '2024-02-01'
RANGE 파티셔닝이면
2024 파티션만 검색
2023 파티션은 아예 안 봄
이걸 Partition Pruning(가지치기) 라고 함
실무에서 많이 쓰는 곳
🔹 주문 테이블 (ORDERS)
🔹 로그 테이블 (CONNECT_LOG)
🔹 게시글 테이블 (BOARD)
🔹 결제 이력
데이터가 계속 쌓이는 테이블
파티셔닝 단점
🔹 설계 복잡
🔹 파티션 키에 인덱스 제약 있음
🔹 PK에 파티션 컬럼 포함해야 함 (MySQL 제약)
🔹 너무 많이 나누면 오히려 느림
ItemRepository의 쿼리문 하나
// 상품 목록 조회 (카테고리 + 검색 + 페이징 + 정렬)
private static final String SELECT_ALL_ITEM =
// 1. 회원 정보 + 누적 구매 금액 계산
"WITH acct AS ( " +
" SELECT " +
" a.ACCOUNT_PK AS accountPk, " + // 회원 PK
" DATE(a.ACCOUNT_DATE) AS joinedDate, " + // 가입일
" a.ACCOUNT_ROLE AS accountRole, " + // 회원 등급
" IFNULL(SUM(oi.ORDERS_ITEM_PRICE * oi.ORDERS_ITEM_COUNT), 0) AS totalAmount " + // 총 구매 금액
" FROM ACCOUNT a " +
" LEFT JOIN ORDERS o ON o.ACCOUNT_PK = a.ACCOUNT_PK " +
" LEFT JOIN ORDERS_ITEM oi ON oi.ORDERS_PK = o.ORDERS_PK " +
" WHERE a.ACCOUNT_PK = ? " + // 조회 대상 회원
" GROUP BY a.ACCOUNT_PK, DATE(a.ACCOUNT_DATE), a.ACCOUNT_ROLE " +
"), " +
// 2️. 상품별 최대 이벤트 할인율 계산
"event_max AS ( " +
" SELECT " +
" i.ITEM_PK AS itemPk, " +
" MAX(IFNULL(e.EVENT_DISCOUNT_RATE, 0)) AS maxDiscountRate " +
" FROM ITEM i " +
" JOIN EVENT e " +
" ON JSON_CONTAINS(e.EVENT_TARGET_CATEGORY, JSON_QUOTE(i.ITEM_CATEGORY)) " + // 이벤트 대상 카테고리 매칭
" AND CURRENT_DATE BETWEEN e.EVENT_START_DATE AND e.EVENT_END_DATE " + // 이벤트 기간 체크
" LEFT JOIN acct a ON 1 = 1 " +
" WHERE ( " +
// 비회원 or 전체 이벤트
" (a.accountPk IS NULL AND (e.EVENT_TARGET_ACCOUNT->>'$.type') = 'ALL') " +
" OR ( " +
" a.accountPk IS NOT NULL AND ( " +
// 전체 회원 대상 이벤트
" (e.EVENT_TARGET_ACCOUNT->>'$.type') = 'ALL' " +
// 구매 금액 기준 이벤트
" OR ( " +
" (e.EVENT_TARGET_ACCOUNT->>'$.type') = 'AMOUNT' " +
" AND a.totalAmount >= CAST(e.EVENT_TARGET_ACCOUNT->>'$.amount' AS UNSIGNED) " +
" ) " +
// 가입 기간 기준 이벤트
" OR ( " +
" (e.EVENT_TARGET_ACCOUNT->>'$.type') = 'JOINED' " +
" AND a.joinedDate BETWEEN " +
" STR_TO_DATE(e.EVENT_TARGET_ACCOUNT->>'$.startDate', '%Y-%m-%d') " +
" AND STR_TO_DATE(e.EVENT_TARGET_ACCOUNT->>'$.endDate', '%Y-%m-%d') " +
" ) " +
// 회원 등급 기준 이벤트
" OR ( " +
" (e.EVENT_TARGET_ACCOUNT->>'$.type') = 'MEMBER_TYPE' " +
" AND JSON_CONTAINS( " +
" JSON_EXTRACT(e.EVENT_TARGET_ACCOUNT, '$.memberType'), " +
" JSON_QUOTE(a.accountRole) " +
" ) " +
" ) " +
" ) " +
" ) " +
" ) " +
" GROUP BY i.ITEM_PK " +
"), " +
// 3️. 상품별 리뷰 평균 평점 계산
"review_avg AS ( " +
" SELECT " +
" r.ITEM_PK AS itemPk, " +
" IFNULL(ROUND(AVG(r.REVIEW_STAR), 2), 0) AS itemAvgStar " +
" FROM REVIEW r " +
" GROUP BY r.ITEM_PK " +
") " +
// 4️. 최종 상품 조회
"SELECT " +
" i.ITEM_PK AS itemPk, " +
" i.ITEM_NAME AS itemName, " +
" i.ITEM_PRICE AS itemPrice, " +
" i.ITEM_IMAGE_URL AS itemImageUrl, " +
" i.ITEM_CATEGORY AS itemCategory, " +
" IFNULL(em.maxDiscountRate, 0) AS itemDiscountRate, " +
// 할인 적용 가격 계산
" CASE " +
" WHEN IFNULL(em.maxDiscountRate, 0) > 0 " +
" THEN ROUND(i.ITEM_PRICE * (1 - IFNULL(em.maxDiscountRate, 0) / 100), 0) " +
" ELSE i.ITEM_PRICE " +
" END AS itemDiscountPrice, " +
" IFNULL(ra.itemAvgStar, 0) AS itemAvgStar " +
"FROM ITEM i " +
"LEFT JOIN event_max em ON em.itemPk = i.ITEM_PK " +
"LEFT JOIN review_avg ra ON ra.itemPk = i.ITEM_PK " +
// 5️. 필터 조건
"WHERE ( ? = 'ALL' OR i.ITEM_CATEGORY = ? ) " + // 카테고리 필터
" AND ( ? IS NULL OR ? = '' OR i.ITEM_NAME LIKE CONCAT('%', ?, '%') ) " + // 검색어 필터
// 6️⃣ 정렬 조건
"ORDER BY " +
" CASE WHEN ? = 'popular' THEN IFNULL(ra.itemAvgStar, 0) END DESC, " +
" CASE WHEN ? = 'discount' THEN IFNULL(em.maxDiscountRate, 0) END DESC, " +
" CASE WHEN ? = 'new-reverse' THEN i.ITEM_PK END ASC, " +
" CASE WHEN ? = 'default' THEN i.ITEM_PK END DESC, " +
" i.ITEM_PK DESC " +
// 7️. 페이징
"LIMIT ? OFFSET ? ";
지금 쿼리문들은 조회할 때 다 계산하는 구조이다
쿼리를 효율적으로 만드려면 미리 계산해두고 조회는 가볍게
나의 코드에서
1️⃣ ACCOUNT_TOTAL_AMOUNT 컬럼 추가
2️⃣ ITEM_AVG_STAR 컬럼 추가
를 하면 좋아질 것 같다
근데 컬럼 추가를 하면 또 메모리를 그만큼 사용하는거니까..
설계 때는 컬럼 추가는 하지 않기로 했다
아카이빙 테이블 분리
아카이빙 테이블 분리란 자주 쓰는 데이터와 거의 안쓰는 데이터를 물리적으로 테이블을 나누는 것
왜 나누냐?
EVENT 테이블을 예로 들어보자
시간이 지나면 2023년 이벤트, 2024년 이벤트, 2025년 이벤트
종료된 이벤트 등 수천~수만 건이 존재할 것이다
하지만 실제로 자주 조회하는 건
현재 진행중인 이벤트와 최근 이벤트이다
그런데 테이블에 옛날 데이터까지 다 쌓여있으면
▪ 인덱스 커짐
▪ 디스크 I/O 증가
▪ 캐시 적중률 떨어짐
▪ 정렬 느려짐
그래서 이렇게 해보았다
🔹 ACTIVE 테이블 : EVENT_ACTIVE
▪ 현재 진행중
▪ 또는 최근 이벤트
▪ 조회가 잦음
▪ 작게 유지
🔹 HISTORY 테이블 : EVENT_HISTORY
▪ 종료된 이벤트
▪ 거의 조회 안 함
▪ 백업성 데이터
| 구분 | 파티셔닝 | 아카이빙 |
| 물리적 분리 | 내부적으로 분리 | 완전히 다른 테이블 |
| 관리 난이도 | 높음 | 낮음 |
| 실무 사용률 | 낮음 | 매우 높음 |
| 운영 편의성 | 복잡 | 쉬움 |
🔹 아카이빙 구조
1. 진행중/활성 이벤트
2. 종료된 이벤트 (보관용)
캐싱 = 미리 계산해두고 다시 쓰는 것
'🍏 개발일기' 카테고리의 다른 글
| AWS 계정 삭제하는 방법 (0) | 2026.02.15 |
|---|---|
| 노랭이 96P 부터 시작 90번 ~ 100번 (0) | 2026.02.13 |
| private SqlSession sqlSession; 에 대해 분석해보았습니다 (0) | 2026.02.12 |
| 코드 분석 : 글 1번 클릭했을 때 전체 흐름 (0) | 2026.02.10 |
| 스프링에서의 정적 리소스(static resources) 핸들러 (0) | 2026.02.10 |