본문으로 건너뛰기
개발 머꼬
개발 노트SQL
hohyeon.dev15

뒷 페이지로 갈수록 목록이 느려진 이유 — OFFSET은 건너뛴 행도 읽는다

  • #Engineering Note
  • #Performance
  • #SQL

문제 발생

목록 1페이지는 즉시 뜨는데 뒤로 갈수록 느려졌습니다. 크롤러가 마지막 페이지들을 훑는 시간대에는 DB CPU가 눈에 띄게 올라갔습니다.

SELECT id, title FROM dev_posts
WHERE deleted_at IS NULL
ORDER BY id DESC
LIMIT 10 OFFSET 49990;

인덱스는 id에 이미 있었습니다.

원인 분석

인덱스가 있어도 OFFSET은 공짜가 아닙니다. MariaDB 문서는 이 상황을 그대로 설명합니다 — 위 같은 쿼리에서 DB는 5만 개의 행을 모두 찾아낸 다음, 앞의 49,990개를 건너뛰고, 그 먼 페이지의 10개를 돌려줍니다.

즉 비용이 "돌려주는 행 수"가 아니라 "페이지 깊이"에 비례합니다. 1페이지는 10행만 읽고 5,000페이지는 5만 행을 읽습니다. 같은 문서는 크롤러가 모든 페이지를 훑을 때 이 방식이 만들어내는 총 읽기량이 어떻게 폭발하는지도 계산해 보여줍니다.

같은 문서의 권고는 한 줄입니다 — "OFFSET을 쓰지 말고, 대신 어디까지 봤는지를 기억하라."

해결 방안

  1. 마지막으로 본 키를 조건으로 넘깁니다(keyset). 문서가 제시하는 형태 그대로입니다.
SELECT id, title FROM dev_posts
WHERE deleted_at IS NULL AND id < :left_off
ORDER BY id DESC
LIMIT 10;

읽는 행 수가 페이지 깊이와 무관하게 항상 10개 남짓입니다.

  1. 인덱스를 이 조건 순서에 맞춥니다. 필터가 있으면 (technology_id, id)처럼 필터 컬럼 다음에 커서 컬럼이 오는 복합 인덱스여야 범위 스캔이 그대로 이어집니다.

  2. 정렬키가 유일하지 않으면 복합 커서로 만듭니다. created_at처럼 중복이 가능한 값으로 자르면 동률 지점에서 행이 사라지거나 두 번 나옵니다. (created_at, id)를 함께 비교합니다.

WHERE (created_at, id) < (:last_created_at, :last_id)
ORDER BY created_at DESC, id DESC
LIMIT 10
  1. "다음 페이지가 있는지"는 COUNT로 묻지 않습니다. LIMIT 11로 하나 더 가져와 11번째의 존재 여부로 판단하면 추가 쿼리가 필요 없습니다.

  2. 임의 페이지 번호 이동이 정말 필요한지 확인합니다. keyset은 "다음/이전"에는 완벽하지만 "347페이지로 점프"는 못 합니다. 대개 그 기능은 실제로 쓰이지 않으며, 필요하다면 검색·필터로 좁히는 UX가 더 맞습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.