Skip to content

카드 표시값 파생 조회의 전량 스캔 제거 - #974

Open
m-a-king wants to merge 1 commit into
devfrom
perf/857-latest-snapshot-query
Open

카드 표시값 파생 조회의 전량 스캔 제거#974
m-a-king wants to merge 1 commit into
devfrom
perf/857-latest-snapshot-query

Conversation

@m-a-king

Copy link
Copy Markdown
Collaborator

Situation

  • 부하테스트(dev 부하테스트: 전용 DB·부하기 구성과 앱 한계 탐색 #911)에서 위시 목록 조회가 이상하게 느렸다. CPU 는 2% 인데 요청당 400-860ms 를 썼다.
  • 자원이 남는데 느리다는 건 뭔가를 기다린다는 뜻이다. 추적해보니 목록 20건을 보여주려고 테이블 10만 행을 읽고 있었다.
  • 이것이 전체 처리량의 천장이었다. 요청당 0.5초씩 걸리니 Tomcat 스레드 25개로는 초당 47건이 한계였고, 초과분은 큐에 쌓여 클라이언트가 p95 18.6초를 기다렸다.

Task

Action

원인 규명

EXPLAIN ANALYZE 로 실행계획을 확인했다.

Materialize with deduplication ... rows=19      loops=1        <- 서브쿼리는 19행 (76ms)
Covering index scan on is1_0   ... rows=101,183 loops=1        <- 바깥은 10만 행
Index lookup on <materialized_subquery> ... loops=101,184      <- 10만 번 대조
전체 483ms

안쪽이 19행으로 좁은데 바깥 10만 행을 전부 훑으며 대조한다. MySQL 이 서브쿼리 결과를 임시 테이블로 만든 뒤, 바깥 테이블을 스캔하며 매 행을 그 테이블과 맞춰보는 계획을 골랐다. 방향이 반대다.

같은 요청의 다른 id in (상수) 쿼리는 examined=20, 0.4ms 로 정상이었다. IN 문법이 아니라 서브쿼리를 끼운 형태가 원인이다.

수정

한 문장을 두 문장으로 나눠, id 목록을 앱에서 받아 넘긴다.

이전 이후
형태 where s.id in (select max(s2.id) ...) 한 문장 id 조회 + findAllById 두 문장
1단계 계획 (없음) idx_item_snapshots_item_id range scan
2단계 계획 인덱스 전량 스캔 PK lookup
요청당 행 읽기 101,281 117
쿼리 실행시간 483ms 0.24ms + PK lookup

왕복이 하나 늘지만 1단계가 0.24ms 라 그 비용을 크게 밑돈다.

반환 타입이 엔티티에서 id 목록으로 바뀌어 메서드명도 findLatestMachineReadyIdsByItemIds 로 옮겼다. 조합은 Impl 이 맡아 호출부(ItemDisplayService) 시그니처는 그대로다.

검토 후 채택 안 한 안

왜 안 했나
커버링 인덱스 추가 인덱스가 이미 있고 서브쿼리는 정상 동작했다. 문제는 인덱스 부재가 아니라 옵티마이저의 조인 방향이라 인덱스를 더해도 같은 계획을 고를 수 있다
표시값을 비정규화 컬럼으로 파생 자체를 없애는 큰 변경이라 이번 스코프를 넘는다. 파생 조회가 정상 속도로 도는 지금은 필요가 없다

Result

  • 전체 처리량 47 -> 176 req/s, p95 18.6초 -> 3.7초, 중앙값 13.8초 -> 256ms (부하테스트 실측, 같은 조건 3회 러닝)
  • 고친 뒤 병목이 CPU 로 이동했다. 이제 앱 CPU 98% 가 천장이고, 그 구간에서 HikariCP 5 도 적정값임을 반대 방향 실험으로 확인했다(풀 10·15 둘 다 더 나빴다).
  • 같은 안티패턴(in (select max ...))이 코드베이스 다른 곳에 있는지 전수 검사했고, 없었다.
  • 측정 상세는 dev 부하테스트: 전용 DB·부하기 구성과 앱 한계 탐색 #911결과 리포트에 있다.

연관 이슈

- `where s.id in (select max(s2.id) ...)` 한 문장이 item_snapshots 를 전량 스캔하고 있었다. 부하테스트(#911) 실측: 위시 목록 1회(size=20)가 행 10만을 읽고 483ms 를 썼다
- EXPLAIN ANALYZE 로 계획을 확인했다. 서브쿼리는 19행/76ms 로 정확한데, MySQL 이 그 결과를 임시 테이블로 materialize 한 뒤 바깥 테이블 10만 행을 훑으며 매 행을 대조한다(loops=101184). 안쪽이 좁은데 바깥을 다 보는 형태라 데이터가 늘수록 선형으로 나빠진다
- id 목록을 앱으로 받아 findAllById 로 넘긴다. 1단계는 idx_item_snapshots_item_id range scan(0.24ms), 2단계는 PK lookup 이라 양쪽 모두 인덱스를 탄다. 왕복이 하나 늘지만 1단계 비용이 그것을 크게 밑돈다
- 같은 요청의 다른 `id in (상수)` 쿼리는 examined=20·0.4ms 로 정상이었다. IN 문법이 아니라 서브쿼리를 끼운 형태가 원인이다
- 반환 타입이 엔티티에서 id 목록으로 바뀌어 메서드명도 findLatestMachineReadyIdsByItemIds 로 옮겼다. 조합은 Impl 이 맡아 호출부(ItemDisplayService) 시그니처는 그대로다
@m-a-king m-a-king added the perf 성능 개선 (측정 가능, 외부 동작 불변) label Aug 23, 2026
@m-a-king m-a-king self-assigned this Aug 23, 2026
@github-actions

Copy link
Copy Markdown

Discord 스레드 연동용 메타데이터입니다. discord-pr-bot 워크플로가 자동 생성하며, 수정·삭제하면 PR 과 Discord 알림 연동이 끊깁니다.

@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6bd58073-9fd6-4574-b5a8-c9f16562fdcc


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

perf 성능 개선 (측정 가능, 외부 동작 불변)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant