새소식

DB

[DB] 쿼리 개선 일지 - MySQL 커버링 인덱스 (Using Index)

  • -

 

App Store에 심사를 올리게 되면서, "유저가 불쾌할 수 있는 게시글에 대해서 즉각적인 조치가 가능하게 하라" 라는 피드백을 받았습니다.

아무래도 소셜네트워크 서비스이다 보니까, 조금 민감한 부분이라고 생각이 됩니다.

 

그래서 유저가 게시글 신고 이후에 즉각적인 조치를 하기 위해서 다음과 같은 기능 요구가 생겼습니다.

 

기능: 유저가 신고처리를 했던 게시글은, 유저에게 보이지 않는다.
성능: 신고 처리 테이블을 스캔하는 비용을 최소화

 

 

전체 코드는 접어두었습니다.

더보기

 

@Override public List<PostFetchDto> findNextPageByCommmand(PostFetchCommand command) { Map<Long, PostFetchDto> resultMap = queryFactory.from(post) .join(post.user) .join(post.shorts) .where(filterByCursor(command)) // 커서 기반 페이징 .where(getFilterExpression(command)) // 내가 업로드한 게시글 .where(post.id.notIn( JPAExpressions .select(reportPost.post.id) .from(reportPost) .where(reportPost.reporter.id.eq(command.getUserId()) ))) // 본인이 신고한 게시글 제거 .where(post.shorts.shortsStateEnum.eq(ShortsStateEnum.OK)) // shorts의 인코딩이 완료된 게시글 .limit(command.getPageSize()) .orderBy(orderByPostSortOption(command.getSortOption())) // 커서 정렬 조건 .orderBy(post.id.desc()) .transform(groupBy(post.id) .as(new QPostFetchDto(post.id, new QPostFetchDto_PostUserInformation(post.user.id, post.user.nickname, post.user.profileImgUrl), new QPostFetchDto_PostShortsInformation(post.shorts.id, post.shorts.shortsStateEnum, post.shorts.shortsUrl, post.shorts.thumbnailUrl), post.view, post.postCategory, post.isBlind ))); return resultMap.keySet().stream() .map(resultMap::get) .collect(Collectors.toList()); }

 

 

성능 이슈 때문에 LEFT JOIN 을 사용해서 처리를 할까 하다가, 실제로 이런 경우에는 크게 성능 차이가 나지 않는다는 글을 여러개 접했습니다.

 

그래서 일단 in 절에 서브쿼리 사용하는 방식으로 구현을 했습니다.

.where(post.id.notIn( JPAExpressions .select(reportPost.post.id) .from(reportPost) .where(reportPost.reporter.id.eq(command.getUserId()) ))) // 본인이 신고한 게시글 제거

 

먼저 신고 테이블의 원래 상황을 보자면..

 

신고 테이블은 다음과 같이 (신고번호, 포스트id, 신고자id)로 구성되어 있습니다.

그리고 한 명의 유저가 동일한 게시글을 신고할 수 없도록 (포스트 id, 신고자 id) 로 multipe unique key 제약조건을 걸어놓은 상태입니다.

 

유니크 키를 걸게 되면, 해당 제약조건의 빠른 검증을 위해 index가 함께 생성됩니다. 현재 'idx01_unique_report_post' 이라는 이름으로 인덱스가 걸려있습니다.

 

 

 

 

처음에는 일단 유저가 신고한 게시글인지 빠르게 탐색하기 위해서 `user_id` 에 인덱스를 걸었었습니다.

그런데 실제로 실행계획을 돌려보니..

제가 이번에 추가한 `idx02_report_post` 를 후보로만 올려놓고, 실제로는 기존에 걸려있던 `idx01_unique_report_post` 키를 활용하더군요.

 

어! 왜 이렇지!!! 라고 샤워를 하며 30분동안 생각에 잠기게 됩니다.

 

어! 왜 이렇지!!! 라고 샤워를 하며 곰곰히 생각을 했는데, 생각해보니 기존 unique index table에 `(user_id, post_id)`가 있다는 것이 떠올라서, 샤워를 끝내고 실제로 호다닥 확인해보니 `Extra: Using index` 가 적혀있더군요 ㅎㅎ

 

왜 Extra 컬럼을 볼 생각을 안했을까

 

Extra의 Using index는 커버링 인덱스를 통해 결과를 가져왔다는 것을 의미합니다.

 

인덱스에 모든 값이 올라가있어서, 실제로는 disk I/O가 일어나지 않는 커버링 인덱스로 실행된 것 입니다.

 

 

 

실제 내부의 인덱스 테이블은 다음과 같이 생겼을 것 입니다.

MySQL 같은 경우에는 인덱스 테이블이 PK 값을 가지고 있어서, 다음과 같이 표현이 되는데

제가 위에서 실행한 쿼리에서 필요한 (post_id, user_id)는 인덱스 테이블 안에 모두 포함되어 있음을 알 수 있습니다.

 

그렇다면 굳이 느린 Disk I/O 를 하면서 테이블을 직접 읽을 필요가 없겠죠?

 

 

 

그런데 사실 다시 생각해보면, 만약 커버링 인덱스가 아니었더라도 `(user_id, post_id)` 복합키에서 user_id가 선행 컬럼이라서, 단독 index를 거는 것과 차이가 없었을 겁니다. ㅎㅎㅎ

 

왜냐. 인덱스 테이블 순서를 보시면, 선행 컬럼인 post_id 이후에 user_id가 정렬되는 방식입니다.

 

그렇다면 post_id에 따로 인덱스를 걸더라도, 동일한 데이터 순서로 탐색을 하게 됩니다.

 

감사합니다.

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.