모르지 않다는 것은 아는것과 다르다.

Database

서브쿼리 (With QueryDSL)

채마스 2023. 10. 22. 15:08

개요

  • 실무에서 종종 서브쿼리가 포함된 쿼리들을 만나곤 한다.
  • 이번 글에서는 서브쿼리의 종류와 특징에 대해서 정리해 보고, QueryDSL로 구현하는 방법도 정리해보려고 한다.



서브쿼리의 종류

서브쿼리는 크게 아래 3가지 형태이다.

  • Nested Subquery (중첩 서브쿼리)
  • Inline View (인라인 뷰)
  • Scalar Subquery (스칼라 서브쿼리)



Nested Subquery (중첩 서브쿼리)

  • 중첩 서브쿼리는 주로 WHERE 절에서 사용된다.
  • 그렇기 때문에 주로 IN, EXISTS, ANY, ALL 등과 같은 연산자와 함께 사용되어 결과집합을 한정하는데 주로 사용된다.
  • 서브쿼리가 메인쿼리 칼럼을 참조하는 형태를 상관관계(Correlated) 서브쿼리라고 한다.

 

QueryDSL-JPA 에서 WHERE절 서브쿼리 사용하기

  • WHERE절에 서브쿼리를 사용하는 형태가 실무에서 가장 자주 마주하는 형태일 것이다.
select t.team_id,
       t.team_name,
       m.member_id as max_member_id
from team t
         left join member m on t.team_id = m.team_id
where (m.team_id, m.member_id) in (
    select sub.team_id, max(sub.member_id)
    from member sub
    group by sub.team_id
)
  • 이제 이 쿼리를 QueryDSL-JPA 로 구현해 보자.
private final JPAQueryFactory queryFactory;

public List<TeamDto> getTeamsWithWhereSubQuery() {
        QMember sub = new QMember("sub");

        return queryFactory
                .select(Projections.fields(TeamDto.class,
                        team.teamId,
                        team.teamName,
                        member.memberId.as("maxMemberId")
                ))
                .from(team)
                .leftJoin(member)
                .on(team.teamId.eq(member.teamId))
                .where(
                        Expressions.list(member.teamId, member.memberId).in(
                                JPAExpressions.select(sub.teamId, sub.memberId.max())
                                        .from(sub)
                                        .groupBy(sub.teamId)
                        )
                ).fetch();
}



Inline View (인라인 뷰)

  • 이 유형의 서브쿼리는 주로 FROM 절에서 사용된다.
  • 인라인 뷰는 결과 세트를 임시 테이블처럼 사용하여, 메인 쿼리에서 이를 참조하고 있다.
  • 따라서 서브쿼리지만, 그냥 테이블 간 조인하는 것과 같은 방식으로 동작한다.

 

QueryDSL-JPA에서 FROM절 서브쿼리 사용하기

  • 먼저, 아래와 같은 쿼리가 있다고 가정하자.
select t.team_id,
       t.team_name,
       sub.max_member_id
from team t
left join
     (
         select m.team_id, max(m.member_id) as max_member_id
         from member m
         group by m.team_id
     ) as sub
     on t.team_id = sub.team_id;
  • QueryDSL-JPA에서는 FROM절의 서브쿼리를 지원하지 않는다.
  • 그렇기 때문에 FROM절의 서브쿼리를 구현하기 위해서는 QueryDSL-SQL을 사용해야 한다.
  • QueryDSL-SQL사용하기 위해서 아래와 같이 JPASQLQueryFactory를 구현하자. (굳이 팩토리를 만들지 않고, 그때그때 JPASQLQuery 객체를 생성해도 무방하다.)
@Component
public class JPASQLQueryFactory {

    private final EntityManager entityManager;
    private final SQLTemplates sqlTemplates;

    public JPASQLQueryFactory(EntityManager entityManager, SQLTemplates sqlTemplates) {
        this.entityManager = entityManager;
        this.sqlTemplates = sqlTemplates;
    }

