[2주차 스터디] Pagination(offset / cursor) 개념과 종류

seonseon·2025년 3월 24일
0

연합동아리_umc

목록 보기
2/6
post-thumbnail

오늘은 mysql 기준 pagination 개념과 사용방식에 대해서 정리하려고 한다.
데이터베이스의 pagination 개념은 또 처음 보는 개념이라 스터디 하는 김에 포스팅한다!


📍pagination이란?

  • 검색 결과를 가져올 때 데이터를 쪼개 번호를 매겨 일부만 가져오는 기법

  • 이때, 특정한 정렬 기준에 따라 + 지정된 갯수의 데이터를 가져오는 것

  • 위 사진처럼 한 페이지 당 몇 개의 콘텐츠를 보여줄 것인지 쿼리를 작성하는 것을 뜻한다.

📍pagination을 사용하는 이유

사용자가 상품 목록 등을 요청 시 결과 값이 총 100만개(매우 많을)일 경우 조회가 느려지는 상황을 방지하기 위해서 사용한다.

📍pagination 구현 방법(2가지)

1️⃣ offset-based Pagination 방식

  • 데이터베이스 쿼리의 limit, offset 문법을 이용하는 방식
  • 우리가 원하는 데이터가 '몇 번째'에 있다는 데에 집중하고 있다.

2️⃣ cursor 방식

  • cursor(사용자가 응답해 준 마지막 데이터의 식별자 값)을 기준으로 n개의 데이터를 응답해주는 방식(= keyset pagination)
  • 우리가 원하는 데이터가 '어떤 데이터의 다음'에 있다는 것에 집중

🚩 offset-based Pagination

먼저 offset-based pagination을 살펴보려고 한다.
앞서 MySQL에서는 limit은 두 가지 방식으로 사용할 수 있는데, 의미는 학생 목록에서 뒤에서부터 ID 기준으로 정렬한 후, 21번째부터 60번째까지의 40개 데이터를 가져오는 것으로 동일하다.

1️⃣ LIMIT 20, 40(offset 키워드 생략)

LIMIT 20, 40 : 20개 건너뛰고(offset), 그다음 40개 가져오기

SELECT id FROM `students` ORDER BY id DESC LIMIT 20, 40

2️⃣ LIMIT 40 OFFSET 20(offset 키워드 포함)

SELECT id FROM `students` ORDER BY id DESC LIMIT 40 OFFSET 20;
``
  • OFFSET: 시작 위치 (0부터 시작)

  • LIMIT: 가져올 개수

⛔️ offset pagination의 문제점

1. 중복 데이터 노출

만약 1~20페이지의 row를 불러와서 유저에게 1페이지를 띄워준 후, 그 사이에 사이트 담당자가 5개의 상품을 새로 올렸을 경우 2페이지로 넘겼을 때, 1페이지에서 봤던 5개의 상품이 2페이지에 보여짐. => 중복 데이터를 보일 수 있다

2. OFFSET 쿼리의 퍼포먼스 이슈

  • row의 수가 많은 경우 offset 값이 올라가면 쿼리의 퍼포먼스는 떨어진다.

💡offset pagination만으로도 충분한 경우

물론 모든 것이 cursor페이지네이션으로 이루어져야 하는 것은 아니다.
왜냐? 필요없는 경우도 있기 때문!
그런 경우에 대해 한 번 나열해보도록 하겠다.

  1. 데이터의 변화가 없어서 중복 데이터에 노출될 일이 없는 경우
  2. 데이터 중복이 있어도 상관 없는 경우
  3. 데이터가 적어서 굳이 cursor 기반을 사용하지 않아도 되는 경우
  • 주로 유저들이 사용하는 서비스 페이지에서는 커서 기반 사용, 백오피스는 오프셋 기반 사용!

🚩 Cursor-based Pagination

위에 설명한 내용처럼 커서 기반 페이징은 마지막으로 본 항목 이후부터 가져오는 방식이다.

📍 id = 3보다 더 옛날에 생성된 책 중에서 15개를 가져오는 쿼리

SELECT * 
FROM book 
WHERE created_at < (SELECT created_at FROM book WHERE id = 3) 
ORDER BY created_at DESC 
LIMIT 15;

📍 정렬 기준 1개
where절을 살펴보면 id가 996 이상인 것들을 5개(limit) 가져올 수 있는데
여기서, cursor가 product 테이블의 id 996이다.

SELECT id, title
  FROM `products`
  WHERE id < 996
  ORDER BY id DESC
  LIMIT 5

📍 정렬 기준 2개
커서 기반 페이지네이션을 위해서는 반드시 정렬 기준이 되는 필드 중 (적어도 하나는) 고유값이어야 하므로, 예시로 정렬기준이 price일 경우 무조건 고유값인 id를 두 번째 정렬 기준으로 추가해야 한다.

SELECT id, title, price
	FROM `products`
	WHERE 
		(price > 14100
			OR
    (price = 14100 AND id > 446))
	ORDER BY price ASC, id ASC
	LIMIT 5

=> 이 경우 클라이언트가 ORDER BY에 걸려있는 모든 필드를 알아야 하므로 문제가 생김

그래서 항상 같은 방향으로 특정 값을 부여하고, 이를 cursor로 사용할 수 있게 만들어야 한다.

📍 커스텀 cursor 생성

정렬 기준들을 문자열로 합쳐서 "고유하고 일관된 커서 값"을 만들어주려는 것

SELECT id, title, price,
		CONCAT(LPAD(price, 10, '0'), LPAD(id, 10, '0')) as `cursor`
	FROM `products`
	ORDER BY price DESC, id DESC
	LIMIT 5;

여기서 핵심은 CONCAT() + LPAD() 조합이다.

🧩 LPAD 함수란?
숫자나 문자열 앞쪽을 특정 문자로 채워서 고정된 길이로 만드는 함수이다.

LPAD(값, 총길이, 채울문자) // LPAD(123, 5, '0') → "00123"

🧩 CONCAT 함수란?
문자열들을 붙여서 하나로 만드는 함수

 CONCAT(문자열1, 문자열2, ...)
함수역할
LPAD(price, 10, '0')가격을 앞에 0을 채워서 10자리 문자열로 변환
LPAD(id, 10, '0')ID도 마찬가지로 10자리로 만듦
CONCAT(...)이 둘을 붙여서 하나의 "정렬 기준 커서값"으로 사용

위 쿼리는 아까 order by에 사용되었던 정렬 기준인 가격과 id를 lpad 문법으로 10자리 문자열로 변환한 뒤, 합쳐서 하나의 정렬 기준 커서값인 "cursor"로 만들어 절대 순서를 만든 것이다.

참고자료 : https://velog.io/@minsangk/%EC%BB%A4%EC%84%9C-%EA%B8%B0%EB%B0%98-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98-Cursor-based-Pagination-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0

0개의 댓글