✅ 1. Offset
offset(어디서부터?), limit(몇개를?)
SELECT ...
FROM ...
WHERE ...
ORDER BY id DESC
OFFSET {page_number}
LIMIT {page_size}
offset 은 pageNumber 에 해당하는 행만큼 데이터를 읽어들인 후 다시 pageSize 만큼의 행을 읽고 앞에 읽은 행을 삭제하는 과정을 거친다.
즉 만약 10020개의 데이터가 있다고 하자. 나는 뒤에 20개의 데이터만 읽고싶어도 offset에서는 앞에 1000개를 다 읽고 20개를 읽는다.
✅ 2. No Offset
무한스크롤에 적합한 방식
SELECT ...
FROM ...
WHERE ...
AND id < ?last_seen_id
ORDER BY id DESC
FETCH FIRST 10 ROWS ONLY
마지막 조회한 행의 id값을 기준으로 읽지 않은 행을 page size만큼 조회하면 됩니다. 이를 No Offset방식이라고 하며 id라는 클러스터 인덱스를 사용하기 때문에 시작부분을 빠르게 찾아 조회가 가능
- page : 페이지 번호
- size : 한 페이지에 불러올 데이터 건수
- sort : 정렬 조건
@Override
public Slice<PostDslDto.ResponseDto> findPostsByLatest(Pageable pageable) {
QPost post = QPost.post;
QUser user = QUser.user;
List<Post> posts = queryFactory
.selectFrom(post)
.leftJoin(post.user, user).fetchJoin()
.orderBy(post.createdAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize() + 1)
.fetch();
boolean hasNext = posts.size() > pageable.getPageSize();
if(hasNext) {
posts.remove(posts.size() - 1);
}
.
.
(중략)
.
.
}
if(hasNext) {
posts.remove(posts.size() - 1);
}
// 전체 게시물 조회
public Slice<PostDto.ResponseDto> getPostsByLatest(Pageable pageable) {
Slice<PostDslDto.ResponseDto> postSlice = postRepository.findPostsByLatest(pageable);
List<PostDto.ResponseDto> content = postSlice.getContent().stream()
.map(post -> {
Long likesCount = getLikesCount(post.getPostId(), LikeCategoryEnum.LIKE);
Long localLikesCount = getLikesCount(post.getPostId(), LikeCategoryEnum.LOCAL_LIKE);
.
.
(중략)
.
.
return new SliceImpl<>(content, pageable, postSlice.hasNext());
}
}
public ResponseEntity<Slice<PostDto.ResponseDto>> getPostsByLatest(
@PageableDefault(
size = 10,
sort = "createdAt",
direction = Sort.Direction.DESC) Pageable pageable) {
Slice<PostDto.ResponseDto> postsSlice = postService.getPostsByLatest(pageable);
return ResponseEntity.ok(postsSlice);
}
"pageable": {
"pageNumber": 0,
"pageSize": 10,
"sort": {
"empty": false,
"sorted": true,
"unsorted": false
},
"offset": 0,
"paged": true,
"unpaged": false
},
"size": 10,
"number": 0,
"sort": {
"empty": false,
"sorted": true,
"unsorted": false
},
"numberOfElements": 10,
"first": true,
"last": false,
"empty": false
}
페이지 시작은 0부터
Slice는 Page에서 카운트 쿼리에 많은 비용이 발생하는 경우에 Slice를 사용하면 된다.
Slice는 다음 Slice가 존재하는지 여부만 알기 때문에 전체 데이터의 셋의 크기가 큰 경우에는 Slice를 사용하는 것이 성능상 유리하다.
무한스크롤에 적합하다.
page는 사용 가능한 데이터의 총 개수 및 전체 페이지 수를 알 수 있다.
총 개수를 알아내기 위해 추가적으로 카운트 쿼리가 실행된다.
기본적으로 카운트 쿼리는 실제로 실행되는 쿼리에서 파생된다.
Pageable을 통해서 정렬을 할 수 있지만, 정렬만 하는 경우 Sort를 사용하는 것이 좋다.
결과를 단순히 List로 받을 수 있다.
이 경우 Page 인스턴스를 생성하기 위한 메타데이터가 생성되지 않기 때문에 카운트 쿼리가 실행되지 않는다.
단순히 주어진 범위내의 엔티티를 검색하기 위한 쿼리만 실행된다.