    public JPASQLQuery<?> createQuery() {
        return new JPASQLQuery<>(entityManager, sqlTemplates);
    }
}
  • 이제, JPASQLQueryFactory를 통해서 JPASQLQuery를 생성할 수 있다.
  • QueryDSL-JPA의 JPAQueryFactory와 같은 역할을 한다고 생각하면 된다.
  • 이제 QueryDSL-SQL를 사용해서 FROM절의 서브쿼리를 구현해 보자.
private final JPASQLQueryFactory jpasqlQueryFactory;

public List<TeamDto> getTeamsWithFromSubQuery() {
        StringPath sub = Expressions.stringPath("sub");

        return jpasqlQueryFactory.createQuery()
                .select(Projections.fields(TeamDto.class,
                        team.teamId,
                        team.teamName,
                        Expressions.numberPath(Long.class, sub, "max_member_id").as("maxMemberId")
                ))
                .from(team)
                .leftJoin(
                        queryFactory
                                .select(member.teamId, member.memberId.max().as("max_member_id"))
                                .from(member)
                                .groupBy(member.teamId)
                        , sub)
                .on(team.teamId.eq(Expressions.numberPath(Long.class, sub, "team_id")))
                .fetch();
}
  • 위 코드를 보면, alias를 표현하기 위해서 Expressions.numberPath()를 사용한 것을 확인할 수 있다.
  • Expressions.numberPath(Long.class, sub, "team_id") 이렇게 alias로 지정한 서브쿼리의 특정 칼럼을 표현한다.
    • 문자열자체를 사용하기 때문에 컴파일이 안되어 Type-Safe 하지 못하다는 단점이 있다.



Scalar Subquery (스칼라 서브쿼리)

  • 스칼라 서브쿼리는 하나의 칼럼값만을 반환한다. 또한, 하나의 행만 반환해야 하는 단일행 서브쿼리이다.
  • 그렇기 때문에 주로 SELECT 절에 사용되지만, WHERE, HAING, ORDER BY... 등등 칼럼이 올 수 있는 위치 (몇몇 예외상황을 제외한) 어디든 들어갈 수 있다.



QueryDSL-JPA에서 SELECT절 서브쿼리 사용하기

  • 아래와 같은 쿼리가 있다고 가정하자
SELECT
    orders.order_id,
    orders.order_date,
    orders.customer_name,
    (
        SELECT count(order_details.product_name)
        FROM order_details
        WHERE order_details.order_id = orders.order_id
    ) AS product_cnt
FROM
    orders;
  • QueryDSL-JPA로 위 쿼리를 구현하면 아래와 같다.
public List<OrderDto> exampleWithSelectSubQuery() {
    QOrder order = QOrder.order;
    QOrderDetail orderDetail = QOrderDetail.orderDetail;

    return queryFactory
            .select(Projections.fields(OrderDto.class,
                    order.orderId,
                    order.orderDate,
                    order.customerName,
                    ExpressionUtils.as(
                            JPAExpressions.select(orderDetail.productName.count())
                                    .from(orderDetail)
                                    .where(orderDetail.orderId.eq(order.orderId)),
                            "productCnt"
                    )

            ))
            .from(order)
            .fetch();
}
  • OrderDto 안에 Long 타입의 productCnt가 있다고 했을 때, 위와 같이 ExpressionUtils를 통해서 productCnt라는 alias를 줄 수 있다.
  • 참고로 QueryDSL-JPA에서는 LIMIT 기능은 제공하지 않는다.



'Database' 카테고리의 다른 글

PostgreSQL 옵티마이저  (0) 2023.11.14
인덱스 (With PostgreSQL)  (0) 2023.11.04
쿼리연습 (겹치는 날짜 검사하기)  (0) 2023.04.07
SQL  (0) 2022.05.30
프로시저  (0) 2022.02.27