본문 바로가기
Q&A해결2026년 7월 24일1분 읽기

게시판 뒤쪽 페이지로 갈수록 목록 쿼리가 느려지는데 OFFSET 때문일까요?

p
page_taeho
조회 983댓글 2

게시판 목록을 OFFSET/LIMIT로 페이징하고 있는데, 1페이지는 즉시 나오는데 3000페이지쯤(OFFSET 60000) 가면 응답이 몇 초씩 걸립니다. created_at에 인덱스도 걸려 있어요.

SELECT id, title, created_at
FROM posts
ORDER BY created_at DESC
OFFSET 60000 LIMIT 20;

이상한 게, 앞쪽 페이지는 같은 쿼리인데도 빠르거든요. 인덱스가 있으면 뒤 페이지도 바로 20개만 집어오면 되는 거 아닌가 싶은데, 왜 페이지가 깊어질수록 점점 느려지는 걸까요? 데이터는 한 30만 건쯤 됩니다.

댓글 2

김김영삼3개월 전
OFFSET의 동작 방식 때문이에요. OFFSET 60000은 '앞의 6만 건을 건너뛴다'인데, DB는 그 6만 건을 그냥 스킵하는 게 아니라 실제로 정렬 순서대로 읽어서 하나씩 버리고, 그 다음 20건을 돌려줍니다. 즉 뒤 페이지일수록 버리는 양이 늘어나니 비용이 페이지 깊이에 비례해서 커져요. 인덱스가 있어도 '읽고 버리는' 건 피할 수가 없습니다. 앞 페이지가 빠른 건 버릴 게 적어서고요.

해결은 keyset 페이지네이션(커서 방식)으로 바꾸는 겁니다. 마지막으로 본 행의 정렬 키를 기억했다가 그 다음부터 집는 방식이에요.

SELECT id, title, created_at FROM posts
WHERE created_at < :lastCreatedAt
ORDER BY created_at DESC LIMIT 20;

이러면 인덱스로 시작 지점을 바로 찾아 들어가서, 몇 페이지든 딱 20건만 읽습니다. 페이지 깊이랑 상관없이 일정하게 빨라요.

한 가지 주의는 created_at이 겹치는 행이 있으면 경계에서 건너뛰거나 중복이 생길 수 있어서, (created_at, id) 복합으로 타이브레이커를 두는 게 안전합니다. WHERE (created_at, id) < (:lastCreatedAt, :lastId) 식으로요. 인덱스도 (created_at DESC, id DESC)로 맞춰두면 좋고요.

단점은 '임의 페이지 번호로 점프'가 안 된다는 건데, 무한 스크롤이나 다음/이전 방식이면 keyset이 정답입니다. 페이지 번호가 꼭 필요하면 깊은 페이지를 제한하거나 별도로 처리하세요.
ppage_taeho(guest)3개월 전
keyset으로 바꾸니까 500페이지든 5000페이지든 응답이 똑같이 빠르네요. (created_at, id) 복합 조건까지 넣으니 경계에서 글 하나 튀던 것도 없어졌어요. OFFSET이 뒤에서 다 읽고 버리는 거였다니... 말씀대로 하니 되네요 🙏
Ctrl+Enter로 등록