수 백, 수 천, 수 만개가 넘는 게시물들을 하나의 화면에 노출할 수 없다
=> 많은 양의 데이터를 어떻게 노출 시킬 것 인가
페이지네이션
스크롤
#sql
SELECT *
FROM post
ORDER BY ____
WHERE memberId = :memberId
LIMIT __ # size
OFFSET __; # page
service:
public Page<Post> getPosts(Long memberId, Pageable pageable) {
return postRepository.findAllByMemberId(memberId, pageable);
}
repository:
public Page<Post> findAllByMemberId(Long memberId, Pageable pageable) {
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("memberId", memberId)
.addValue("size", pageable.getPageSize())
.addValue("offset", pageable.getOffset());
String sql = String.format("""
SELECT *
FROM %s
WHERE memberId = :memberId
ORDER BY %s
LIMIT :size
OFFSET :offset
""", TABLE, PageHelper.orderBy(pageable.getSort()));
List<Post> posts = namedParameterJdbcTemplate.query(sql, params, ROW_MAPPER);
return new PageImpl<>(posts, pageable, getCount(memberId));
}
PageHelper.class
public class PageHelper {
public static String orderBy(Sort sort) {
if (sort.isEmpty()) {
return "id DESC";
}
List<Sort.Order> orders = sort.toList();
List<String> orderBys = orders.stream()
.map(order -> order.getProperty() + " " + order.getDirection())
.toList();
return String.join(", ", orderBys);
}
}
[정리]
[문제]
#sql
SELECT *
FROM post
WHERE member_id = :member_id and id < :id # 조건
ORDER BY id desc
LIMIT __; # size
PageCursor.record
public record PageCursor<T>(
CursorRequest nextCursorRequest,
List<T> body
) {
// 제너릭하게 이용
}
CursorRequest.record
public record CursorRequest(Long key, int size) {
public static final Long NONE_KEY = -1L;
public boolean hasKey() {
return key != null;
}
public CursorRequest next(Long key) {
return new CursorRequest(key, size);
}
}
// request 객체
repository:
public List<Post> findAllByMemberIdAndOrderByIdDesc(Long memberId, int size) {
String sql = String.format("""
SELECT *
FROM %s
WHERE memberId = :memberId
ORDER BY id desc
LIMIT :size
""", TABLE);
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("memberId", memberId)
.addValue("size", size);
return namedParameterJdbcTemplate.query(sql, params, ROW_MAPPER);
}
public List<Post> findAllLessThanIdAndByMemberIdAndOrderByIdDesc(Long id, Long memberId, int size) {
String sql = String.format("""
SELECT *
FROM %s
WHERE memberId = :memberId and id < :id
ORDER BY id desc
LIMIT :size
""", TABLE);
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("memberId", memberId)
.addValue("id", id)
.addValue("size", size);
return namedParameterJdbcTemplate.query(sql, params, ROW_MAPPER);
}
controller:
@GetMapping("/members/{memberId}/by-cursor")
public PageCursor<Post> getPosts(
@PathVariable Long memberId,
CursorRequest cursorRequest) {
return postReadService.getPosts(memberId, cursorRequest);
}
service:
public PageCursor<Post> getPosts(Long memberId, CursorRequest cursorRequest) {
List<Post> posts = findAllBy(memberId, cursorRequest);
Long nextKey = posts.stream()
.mapToLong(Post::getId)
.min().orElse(CursorRequest.NONE_KEY);
return new PageCursor<>(cursorRequest.next(nextKey), posts);
}
private List<Post> findAllBy(Long memberId, CursorRequest cursorRequest) {
if (cursorRequest.hasKey()) {
return postRepository.findAllLessThanIdAndByMemberIdAndOrderByIdDesc(
cursorRequest.key(),
memberId,
cursorRequest.size());
}
return postRepository.findAllByMemberIdAndOrderByIdDesc(
memberId,
cursorRequest.size());
}
[정리]
[index table]
age | id |
---|---|
19 | 1 |
27 | 3 |
30 | 2 |
40 | 5 |
47 | 4 |
[user table]
id | age | name | password | |
---|---|---|---|---|
1 | 19 | ss | ss@ss | 1234 |
2 | 30 | aa | aa@aa | 1234 |
3 | 27 | bb | bb@bb | 1234 |
4 | 47 | cc | cc@cc | 1234 |
5 | 40 | dd | dd@dd | 1234 |
# 1
select age
from user
where age > 30;
# 2
select age, id
from user
where age > 30;
[커버링 인덱스]
커버링 인덱스를 이용해 페이지네이션을 최적화하기 (how?)
ex) 나이가 30 이하인 회원의 이름을 2개만 조회
SELECT name
FROM user
WHERE age < 30
LIMIT 2;
해결 방법
with 커버링 as (
SELECT id
FROM user
WHERE age < 30
LIMIT 2
); # age와 id 모두 인덱스로 커버 가능 -> 원본데이터를 참조할 일 없음
SELECT 이름
FROM user INNER JOIN 커버링 on user.id = 커버링.id; # -> 위 쿼리로 받아온 id