뒷 페이지로 갈수록 목록이 느려진 이유 — OFFSET은 건너뛴 행도 읽는다
문제 발생
목록 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을 쓰지 말고, 대신 어디까지 봤는지를 기억하라."
해결 방안
- 마지막으로 본 키를 조건으로 넘깁니다(keyset). 문서가 제시하는 형태 그대로입니다.
SELECT id, title FROM dev_posts
WHERE deleted_at IS NULL AND id < :left_off
ORDER BY id DESC
LIMIT 10;읽는 행 수가 페이지 깊이와 무관하게 항상 10개 남짓입니다.
-
인덱스를 이 조건 순서에 맞춥니다. 필터가 있으면
(technology_id, id)처럼 필터 컬럼 다음에 커서 컬럼이 오는 복합 인덱스여야 범위 스캔이 그대로 이어집니다. -
정렬키가 유일하지 않으면 복합 커서로 만듭니다.
created_at처럼 중복이 가능한 값으로 자르면 동률 지점에서 행이 사라지거나 두 번 나옵니다.(created_at, id)를 함께 비교합니다.
WHERE (created_at, id) < (:last_created_at, :last_id)
ORDER BY created_at DESC, id DESC
LIMIT 10-
"다음 페이지가 있는지"는 COUNT로 묻지 않습니다.
LIMIT 11로 하나 더 가져와 11번째의 존재 여부로 판단하면 추가 쿼리가 필요 없습니다. -
임의 페이지 번호 이동이 정말 필요한지 확인합니다. keyset은 "다음/이전"에는 완벽하지만 "347페이지로 점프"는 못 합니다. 대개 그 기능은 실제로 쓰이지 않으며, 필요하다면 검색·필터로 좁히는 UX가 더 맞습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.