커서 베이스 페이지네이션 구현하기

avatar

커서 베이스(Cursor-based) 페이지네이션이란?

no offset 이라고도 하며
커서(고유식별자)를 사용하여 해당 지점을 기준으로
데이터를 탐색하는 페이지네이션 방식이다.

오프셋 베이스(Offset-based) 방식에서 발생하는 문제

  • 퍼포먼스

    오프셋 방식을 사용하는 경우 데이터베이스에서 요청된 행을 반환할때 오프셋 값까지 모든 행을 탐색해야한다.
    데이터 규모가 방대해짐에 따라 점차 많은 행을 건너뛰어야하기 때문에 점점 느려질 수 있다.

SELECT *
FROM posts
ORDER BY id ASC
LIMIT 5 OFFSET 100000;

100000개의 데이터를 탐색후 이후 5개를 가져오고
나머지 100000개의 데이터는 버리는 방식이기 때문에
OFFSET이 증가할수록 점점 성능은 저하하게 된다.

  • 신뢰성

    새로운 데이터가 추가되거나 삭제될 때 중복된 데이터를 가져오거나 누락될 수 있다.
    행이 추가되거나 삭제될 경우 오프셋 값이 변경될 수 있기 때문이다.
    때문에 정확한 데이터를 가져오지 힘들다.

e.g.
페이지당 5개씩 게시물 데이터를 조회할때

페이지 1 offset: 0
페이지 2 offset: 5
페이지 3 offset: 10

페이지 2의 경우
offset 5로
id 6부터 시작하게된다.

SELECT *
FROM posts
ORDER BY id ASC
LIMIT 5 OFFSET (1 * 5);
offset1234offset(5)*****
id12345678910

output: [6,7,8,9,10]


id 1, 2 데이터가 삭제되고 다음 페이지 3으로 넘어간 경우
원래대로라면 offset 10, id는 11부터 시작해야했지만
기존 행의 삭제로 id 13부터 시작하게된다.

SELECT *
FROM posts
ORDER BY id ASC
LIMIT 5 OFFSET (2 * 5);
offsetdeldel12345
id1234567
offset6789offset(10)*****
id891011121314151617

output: [13,14,15,16,17]

이 결과 11,12번 게시글은 확인이 불가능하며
반대로 데이터가 추가될 경우 중복된 데이터를 조회할 수 있다.

커서 베이스 페이지네이션 구현

e.g.
페이지당 5개씩 게시물 데이터를 조회할때
cusor를 id로 사용할 경우 마지막 id를 cursor로 사용

페이지 1 cursor: 0
페이지 2 cursor: 5
페이지 3 cursor: 10

페이지 2의 경우
페이지 1의 마지막 cursor(id: 5)를 기준으로 가져온다.

SELECT *
FROM posts
WHERE id > cursor (5)
ORDER BY id ASC
LIMIT 5;
cursor*****
5678910

output: [6,7,8,9,10]

id 1, 2 데이터가 삭제되고 다음 페이지 3으로 넘어간 경우
페이지 2의 마지막 cursor(id: 10)를 기준으로 가져온다

SELECT *
FROM posts
WHERE id > cursor (10)
ORDER BY id ASC
LIMIT 5;
cursor*****
101112131415

output: [11,12,13,14,15]

위 예제로 알 수 있듯이 데이터 행의 변동 여부와 상관 없이
안정적으로 원하는 데이터를 정확하게 가져올 수 있으며
오프셋 방식과 달리 이전데이터를 탐색하지 않기때문에 성능면에서도 효율적이다.

결론 - OFFSET은 사용하지 말아야할까?

커서 기반은 성능과 데이터 신뢰성 모두 오프셋 기반에 비해 우수하지만
구현방법이 더 복잡하고 (데이터 정렬방식, 조건 등이 복잡해질수록 더)
구현방법이 복잡해짐에 따라 오히려 오프셋보다 성능이 저하될 가능성도 없지 않다.

따라서 페이지네이션을 구현할 때 요구사항에 따른 적합한 방식을 선택하는것이 중요하다.
예를들면 데이터 규모가 방대하고 조회가 잦을수록 커서 기반 선택을 피할수 없겠지만
반대의 경우라면 오히려 오프셋 방식이 옳은 선택일 수 있다.