[Troubleshooting] QueryDsl 서브쿼리 limit 1 적용 안 됨

2025. 9. 28. 15:17·SPRING/TIL

문제 상황

문제 목록을 조회할 때 각 문제의 Top 유저를 함께 가져와야 했다. QueryDSL로 구현하려면, 기본적으로 문제를 조회하는 메인 쿼리와, 각 문제에 대한 Top 유저를 구하는 서브쿼리가 필요했다.

 

Top 유저를 조회하는 기준 컬럼을 내림차순 정렬한 뒤 상위 1명만 선택한다는 아이디어로 설계했다. 즉, 정렬 후 limit 1을 적용해 한 행만 가져오는 방식을 떠올렸다.

 

실행을 돌려보니, Scalar subquery contains more than one row; SQL statement: 에러가 떴다.

 

문제 코드

public List<HomeProblemQuery> findPopularProblem(int limit) {
        QProblem problem = QProblem.problem;
        QProblemImage problemImage = QProblemImage.problemImage;
        QUserRating topRating = new QUserRating("topRating");
        QUser topUser = new QUser("topUser");

        return queryFactory
                .select(Projections.constructor(HomeProblemQuery.class,
                        problem.id,
                        problem.title,
                        problem.submissionCount,
                        problem.submitterCount,
                        // ✅SubQuery
                        JPAExpressions.select(topUser.identifier)
                                .from(topRating)
                                .join(topRating.user, topUser)
                                .where(topRating.problem.eq(problem))
                                .orderBy(topRating.rating.desc())
                                .limit(1),	// ✅상위 1명
                        problem.description,
                        problemImage.imageKey
                ))
                .from(problem)
                .leftJoin(problem.iconKey, problemImage)
                .orderBy(problem.submissionCount.desc())
                .limit(limit)
                .fetch();
    }

 

실제 쿼리를 살펴보면 아래와 같다. 

select
    p1_0.problem_id,
    p1_0.title,
    p1_0.submission_count,
    p1_0.submitter_count,
    (select	// ✅SubQuery 시작
        u1_0.identifier 
    from
        user_rating ur1_0 
    join
        users u1_0 
            on u1_0.user_id=ur1_0.user_id 
    where
        ur1_0.problem_id=p1_0.problem_id 
    order by
        ur1_0.rating desc),	// ❓limit 1 누락
    p1_0.description,
    ik1_0.image_key 
from
    problem p1_0 
left join
    problem_image ik1_0 
        on ik1_0.problem_image_id=p1_0.icon_image_id 
order by
    p1_0.submission_count desc 
fetch
    first ? rows only
2025-09-28T12:19:59.765+09:00  WARN 22604 --- [nio-8080-exec-6] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 90053, SQLState: 90053
2025-09-28T12:19:59.765+09:00 ERROR 22604 --- [nio-8080-exec-6] o.h.engine.jdbc.spi.SqlExceptionHelper   : Scalar subquery contains more than one row; SQL statement:

서브쿼리에서 limit 절이 누락된 것을 확인할 수 있다.

 

원인 분석

JPQL 자체가 서브쿼리에서 LIMIT/OFFSET을 지원하지 않는다. QueryDSL JPA는 내부적으로 JPQL을 생성하기 때문에, 서브쿼리의 `.limit()`/`.offset()`이 무시된다. 

 

해결 방법

서브쿼리에 min()을 적용해 우회적으로 해결했다. min은 대상 집합에서 최소값 하나로 축약하므로, 동률이 있어도 결과가 자연스럽게 단일 값이 된다. 정렬 후 limit 1을 쓰는 것과 같은 효과를 볼 수 있다.

 

최종 코드

public List<HomeProblemQuery> findPopularProblems(int limit) {
        QProblem problem = QProblem.problem;
        QProblemImage problemImage = QProblemImage.problemImage;
        JPQLQuery<String> topIdentifier = getTopIdentifier(problem);	// ✅TopUser 가져오기

        return queryFactory
                .select(Projections.constructor(HomeProblemQuery.class,
                        problem.id,
                        problem.title,
                        problem.submissionCount,
                        problem.submitterCount,
                        topIdentifier,	// ✅활용
                        problem.description,
                        problemImage.imageKey
                ))
                .from(problem)
                .leftJoin(problem.iconKey, problemImage)
                .orderBy(problem.submissionCount.desc())   
                .limit(limit)
                .fetch();
    }

    private JPQLQuery<String> getTopIdentifier(QProblem problem) {
        QUserRating ur = QUserRating.userRating;

        QUserRating urForMaxRating = new QUserRating("urForMaxRating");
        QUserRating urForMaxWin = new QUserRating("urForMaxWin");
        QUserRating urForEarliest = new QUserRating("urForEarliest");

        // 최고 레이팅
        JPQLQuery<Integer> maxRating =
                JPAExpressions.select(urForMaxRating.rating.max())
                        .from(urForMaxRating)
                        .where(urForMaxRating.problem.eq(problem));

        // 최고 승수
        JPQLQuery<Integer> maxWinAtMaxRating =
                JPAExpressions.select(urForMaxWin.win.max())
                        .from(urForMaxWin)
                        .where(
                                urForMaxWin.problem.eq(problem),
                                urForMaxWin.rating.eq(maxRating)
                        );

        // 가장 이른 시각
        JPQLQuery<LocalDateTime> earliestAtTie =
                JPAExpressions.select(urForEarliest.updatedAt.min())
                        .from(urForEarliest)
                        .where(
                                urForEarliest.problem.eq(problem),
                                urForEarliest.rating.eq(maxRating),
                                urForEarliest.win.eq(maxWinAtMaxRating)
                        );

        return JPAExpressions.select(ur.user.identifier.min())	// ✅limit 1과 같은 효과
                .from(ur)
                .where(
                        ur.problem.eq(problem),
                        ur.rating.eq(maxRating),
                        ur.win.eq(maxWinAtMaxRating),
                        ur.updatedAt.eq(earliestAtTie)
                );
    }

