개요
- 실무에서 종종 서브쿼리가 포함된 쿼리들을 만나곤 한다.
- 이번 글에서는 서브쿼리의 종류와 특징에 대해서 정리해 보고, 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 |