ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • QueryDSL으로 페이징 구현하기
    JPA 2023. 9. 18. 00:01
    728x90

    개요

    페이지네이션에 대해 알아보다 보면 상황에 따라 offset 기반과 cursor 기반의 방식으로 구현할 수 있습니다.

    이때 offset 방식을 활용하여 QueryDSL으로 쿼리를 구현해보고자 합니다.

     

    Offset과 Limit 그리고 성능

    SELECT *
    FROM TABLE_NAME
    LIMIT 10
    OFFSET 5000;

    offset절은 만약 N이 들어온다면 N번째행부터 읽겠다는 의미입니다.

    예를 들어 5000이라면 5000번째 행부터 읽겠다는 의미입니다.

     

    Limit 절은 쿼리 결과에서 지정된 순서에 위치한 레코드만 가져오고자 할 때 사용됩니다.

    Limit에는 클라이언트가 요청한 크기 N이 들어가게 되고 offset을 통해 가져온 전체 데이터에서 상위 N개를 가져옵니다.

     

    위 둘을 조합하게 되면 5000번째부터 10번째 레코드만 읽겠다는 의미입니다.

     

    이때 offset이 점점 커지게 되면 조회비용이 증가하게 되어 주의해야 합니다.

    N개 결과의 row만 DB에서 조회하는것이 아니라 모든 row를 디스크로부터 읽어오기 때문에 데이터베이스에 많은 부하를 주게 됩니다.

    예를 들어 5000~5010번째 데이터를 가져오는 것이 아니라 1~5010번째 데이터를 디스크에서 읽어옵니다.

     

    Spring에서 실행되는 쿼리

    SELECT *
    FROM TABLE
    OFFSET 5000 ROWS
    FETCH FIRST 10 row only;

    실제 날라가는 쿼리는 위와 같아서 찾아보니 limit offset 방식과 실행계획은 동일하게 보입니다.

     

    구현

    override fun selectDeliveryInfos(condition: Condition, pageable: Pageable, pagingQuery: PagingQuery): Page<DeliveryJpaEntity> {
        val delivery: QDeliveryJpaEntity = QDeliveryJpaEntity.deliveryJpaEntity
        val content = jpaQueryFactory.select(delivery)
          .from(delivery)
          .where(betweenDateConditionType(condition))
          .offset(pageable.offset)
          .limit(pageable.pageSize.toLong())
          .orderBy(setOrderByQuery(pagingQuery.sort))
          .fetch()
    
        val count: Long = jpaQueryFactory
          .select(delivery.count())
          .from(delivery)
          .where(betweenDateConditionType(condition))
          .fetchOne() ?: 0
    
        return PageImpl(content, pageable, count)
      }
      
    private fun setOrderByQuery(sortCondition: String): OrderSpecifier<*> {
        //TODO: sortCondition에 따라 분기문 추가 현재는 createdAt만 오름차순(날짜 빠른순)으로 지원
        val delivery: QDeliveryJpaEntity = QDeliveryJpaEntity.deliveryJpaEntity
        return OrderSpecifier(Order.ASC, delivery.createdAt)
      }

    offset과 limiet을 주고 orderBy의 경우에는 OrderSpecifier를 활용하여 지정하였습니다.

    ORderSpecifier는 동적정렬에서 활용됩니다.

    count 쿼리는 별도로 작성하였고 추후 데이터가 쌓여가면서 느려질 수 있기 때문에 비즈니스 상황에 따라 no offset 방법이나 트리거를 활용하여 count 수를 따로 관리해 주는 방안으로 빠르게 개선할 수 있을 것 같습니다.

     

    혹은 offset대신 다음과 같은 방식도 고려해 볼 수 있습니다.

    SELECT ...
      FROM ...
     WHERE ...
       AND id < ?last_seen_id
     ORDER BY id DESC
     FETCH FIRST 10 ROWS ONLY

    이러한 방법을 seek method 또는 keyset pagination이라고 부릅니다.

     

     

    참고자료

    https://thalals.tistory.com/298

    https://jojoldu.tistory.com/528

    https://brownbears.tistory.com/582

    https://binux.tistory.com/148

     

    Pagination(Paging), offset을 사용하지 맙시다

    들어가기 전에 이 글은 use-the-index-luke 사이트의 no-offset 글을 번역한 글입니다. 원 글이 좀 딱딱한 것 같아서 이해하기 쉽게 번역해보았습니다. 참고부탁드립니다. 왜 offset을 사용하면 안돼? SQL로

    binux.tistory.com

     

    댓글

Designed by Tistory.