DEVELOPMENT NOTE / Modu Square
대용량 데이터 목록 조회 최적화
문제
SELECT article_id, title, content, board_id, writer_id, created_at, modified_at
FROM article
WHERE board_id = :boardId
ORDER BY article_id DESC
LIMIT :pageSize OFFSET :offset;
- 페이지가 깊어질수록 앞쪽 게시글을 순서대로 건너뛰는 OFFSET 비용 증가
- 제목과 본문을 함께 조회하므로 반환하지 않을 게시글도 Clustered Index에서 읽는 문제
기술 선택 이유
-
Keyset Pagination — 제외
- OFFSET 없이 일정한 조회 성능을 얻지만 특정 페이지 번호로 바로 이동할 수 없음
- 현재 페이지 번호 이동 UI를 유지해야 해 우선 적용 대상에서 제외
-
Covering Index — 채택
- 페이지 번호 이동을 유지하면서 건너뛸 게시글의 원본 접근 제거
board_id와article_id만 먼저 조회하고 화면에 표시할 30개 ID만 원본과 조인
SELECT article.*
FROM (
SELECT article_id
FROM article
WHERE board_id = :boardId
ORDER BY article_id DESC
LIMIT :limit OFFSET :offset
) page
JOIN article ON page.article_id = article.article_id;
- 건너뛸 게시글은 제목과 본문을 읽지 않고 ID만 조회
- Secondary Index에서도 OFFSET만큼 ID를 건너뛰는 비용은 유지
측정
page 100,000 — 커버링 인덱스 적용 전·후
| 방식 | 실행 시간 | 조회 범위 |
|---|---|---|
| 기존 목록 조회 | 약 3.8s | 건너뛸 게시글도 원본에서 조회 |
| ID 선조회 후 원본 조인 | 약 0.3s | 최종 30건만 원본에서 조회 |
- page size 30, OFFSET 2,999,970에서 같은 위치 비교
page 500,000 — 남은 OFFSET 비용
| 방식 | 실행 시간 | 조회 범위 |
|---|---|---|
| 커버링 인덱스 + OFFSET | 9.42s | 게시글 ID 14,999,970개를 순서대로 건너뜀 |
| 같은 위치의 Keyset 조회 | 5.5ms | lastArticleId 이후 30건만 조회 |
- 커버링 인덱스 적용 후에도 깊은 페이지의 OFFSET 탐색 비용은 남음
- 최신 글부터 보는 서비스 특성상 page 500,000을 직접 조회할 가능성은 낮음
Keyset Pagination 전환 조건
SELECT article_id, board_id, writer_id, title, content, created_at
FROM article
WHERE board_id = :boardId
AND article_id < :lastArticleId
ORDER BY article_id DESC
LIMIT :pageSizePlusOne;
- 앞선 게시글을 건너뛰지 않아 깊은 위치에서도 일정한 조회 성능
- 특정 페이지 번호로 바로 이동할 수 없는 제약
- 현재는 번호 페이지 유지. 무한 스크롤이나 연속 탐색이 중요해지면 Keyset으로 전환