Top User의 기준 우선순위는

  1. rating이 가장 높을 것
  2. rating이 같을 경우, win이 가장 높을 것
  3. rating과 win이 같을 경우, updatedAt이 가장 오래되었을 것

이렇게 하면 1명으로 좁혀진다! 그래야만 한다

그리고 마지막으로 min()을 통해서 1row를 명시적으로 반환한다. 

 

Top user를 찾는 로직은 여러 번 재사용될 예정이라 메서드로 분리했다.

 

결과 확인

select
    p1_0.problem_id,
    p1_0.title,
    p1_0.submission_count,
    p1_0.submitter_count,
    (select
        min(u1_0.identifier) // ✅min 적용 확인
    from
        user_rating ur1_0 
    join
        users u1_0 
            on u1_0.user_id=ur1_0.user_id 
    where
        ur1_0.problem_id=p1_0.problem_id 
        and ur1_0.rating=(
            select
                max(ur2_0.rating) 
            from
                user_rating ur2_0 
            where
                ur2_0.problem_id=p1_0.problem_id
        ) 
        and ur1_0.win=(
            select
                max(ur3_0.win) 
            from
                user_rating ur3_0 
            where
                ur3_0.problem_id=p1_0.problem_id 
                and ur3_0.rating=(
                    select
                        max(ur4_0.rating) 
                    from
                        user_rating ur4_0 
                    where
                        ur4_0.problem_id=p1_0.problem_id
                )
        ) 
        and ur1_0.updated_at=(
            select
                min(ur5_0.updated_at) 
            from
                user_rating ur5_0 
            where
                ur5_0.problem_id=p1_0.problem_id 
                and ur5_0.rating=(
                    select
                        max(ur6_0.rating) 
                    from
                        user_rating ur6_0 
                    where
                        ur6_0.problem_id=p1_0.problem_id
                ) 
                and ur5_0.win=(
                    select
                        max(ur7_0.win) 
                    from
                        user_rating ur7_0 
                    where
                        ur7_0.problem_id=p1_0.problem_id 
                        and ur7_0.rating=(
                            select
                                max(ur8_0.rating) 
                            from
                                user_rating ur8_0 
                            where
                                ur8_0.problem_id=p1_0.problem_id
                        )
                )
        )),
    p1_0.description,
    ik1_0.image_key 
from
    problem p1_0 
left join
    problem_image ik1_0 
        on ik1_0.problem_image_id=p1_0.icon_image_id 
order by
    p1_0.submission_count desc 
fetch
    first ? rows only

 

회고

이 에러를 해결하기 위해 여러 방법을 많이 시도해봤는데 이렇게 자체 오류는 근본적으로 해결하기 어려운 것 같다. 그래도 우회적으로 잘 해결한 것 같아서 만족스럽다. 그리고 메서드를 분리해 코드 가독성도 더 좋아졌다.

 

'SPRING > TIL' 카테고리의 다른 글

[Study] SNS FIFO : MessageGroupId 지정  (0) 2025.10.09
[Study] @Modifying: 벌크 연산과 영속성 컨텍스트 관리  (5) 2025.09.29
'SPRING/TIL' 카테고리의 다른 글
  • [Study] SNS FIFO : MessageGroupId 지정
  • [Study] @Modifying: 벌크 연산과 영속성 컨텍스트 관리
seo-cherry
seo-cherry
devyeonee911 님의 블로그 입니다.
  • seo-cherry
    Emergency
    seo-cherry
  • 전체
    오늘
    어제
    • 분류 전체보기 (11)
      • SPRING (7)
        • TIL (3)
        • HOW-TO (4)
      • CI-CD (1)
      • REVIEW (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
seo-cherry
[Troubleshooting] QueryDsl 서브쿼리 limit 1 적용 안 됨
상단으로

티스토리툴바