목표
- 토론글 목록 조회 시 관련된 추가 데이터 조회
- 정렬 기준
- 1차로 유저가 선택한 기준대로 정렬하고, 해당 조건이 동일하다면 2차로 최신순(ID)으로 정렬
- 1차 : 인기순(upvote-downvote), 추천순(upvote), 최신순 중 하나
- 2차 : 최신순
- 기본 정렬은 인기순임
- 기존의 아키텍처를 유지하면서 쿼리 및 관련 설정만 변경하여 단기간에 성능 개선
성능 측정 (AS-IS)
테스트 데이터 삽입
- 의존성 문제 없이 테스트를 수행하려면 5개의 테이블이 필요 : user, problem, discussion, reply, discussion_vote
- 만약 discussion 백 만건의 테스트 데이터를 삽입해서 쿼리의 성능을 측정하려고 한다면, 다른 테이블 데이터의 개수는? 만 건, 십만 건 단위 테스트할 때 비율 맞춰야 할 듯
- user : 100,000 개
- problem : 1,000 개
- language : 5 개
- discussion : 1,000,000 개
- reply : 약 3,000,000 개 (discussion 당 3개)
- vote : 약 8,000,000 개 (discussion 당 8개)
- 데이터 생성 코드
기존 QueryDslRepository
@Repository
@RequiredArgsConstructor
public class DiscussionQueryRepositoryImpl implements DiscussionQueryRepository {
private final JPAQueryFactory jpaQueryFactory;
public Page<DiscussionQueryResult> findAllByProblemId(Long problemId, String sortBy, Long currentUserId, Pageable pageable) {
QUser user = discussion.user;
JPQLQuery<Long> upvoteCount = getVoteCount(VoteType.UP);
JPQLQuery<Long> downvoteCount = getVoteCount(VoteType.DOWN);
NumberOperation<Long> bestScore = Expressions.numberOperation(
Long.class,
Ops.SUB,
upvoteCount,
downvoteCount
);
JPQLQuery<Long> replyCount = JPAExpressions
.select(reply.count())
.from(reply)
.where(reply.discussion.eq(discussion));
Expression<VoteType> userVoteType = getUserVoteTypeExpression(currentUserId);
List<DiscussionQueryResult> results = jpaQueryFactory
.select(new QDiscussionQueryResult(
discussion.id,
Projections.constructor(SimpleUserInfoResponse.class,
user.id,
user.nickname,
user.tier,
user.profileImageUrl
),
discussion.problem.id,
discussion.content,
discussion.createdAt,
upvoteCount,
downvoteCount,
replyCount,
userVoteType
))
.from(discussion)
.where(discussion.problem.id.eq(problemId))
.orderBy(getOrderSpecifier(sortBy, bestScore, upvoteCount))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
JPAQuery<Long> countQuery = jpaQueryFactory
.select(discussion.count())
.from(discussion)
.where(discussion.problem.id.eq(problemId));
return PageableExecutionUtils.getPage(results, pageable, countQuery::fetchOne);
}
private JPQLQuery<Long> getVoteCount(VoteType voteType) {
return JPAExpressions
.select(discussionVote.count())
.from(discussionVote)
.where(discussionVote.discussion.eq(discussion)
.and(discussionVote.voteType.eq(voteType)));
}
private OrderSpecifier<?>[] getOrderSpecifier(String sort, NumberExpression<Long> bestScore, Expression<Long> upvoteCount) {
OrderSpecifier<?> primarySort;
switch (sort.toLowerCase()) {
case "best":
primarySort = new OrderSpecifier<>(Order.DESC, bestScore);
break;
case "upvote":
primarySort = new OrderSpecifier<>(Order.DESC, upvoteCount);
break;
default:
return new OrderSpecifier<?>[]{ new OrderSpecifier<>(Order.DESC, discussion.id) };
}
OrderSpecifier<?> secondarySort = new OrderSpecifier<>(Order.DESC, discussion.id);
return new OrderSpecifier<?>[] { primarySort, secondarySort };
}
private Expression<VoteType> getUserVoteTypeExpression(Long currentUserId) {
if (currentUserId == null) {
return Expressions.nullExpression(VoteType.class);
}
QDiscussionVote discussionVoteForUser = new QDiscussionVote("discussionVoteForUser");
return JPAExpressions
.select(discussionVoteForUser.voteType)
.from(discussionVoteForUser)
.where(discussionVoteForUser.discussion.eq(discussion)
.and(discussionVoteForUser.voter.id.eq(currentUserId)));
}
}
SQL 로그 및 실행 계획
Hibernate:
select
d1_0.id,
d1_0.user_id,
u1_0.nickname,
u1_0.tier,
u1_0.profile_image_url,
d1_0.problem_id,
d1_0.content,
d1_0.created_at,
(select
count(dv1_0.id)
from
discussion_vote dv1_0
where
dv1_0.discussion_id=d1_0.id
and dv1_0.vote_type=?),
(select
count(dv2_0.id)
from
discussion_vote dv2_0
where
dv2_0.discussion_id=d1_0.id
and dv2_0.vote_type=?),
(select
count(r1_0.id)
from
reply r1_0
where
r1_0.discussion_id=d1_0.id),
(select
dv3_0.vote_type
from
discussion_vote dv3_0
where
dv3_0.discussion_id=d1_0.id
and dv3_0.voter_id=?)
from
discussion d1_0
join
users u1_0
on u1_0.id=d1_0.user_id
where
d1_0.problem_id=?
order by
((select
count(dv4_0.id)
from
discussion_vote dv4_0
where
dv4_0.discussion_id=d1_0.id
and dv4_0.vote_type=?)-(select
count(dv5_0.id)
from
discussion_vote dv5_0
where
dv5_0.discussion_id=d1_0.id
and dv5_0.vote_type=?)) desc,
d1_0.id desc
limit
?, ?
Hibernate:
select
count(d1_0.id)
from
discussion d1_0
where
d1_0.problem_id=?

원인 분석 및 코드 최적화
1차 개선 : JOIN + GROUP BY
실행 계획 분석
- 서브쿼리 등을 사용할 때 인덱스를 사용하고 있으므로
type 칼럼에는 ALL(Full Scan)이 없음
- ref, eq_ref 등 사용 중
- 인덱싱 작업은 따로 하지 않아도 될 듯
- problem_id=510인 discussion 994개 조회
- 각 discussion마다 서브 쿼리 실행
- 데이터가 많아질수록 실행되는 서브 쿼리는 비례해서 증가
- 서브쿼리를 join + group by로 대체해보자