대용량 목록 조회 최적화

목록 쿼리 응답 3.8s→0.3s

1,400만 건 게시글 깊은 페이지 조회를 Covering Index로 최적화

JavaSpringMySQLCovering Index

아키텍처

Before · OFFSET 깊은 페이지 조회Secondary IndexClustered Index(board_id, article_id)실제 본문 데이터offset 0article 0offset 1article 1offset …article …offset 2,999,969article 2,999,969offset 2,999,970article 2,999,970offset …article …offset 2,999,999article 2,999,999버려질 row까지 본문 접근After · Covering Index 적용Secondary IndexClustered Index(board_id, article_id)실제 본문 데이터offset 0article 0offset 1article 1offset …article …offset 2,999,969article 2,999,969offset 2,999,970article 2,999,970offset …article …offset 2,999,999article 2,999,999필요한 30건만 본문 접근

개요

  • 1,400만 건 게시글 테이블의 깊은 페이지 목록 조회 성능 최적화
  • OFFSET 스캔 자체보다 본문 테이블 Access 비용 축소에 집중

문제

  • OFFSET 2,999,970 구간까지 읽고 버리는 구조로 조회 비용 증가
  • Secondary Index 조회 후 본문 테이블을 다시 읽는 랜덤 I/O 발생
  • 깊은 페이지 조회에서 실제 본문 row 접근 범위 과도
  • 임의 페이지 이동 요구로 cursor 기반 keyset pagination만 적용하기 어려운 화면 구조

해결 전략

  • (board_id, article_id) 커버링 인덱스로 게시글 ID 목록 먼저 조회
  • 조회된 ID만 본문 테이블과 JOIN해 테이블 접근 범위 축소
  • OFFSET 스캔 비용은 남지만 ID를 먼저 조회한 뒤 JOIN해 본문 row 접근과 랜덤 I/O 비용 축소

기술 선택 이유

  • Covering Index

    • 깊은 페이지에서 OFFSET으로 건너뛰는 구간을 인덱스 엔트리 중심으로 처리
    • 필요한 ID를 찾은 뒤 본문 테이블에 접근해 랜덤 I/O 범위 축소
    • 정렬 기준과 필터 조건을 함께 만족하는 인덱스 설계 가능
  • Keyset Pagination 제외

    • 마지막 조회 ID 기반으로 다음 페이지를 넘기는 구조는 성능상 유리
    • 임의 페이지 번호 이동 UI 요구와 맞지 않아 기본 목록 조회 방식에서 제외

검증

테스트 설정

  • 게시글 약 1,400만 건 적재
  • 동일 board 기준 깊은 페이지 조회
  • 기존 OFFSET 쿼리와 개선 쿼리(ID 선 조회 후 JOIN) 실행 시간 비교
  • 임의 페이지 이동 요구가 있는 조건에서 동일 offset 기준 비교

측정 결과

  • 목록 쿼리 응답 3.8초에서 0.3초로 감소
  • OFFSET 스캔은 유지되지만 본문 테이블 접근 범위 축소

핵심 쿼리

  • 인덱스로 ID 목록 먼저 조회하는 구조
  • 필요한 ID만 본문 테이블과 JOIN
SELECT a.article_id,
       a.title,
       a.content,
       a.board_id,
       a.writer_id,
       a.created_at,
       a.modified_at
FROM (
    SELECT article_id
    FROM article
    WHERE board_id = :boardId
    ORDER BY article_id DESC
    LIMIT :limit OFFSET :offset
) page
JOIN article a ON a.article_id = page.article_id;

배운 점

  • 응답 시간의 핵심 비용은 인덱스에서 찾은 PK로 본문 테이블을 다시 읽을 때 발생하는 random I/O
  • Covering Index로 필요한 30건만 본문에 접근하게 해 random I/O를 크게 축소