포스트

Offset & limit 성능 개선

OFFSET & LIMIT의 문제점

  • 페이지를 조회할 때마다 제일 첫번째 데이터부터 limit까지의 데이터를 검색하게 된다.
  • 그 후에 OFFSET으로 설정된 값만 가지고 나머지는 버린다.

img.png

0 ~ 2까지의 데이터를 조회할 때 0부터시작
9990 ~ 10000까지의 데이터를 조회할 때도 0부터시작 한다.

  • 이로 인해 데이터의 양이 많아 지면 많아 질수록 쿼리의 속도가 느려지게 된다.
  • 사실상 몇천 몇만 건이 되는 데이터를 가지고 있지 않으면 유의미한 수치가 아닐 수 있지만, 문제가 발생하고 조치하는 것보다 미연에 방지하는 것이 더 좋을 것 같아 문제점을 해결해 보려고 한다.




문제가 발생하는지 테스트

  • 실제로 문제가 발생하는지 환경을 만들어 테스트 해보도록 했다.
  • 조건 : 한 페이지당 4건으로 약 2만건의 데이터를 가진 테이블을 조회


1페이지조회.

img.png

약 85ms




400페이지조회.

img.png

약 89ms



500페이지조회.

img.png

약 128ms


약간의 오차 범위는 존재 하지만 페이지를 넘기면 넘길 수록 조회 속도가 느려 지는걸 확인할 수 있었다.

현재는 사람이 느낄 수 있을 정도로 느리지는 않지만 데이터가 늘어 나면 확실히 느려질 거라는 확신이 생겼다.


해결 방안


해결 방안은 크게 2가지가 있었다.


1 - NoOffset 방식

  • NoOffset 방식은 SNS인 유튜브나 인스타그램에서 사용하는 스크롤 페이징 UI에 많이 사용된다.
  • 기존 페이징 방식은 조회 페이지 첫 데이터 ID(offset)페이지 사이즈(limit)기반 이지만
    조회 시작 부분과 조회 끝 부분을 인덱스로 찾고 매번 첫 페이지를 읽도록 하는 방식
1
2
3
4
5
6
SELECT * 
FROM room_post
WHERE 조건문
AND  번째 id < 마지막 id 
ORDER BY id DESC
LIMIT 페이지사이즈 


2 - 커버링 인덱스

  • 필요한 모든 데이터가 인덱스에서만 추출할 수 있도록 하는 방식
  • NoOffset조회 페이지 첫 데이터 ID(offset)를 사용하지 않고 인덱스의 범위를 따로 지정해 줘야 하지만, 커버링 인덱스는 필요한 모든 데이터를 포함할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
SELECT *
FROM room_post as rp
JOIN (
	SELECT id
	FROM room_post
	WHERE 조건문
	ORDER BY id DESC
	OFFSET 조회 페이지  데이터 ID
	LIMIT 페이지사이즈
) as rpids
ON rpids.id = rp.id


해결 방안으로 커버링 인덱스 방식으로 선택했다.
이유는 NoOffset 방식조회 페이지 첫 데이터 ID(offset)를 사용하지 않고 인덱스의 범위를 지정해 줘야 하기 때문에 구조를 수정해야 한다는 번거로움이 존재했다.

반면 커버링 인덱스는 현재 조회 페이지 첫 데이터 ID(offset)페이지 사이즈(limit)를 그대로 사용할 수 있기에 선택했고, 스크롤 페이징 UI가 아닌 pagination Bar를 이용하고 있기 때문에 선택했다.



Querydsl 작성

1
2
3
4
5
6
7
8
List<RoomPost> roomPostList = jpaQueryFactory
        .selectFrom(roomPost)
        .join(roomPost.member, member)
        .where(containsSearch(searchOption, searchContent))
        .limit(pageable.getPageSize())
        .offset(pageable.getOffset())
        .orderBy(roomPost.createAt.desc())
        .fetch();

limitoffset을 사용해서 member테이블만 조인해서 사용하고 있다.


보통 SELECT절은 너무 많은 컬럼을 인덱스로 포함시킬 수 있어서 JOIN에서 커버링 인덱스를 작성하는데 room_postid를 인덱스로 활용했다.


하지만 JPQLfrom절에 서브쿼리를 작성할 수 없기 때문에 JOIN커버링 인덱스를 작성할 수 없었다.
그래서 커버링 인덱스용도의 쿼리를 따로 작성하고 필요한 조건들도 미리 처리해 준다.
그 후, WHERE에서 in을 사용해 커버링 인덱스를 가져와 조회하는 방법으로 처리 했다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
List<Long> roomPostIds = jpaQueryFactory
                .select(roomPost.id)
                .from(roomPost)
                .where(containsSearch(searchOption, searchContent))
                .orderBy(roomPost.id.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        List<RoomPost> roomPostList = jpaQueryFactory
                .selectFrom(roomPost)
                .join(roomPost.member, member)
                .where(roomPost.id.in(roomPostIds))
                .fetch();




개선


개선 후에 같은 조건으로 테스트 해보았다.

  • 조건 : 한 페이지당 4건으로 약 2만건의 데이터를 가진 테이블을 조회


1페이지를 조회.

img.png

약 19ms




400페이지를 조회.

img.png

약 19ms




500페이지를 조회.

img.png

약 18ms


거의 균등하게 조회되는 것을 확인할 수 있었다.
오차 범위도 3ms ~ 7ms정도 밖에 나지 않는 것 같다.


500페이지를 조회하는 기준 약 128ms에서 약 18ms까지 조회 시간을 줄일 수 있었고,
적은 작업량으로 속도를 약 85%개선 할 수 있었다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.