2022-10-11-TIL
2022-10-11-TIL
Today I Learned
Pagination with Cursor
오프셋 페이지네이션은 LIMIT 20 OFFSET 100000처럼 건너뛸 행 수를 지정한다. 문제는 DB가 건너뛸 행을 실제로 읽은 뒤 버린다는 점이다. 뒤 페이지로 갈수록 읽고 버리는 행이 늘어서 느려진다. 또 페이지를 넘기는 사이 앞쪽에 새 행이 추가되거나 삭제되면 같은 행이 두 번 보이거나 빠진다.
커서(키셋) 페이지네이션은 “마지막으로 본 행의 정렬 키 다음부터”를 조건으로 건다.
1
2
3
4
5
6
7
8
9
10
-- 첫 페이지
SELECT id, title, created_at FROM post
ORDER BY created_at DESC, id DESC
LIMIT 20;
-- 다음 페이지: 직전 페이지 마지막 행의 (created_at, id)를 커서로 사용
SELECT id, title, created_at FROM post
WHERE (created_at, id) < (:lastCreatedAt, :lastId)
ORDER BY created_at DESC, id DESC
LIMIT 20;
정렬 키에 인덱스가 있으면 커서 위치로 바로 찾아가서 20건만 읽으므로, 몇 번째 페이지든 비용이 거의 같다. 정렬 키가 유일하지 않으면(같은 created_at이 여러 개) 경계에서 행이 빠지거나 중복되므로 id 같은 유일한 컬럼을 보조 정렬 키로 함께 써야 한다. 같은 조건을 created_at < ? OR (created_at = ? AND id < ?)로 풀어 쓸 수도 있으며, 어느 형태가 인덱스를 타는지는 EXPLAIN으로 확인한다.
대가도 있다. “17페이지로 바로 이동”이 불가능하고 이전·다음 이동만 자연스럽다. 그래서 무한 스크롤이나 피드에 잘 맞고, 페이지 번호가 필요한 관리자 화면에는 오프셋 방식이 여전히 쓰인다. 클라이언트에는 커서 값을 Base64 같은 불투명한 문자열로 감싸 내려 주면 서버가 커서 구성을 바꿔도 API가 유지된다.
- https://dev.to/appwrite/this-is-why-you-should-use-cursor-pagination-4nh5
- https://bojithapiyathilake.medium.com/pagination-offset-vs-cursor-in-mysql-92cbf1a02cfa
- 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
- https://www.eversql.com/faster-pagination-in-mysql-why-order-by-with-limit-and-offset-is-slow/
- https://davidasync.medium.com/why-pagination-is-slow-1734f8709492
- https://medium.com/swlh/how-to-implement-cursor-pagination-like-a-pro-513140b65f32
Pagination Best Practices
검색엔진 관점의 페이지네이션 가이드도 함께 봤다. Google 검색 문서의 권장은 다음과 같다(Google Search Central: Pagination).
- 각 페이지에서 다음 페이지로
<a href>링크를 걸어 크롤러가 순서대로 따라갈 수 있게 하고, 모든 페이지에서 첫 페이지로도 링크한다. - 페이지마다
?page=n처럼 고유한 URL을 준다. - 모든 페이지의 canonical을 첫 페이지로 지정하지 말고, 각 페이지가 자기 자신을 canonical로 갖게 한다.
- Google은 더 이상
rel="next",rel="prev"를 사용하지 않는다. 다른 검색엔진은 쓸 수 있다. Ahrefs 글이 이 변화를 다룬다. - “더 보기” 버튼이나 무한 스크롤은 사용자 동작이 있어야 콘텐츠가 로드되므로, 크롤러가 찾을 수 있게 별도 링크나 사이트맵을 제공한다.
정렬·필터 조합마다 URL이 생겨 비슷한 목록이 대량으로 색인되는 것을 막으려면 해당 URL을 noindex로 두거나 robots.txt로 막는다. 커서 방식 목록을 공개 페이지에 쓴다면 크롤러용으로 따라갈 수 있는 링크를 따로 마련해야 한다는 뜻이기도 하다.
- https://www.seoquake.com/blog/paginated-pages/
- https://www.seroundtable.com/google-search-tests-pagination-28056.html
- https://ahrefs.com/blog/rel-prev-next-pagination/
- https://developers.google.com/search/docs/specialty/ecommerce/pagination-and-incremental-page-loading#sequentially
- https://isquared.wordpress.com/2013/01/22/designing-search-part-6-interacting-with-results/
선분이력
선분이력은 데이터의 변경 이력을 시작 시점과 종료 시점을 가진 구간(선분)으로 저장하는 모델링 기법이다. 예를 들어 상품 가격 이력을 (상품ID, 시작일시, 종료일시, 가격)으로 두고, 현재 유효한 행의 종료일시에는 9999-12-31 같은 최대값을 넣는다.
1
2
3
4
-- 특정 시점의 가격
SELECT price FROM product_price_history
WHERE product_id = :id
AND :at BETWEEN start_at AND end_at;
값이 바뀔 때마다 변경 시점만 기록하는 점이력 방식은 “특정 시점의 값”을 구하려면 그 시점 이전의 가장 최근 행을 찾는 서브쿼리가 필요하다. 선분이력은 BETWEEN 한 번으로 찾을 수 있어 조회가 단순하다. 대신 값이 바뀔 때 기존 행의 종료일시를 닫고 새 행을 넣는 두 단계 쓰기를 한 트랜잭션으로 묶어야 하고, 구간이 겹치거나 비지 않도록 관리해야 한다. 경계값을 포함할지(end_at을 다음 시작 직전으로 둘지, 다음 시작과 같게 둘지) 규칙을 정해 두지 않으면 경계 시점에 두 행이 조회될 수 있다.
- https://moons08.github.io/programming/history_management/
- https://daphne-dev.github.io/2020/11/17/database-005/
댓글
아직 댓글이 없습니다