🍏 개발일기

DB 파티셔닝(Partitioning)이란?

보배 진 2026. 2. 12. 17:10

📦 파티셔닝이란?

하나의 큰 테이블을 여러 개로 나눠서 저장하는 기술

하지만 중요한 건 : 논리적으로 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. 종료된 이벤트 (보관용)

 

 

 

 

 

 

 

 

캐싱 = 미리 계산해두고 다시 쓰는 것