perf(S15P11A705-404): 지도 bbox 쿼리를 custom plan에 묶는다 - #208
Open
minyongP wants to merge 2 commits into
Open
Conversation
pgjdbc가 같은 문장을 5회 실행한 뒤 서버 프리페어로 갈아타면 Postgres가 generic plan을 고른다. 그 계획은 bbox가 실제로 얼마나 걸러내는지 모르므로 조인 순서를 뒤집는다 — 회원의 record에서 출발하는 대신 bbox 안 place를 전부 훑고 건마다 record를 찔러 대부분 0행을 얻는다. record 1,012만 볼륨에서 마커 조회 2.8ms→45~62ms(16~22배), 키워드 조회 5.4ms→32~34ms(6배). 리터럴을 넣어 EXPLAIN을 뜨면 항상 custom plan이라 이 전환은 검증에서 드러나지 않는다. SQL을 두 갈래로 나눈 것(S15P11A705-388)으로도 막지 못한다 — 나누면 플래너가 bbox 조건의 존재를 알게 되지만 값은 여전히 파라미터다. bbox가 넷 다 주어진 경로에만 SET LOCAL plan_cache_mode = force_custom_plan을 건다. CTE로 감싸는 두 방식은 실측에서 듣지 않았다(bbox 포함 33ms, bbox 제외 19ms). 전역 설정은 다른 쿼리의 계획 재사용 이득까지 버리므로 범위 판단(S15P11A705-405)까지 보류한다. 테스트는 행 수에 의존하지 않는 것만 자동화했다 — 트랜잭션의 plan_cache_mode와 pg_prepared_statements.generic_plans 증분. 계획 모양은 Testcontainers에서 재현되지 않아 대량 볼륨 실측으로 갈음하고 BI-44에 남겼다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
요약
지도 bbox 조회 두 개(
GET /v1/records/map·GET /v1/records/map/keywords)가 실사용에서만 느린 실행 계획을 타고 있었습니다. pgjdbc가 같은 문장을 5회 실행한 뒤 서버 프리페어로 갈아타면 Postgres가 generic plan을 고르고, 그 계획은 bbox가 실제로 얼마나 걸러내는지 모르므로 조인 순서를 뒤집습니다. bbox가 넷 다 주어진 경로에만SET LOCAL plan_cache_mode = force_custom_plan을 걸어 막습니다.Jira (필수)
변경 사항
QueryPlanPin(신규) —SET LOCAL plan_cache_mode = force_custom_plan한 문장을 쥔 컴포넌트. 왜 이 방식인지, 무엇이 듣지 않았는지가 클래스 주석에 있습니다.RecordService.map·RecordService.mapKeywords— bbox가 넷 다 주어졌을 때만 위 핀을 겁니다. bbox 없는 경로는 다른 SQL을 쓰므로 대상이 아닙니다.MapBboxPlanCacheTests(신규, 5개) — 트랜잭션의plan_cache_mode와pg_prepared_statements.generic_plans증분을 봅니다.docs/backend/implements/BI-44· 작업 로그 — 실측 원문과 탈락한 후보의 수치.왜 이게 검증에서 안 걸렸나
리터럴을 넣어
EXPLAIN을 뜨면 항상 custom plan입니다. 드러내려면PREPARE후 6회 이상 실행한 계획을 봐야 합니다. record 1,012만 볼륨 실측:generic plan은 회원의 record(697건)에서 출발하는 대신
ix_place_lat_lng로 bbox 안 place 24,503건을 훑고 건마다 record를 찔러 대부분 0행을 얻습니다.SQL을 두 갈래로 나눈 것(S15P11A705-388)으로는 막지 못합니다. 나누면 플래너가 bbox 조건의 존재를 알게 되지만, 값은 여전히 파라미터이고 generic plan은 그 값을 못 읽습니다. BI-42의 성능 근거도 리터럴 측정이었습니다.
테스트 / 검증
./gradlew clean check --no-daemon— BUILD SUCCESSFUL in 3m 6sMapBboxPlanCacheTests5개 통과docs/backend/decisions/에 BD 추가 — 새 BD 없음. 되돌리기 쉬운 설정 한 줄이고, 범위 확대 판단은 S15P11A705-405가 별도로 합니다RED → GREEN
수정 전 3개 실패(
auto≠force_custom_plan, generic plan 증분 5), 수정 후 5개 통과. 핀을 일시 제거해 되돌리면 다시 3개가 깨지는 것(증분 2)까지 확인했습니다.대량 볼륨 실사용 실측 — 같은 엔드포인트를 순차로 20회 호출, 10~20회차 중앙값:
수정 전에는 계단이 뚜렷했습니다(마커 9회차부터 27
45ms→6587ms, 키워드 10회차부터 2237ms→60104ms). 수정 후 그 계단이 사라집니다.리뷰 포인트
QueryPlanPin을 서비스에서 부르는 배치가 맞는지. 보호해야 할 SQL은 리포지터리에 있는데 핀은 서비스에서 겁니다. 마커는 JPQL(Spring Data 인터페이스)이라 문장을 끼울 자리가 없고, 키워드만 리포지터리에 넣으면 두 자리가 갈라져서 서비스로 모았습니다. 커스텀 프래그먼트를 만들어 SQL 옆에 두는 편이 낫다고 보시면 그렇게 바꾸겠습니다.SET LOCAL은 트랜잭션 밖에서 경고만 남기고 아무 일도 하지 않습니다. 즉 호출부의@Transactional(readOnly = true)가 사라지면 이 보호가 조용히 없어집니다. 테스트가 그 상태를 잡지만, 더 튼튼한 방법이 있으면 알려주세요.FeedChannelPlanTests가 겪은 플레이크를 되풀이합니다. 그래서 행 수와 무관한 두 가지(설정·계수 증분)만 자동화하고 계획 모양은 BI-44의 실측으로 갈음했습니다.MATERIALIZEDCTE는 33.17ms(개선 0), bbox 제외 CTE는 18.94ms(절반만 회복, place 24,503건을 해시로 쌓습니다).미결 / 후속
LIKE·IN을 쓰는 다른 쿼리에도 있는지. 피드·검색은 AI 파트 소유라 그 티켓은 전달 문구까지만 만듭니다.TOP_KEYWORDS_FOR_OWNER_SQL주석)은 이 PR에서 건드리지 않았습니다. 함께 걸면 감사 결과를 미리 단정하는 것이 됩니다.prepareThreshold=0등)은 감사 결과를 보고 판단합니다.compose.bench.yaml미적용). 1,012만 행이 전부 페이지 캐시에 들어간 하한값이라 절대 수치를 운영 예측에 쓰면 안 됩니다 — 이 PR이 근거로 삼는 것은 배율과 계획 모양입니다.이 PR과 무관한 관측
docs/backend/implements/의 번호가 두 쌍 겹쳐 있습니다 — BI-43(#203과 #206이 오늘 30분 차로 같은 번호를 가져갔습니다)과 BI-38(massive-scale-plan-observation·unlink-before-withdrawal). 기록 구역이라 옮기지 않고 관측만 남기며, 제 리포트는 BI-44를 썼습니다. BI-43 한쪽은 검색 담당자(@colosair) 작업이라 합의가 필요합니다. 오늘 BD-46에서도 같은 일이 있었으니(#201) 번호 발급 방식 자체를 볼 필요가 있어 보입니다.