- generic [ref=f26e3]: - link "본문으로 건너뛰기" [ref=f26e4] [cursor=pointer]: - /url: "#main-content" - banner [ref=f26e5]: - generic [ref=f26e6]: - link "TechLog Studio" [ref=f26e7] [cursor=pointer]: - /url: /studio - text: TechLog - generic [ref=f26e8]: Studio - navigation "Studio 주 탐색" [ref=f26e10]: - link "작업본" [ref=f26e11] [cursor=pointer]: - /url: /studio/documents - link "게시 기록" [ref=f26e12] [cursor=pointer]: - /url: /studio/publications - link "새 문서" [ref=f26e13] [cursor=pointer]: - /url: /studio/documents/new - link "주제·프로젝트" [ref=f26e14] [cursor=pointer]: - /url: /studio/taxonomy - link "릴리즈" [ref=f26e15] [cursor=pointer]: - /url: /studio/releases - link "공개 사이트 보기" [ref=f26e16] [cursor=pointer]: - /url: / - button "로그아웃" [ref=f26e17] - main [ref=f26e18]: - generic [ref=f26e19]: - generic [ref=f26e20]: - region [ref=f26e21]: - generic [ref=f26e22]: - paragraph [ref=f26e23]: QUESTION · VERSION 6 - heading "문서 편집" [level=1] [ref=f26e24] - paragraph [ref=f26e25]: ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가 - region [ref=f26e26]: - generic [ref=f26e27]: - paragraph [ref=f26e28]: DOCUMENT - heading "기본 정보" [level=2] [ref=f26e29] - generic [ref=f26e30]: - generic [ref=f26e31]: - generic [ref=f26e32]: 제목 - textbox "제목" [ref=f26e33]: ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가 - generic [ref=f26e34]: - generic [ref=f26e35]: slug - textbox "slug" [ref=f26e36]: - /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈) - text: cardinality-estimate-after-analyze - generic [ref=f26e37]: - generic [ref=f26e38]: 요약 - textbox "요약" [ref=f26e39]: 반복되는 하이라이트 조회의 실행계획에서 추정 행수는 1이고 실제 행수는 500이었다. 대량 데이터를 넣은 직후 통계를 갱신하지 않아 편중을 담지 못했다는 가설을 세웠지만 아직 검증하지 않았다. - generic [aria-hidden] [ref=f26e40]: 목록 카드에는 약 90자까지 보입니다 · 107 / 2000 - generic [ref=f26e41]: - generic [ref=f26e42]: Topic - combobox "Topic" [ref=f26e43]: - option "선택하지 않음" - option "JPA 피드 조회 성능" [selected] - option "OAuth/OIDC 인증 경계" - generic [ref=f26e44]: - generic [ref=f26e45]: Project - combobox "Project" [ref=f26e46]: - option "미지정" - option "Backend Clean Architecture" - option "KeyCloak Patterns" - option "Liner N + 1문제" [selected] - status [ref=f26e47] - group "축 — 고르지 않으면 이 주제의 공통 기록이 됩니다" [ref=f26e48]: - generic [ref=f26e50] [cursor=pointer]: - checkbox "파생 쿼리 그대로" [ref=f26e51] - generic [ref=f26e52]: 파생 쿼리 그대로 - generic [ref=f26e53] [cursor=pointer]: - checkbox "컬렉션 fetch join" [ref=f26e54] - generic [ref=f26e55]: 컬렉션 fetch join - generic [ref=f26e56] [cursor=pointer]: - checkbox "fetch join + 페이징" [ref=f26e57] - generic [ref=f26e58]: fetch join + 페이징 - group "관계" [ref=f26e59]: - generic [ref=f26e61]: - generic [ref=f26e62]: - generic [ref=f26e63]: 관계 1 대상 - combobox "관계 1 대상" [ref=f26e64]: - option "대상 선택" - option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" - option "Collection Fetch Join으로 N+1을 해결하다 만난 MultiBag예외와 행 폭증 문제" - option "Collection Fetch Join Pagination의 In-memory Paging" - option "DTO 변환 과정에서 발생한 Highlight 컬렉션 N+1" - option "Fetch 타입이 아닌 조회 방식으로 인한 N+1" [disabled] - option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" - option "Projection 이후에도 1,509행을 읽은 Row Over-fetch" - option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" - option "SPA에서 OAuth Token을 JavaScript Memory에 보관한 경우" - option "Visibility OR이 Keyset Index를 깨뜨린 문제" - option "Authorization Code와 PKCE가 보호하는 구간" - option "Bearer JWT가 인증된 principal이 되기까지" - option "Cookie로 인증하는 요청에서 CSRF token이 하는 일" - option "브라우저가 credential을 보관하는 위치와 그 성질" - option "Forward-Auth와 Nginx auth_request의 동작" - option "외부 IdP Brokering의 동작" - option "Authorization Code Flow의 Endpoint와 Credential 이동 기준" - option "BFF 인증 구조 설계 기준" - option "Feed Visibility Query Pattern" - option "Fetch Join · Batch · Projection 선택 기준" - option "Fetch Type과 Fetch Strategy 구분" - option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건" - option "외부 IdP 연동과 Application 인증 구조의 경계" - option "JPA N+1 정량 진단 기준" - option "Keyset Pagination 설계 기준" - option "OAuth/OIDC 인증 패턴 선택 기준" - option "OAuth Token과 Application Session을 구분하는 기준" - option "PostgreSQL Query Plan 측정 기준" [selected] - option "Public Client와 Confidential Client 구분 기준" - option "Top-N-per-group 선택 기준" - option "실제 동시 트래픽에서도 이 구조가 안정적인가" - option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" - option "ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가" - option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" - option "feed_visible을 Production CQRS로 승격할 것인가" - option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가" - option "Highlight 없는 FeedItem을 허용할 것인가" - option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가" - option "Round Trip과 Row Volume을 독립 측정할 것인가" - option "BFF가 OAuth Token을 관리하는 조건" - option "Collection Fetch Join과 Pagination을 같이 사용하지 않는다" - option "Entity Graph 조회에는 Batch Fetch를 사용한다" - option "Feed Pagination은 Keyset을 사용한다" - option "외부 IdP와의 연동이라도 별도의 인증 방식이 아니다." - option "Query Plan은 실제 PostgreSQL에서 측정한다" - option "Query Strategy는 FeedQueryPort 뒤에서 소유한다" - option "현재 Read Model은 CQRS-lite로 유지한다" - option "화면 조회는 Read Projection을 사용한다" - generic [ref=f26e65]: - generic [ref=f26e66]: 관계 1 이유 - textbox "관계 1 이유" [ref=f26e67]: 추정과 실제의 차이를 기록하는 기준이다. - generic [aria-hidden] [ref=f26e68]: 공개 화면의 「다음에 읽을 것」에 그대로 나갑니다. - generic [ref=f26e69]: - button "위로" [disabled] [ref=f26e70] - button "아래로" [ref=f26e71] - button "삭제" [ref=f26e72] - generic [ref=f26e73]: - generic [ref=f26e74]: - generic [ref=f26e75]: 관계 2 대상 - combobox "관계 2 대상" [ref=f26e76]: - option "대상 선택" - option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" - option "Collection Fetch Join으로 N+1을 해결하다 만난 MultiBag예외와 행 폭증 문제" - option "Collection Fetch Join Pagination의 In-memory Paging" - option "DTO 변환 과정에서 발생한 Highlight 컬렉션 N+1" - option "Fetch 타입이 아닌 조회 방식으로 인한 N+1" [selected] - option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" - option "Projection 이후에도 1,509행을 읽은 Row Over-fetch" - option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" - option "SPA에서 OAuth Token을 JavaScript Memory에 보관한 경우" - option "Visibility OR이 Keyset Index를 깨뜨린 문제" - option "Authorization Code와 PKCE가 보호하는 구간" - option "Bearer JWT가 인증된 principal이 되기까지" - option "Cookie로 인증하는 요청에서 CSRF token이 하는 일" - option "브라우저가 credential을 보관하는 위치와 그 성질" - option "Forward-Auth와 Nginx auth_request의 동작" - option "외부 IdP Brokering의 동작" - option "Authorization Code Flow의 Endpoint와 Credential 이동 기준" - option "BFF 인증 구조 설계 기준" - option "Feed Visibility Query Pattern" - option "Fetch Join · Batch · Projection 선택 기준" - option "Fetch Type과 Fetch Strategy 구분" - option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건" - option "외부 IdP 연동과 Application 인증 구조의 경계" - option "JPA N+1 정량 진단 기준" - option "Keyset Pagination 설계 기준" - option "OAuth/OIDC 인증 패턴 선택 기준" - option "OAuth Token과 Application Session을 구분하는 기준" - option "PostgreSQL Query Plan 측정 기준" [disabled] - option "Public Client와 Confidential Client 구분 기준" - option "Top-N-per-group 선택 기준" - option "실제 동시 트래픽에서도 이 구조가 안정적인가" - option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가" - option "ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가" - option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" - option "feed_visible을 Production CQRS로 승격할 것인가" - option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가" - option "Highlight 없는 FeedItem을 허용할 것인가" - option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가" - option "Round Trip과 Row Volume을 독립 측정할 것인가" - option "BFF가 OAuth Token을 관리하는 조건" - option "Collection Fetch Join과 Pagination을 같이 사용하지 않는다" - option "Entity Graph 조회에는 Batch Fetch를 사용한다" - option "Feed Pagination은 Keyset을 사용한다" - option "외부 IdP와의 연동이라도 별도의 인증 방식이 아니다." - option "Query Plan은 실제 PostgreSQL에서 측정한다" - option "Query Strategy는 FeedQueryPort 뒤에서 소유한다" - option "현재 Read Model은 CQRS-lite로 유지한다" - option "화면 조회는 Read Projection을 사용한다" - generic [ref=f26e77]: - generic [ref=f26e78]: 관계 2 이유 - textbox [ref=f26e79] - generic [aria-hidden] [ref=f26e80]: 공개 화면의 「다음에 읽을 것」에 그대로 나갑니다. - generic [ref=f26e81]: - button "위로" [ref=f26e82] - button "아래로" [disabled] [ref=f26e83] - button "삭제" [ref=f26e84] - button "관계 추가" [ref=f26e85] - region [ref=f26e86]: - generic [ref=f26e87]: - paragraph [ref=f26e88]: QUESTION - heading "판단과 다음 검증" [level=2] [ref=f26e89] - generic [ref=f26e90]: - generic [ref=f26e91]: 질문 상태 - combobox "질문 상태" [ref=f26e92]: - option "아직 정하지 않음" - option "OPEN" [selected] - option "RESOLVED" - group "확인한 사실" [ref=f26e93]: - generic [ref=f26e95]: - generic [ref=f26e96]: - generic [ref=f26e97]: 확인한 사실 1 - textbox "확인한 사실 1" [ref=f26e98]: 대량 시드 직후 측정한 계획에서 추정 rows는 1, 실제 rows는 500이었다. 500배 차이다. - generic [ref=f26e99]: - button "위로" [disabled] [ref=f26e100] - button "아래로" [ref=f26e101] - button "삭제" [ref=f26e102] - generic [ref=f26e103]: - generic [ref=f26e104]: - generic [ref=f26e105]: 확인한 사실 2 - textbox "확인한 사실 2" [ref=f26e106]: 이 계획은 feed_item_id 조건을 인덱스로 처리했고 실행시간은 0.173 ms였다. - generic [ref=f26e107]: - button "위로" [ref=f26e108] - button "아래로" [ref=f26e109] - button "삭제" [ref=f26e110] - generic [ref=f26e111]: - generic [ref=f26e112]: - generic [ref=f26e113]: 확인한 사실 3 - textbox "확인한 사실 3" [ref=f26e114]: 읽은 블록은 모두 캐시에서 왔다. 디스크 읽기는 0이었다. - generic [ref=f26e115]: - button "위로" [ref=f26e116] - button "아래로" [ref=f26e117] - button "삭제" [ref=f26e118] - generic [ref=f26e119]: - generic [ref=f26e120]: - generic [ref=f26e121]: 확인한 사실 4 - textbox "확인한 사실 4" [ref=f26e122]: 하이라이트 개수는 순위 기반 편중 분포라 feed_item_id별 자식 수가 크게 다르다. 상한 500, 하한 1이다. - generic [ref=f26e123]: - button "위로" [ref=f26e124] - button "아래로" [ref=f26e125] - button "삭제" [ref=f26e126] - generic [ref=f26e127]: - generic [ref=f26e128]: - generic [ref=f26e129]: 확인한 사실 5 - textbox "확인한 사실 5" [ref=f26e130]: fetch join 조인 계획에서도 추정 4,202와 실제 1,961의 차이가 있었다. - generic [ref=f26e131]: - button "위로" [ref=f26e132] - button "아래로" [ref=f26e133] - button "삭제" [ref=f26e134] - generic [ref=f26e135]: - generic [ref=f26e136]: - generic [ref=f26e137]: 확인한 사실 6 - textbox "확인한 사실 6" [ref=f26e138]: 시드 직후 통계 갱신 명령을 실행하지 않았다. - generic [ref=f26e139]: - button "위로" [ref=f26e140] - button "아래로" [disabled] [ref=f26e141] - button "삭제" [ref=f26e142] - button "확인한 사실 추가" [ref=f26e143] - group "가정" [ref=f26e144]: - generic [ref=f26e146]: - generic [ref=f26e147]: - generic [ref=f26e148]: 가정 1 - textbox "가정 1" [ref=f26e149]: 통계를 갱신하면 feed_item_id별 분포가 반영되어 추정이 실제에 가까워진다. - generic [ref=f26e150]: - button "위로" [disabled] [ref=f26e151] - button "아래로" [ref=f26e152] - button "삭제" [ref=f26e153] - generic [ref=f26e154]: - generic [ref=f26e155]: - generic [ref=f26e156]: 가정 2 - textbox "가정 2" [ref=f26e157]: 추정이 달라지면 플래너가 다른 계획을 고를 수 있다. - generic [ref=f26e158]: - button "위로" [ref=f26e159] - button "아래로" [ref=f26e160] - button "삭제" [ref=f26e161] - generic [ref=f26e162]: - generic [ref=f26e163]: - generic [ref=f26e164]: 가정 3 - textbox "가정 3" [ref=f26e165]: 편중이 큰 컬럼은 기본 통계 대상 수로 부족할 수 있다. - generic [ref=f26e166]: - button "위로" [ref=f26e167] - button "아래로" [disabled] [ref=f26e168] - button "삭제" [ref=f26e169] - button "가정 추가" [ref=f26e170] - group "남은 미지수" [ref=f26e171]: - generic [ref=f26e173]: - generic [ref=f26e174]: - generic [ref=f26e175]: 남은 미지수 1 - textbox "남은 미지수 1" [ref=f26e176]: 통계를 갱신한 뒤 추정 행수가 실제에 얼마나 가까워지는가. - generic [ref=f26e177]: - button "위로" [disabled] [ref=f26e178] - button "아래로" [ref=f26e179] - button "삭제" [ref=f26e180] - generic [ref=f26e181]: - generic [ref=f26e182]: - generic [ref=f26e183]: 남은 미지수 2 - textbox "남은 미지수 2" [ref=f26e184]: 추정이 바뀌면 스캔 방식이 바뀌는가. 인덱스에서 순차 스캔으로, 또는 그 반대로 뒤집히는가. - generic [ref=f26e185]: - button "위로" [ref=f26e186] - button "아래로" [ref=f26e187] - button "삭제" [ref=f26e188] - generic [ref=f26e189]: - generic [ref=f26e190]: - generic [ref=f26e191]: 남은 미지수 3 - textbox "남은 미지수 3" [ref=f26e192]: 편중이 큰 컬럼에 통계 대상 수를 늘리면 추정이 더 좋아지는가. - generic [ref=f26e193]: - button "위로" [ref=f26e194] - button "아래로" [ref=f26e195] - button "삭제" [ref=f26e196] - generic [ref=f26e197]: - generic [ref=f26e198]: - generic [ref=f26e199]: 남은 미지수 4 - textbox "남은 미지수 4" [ref=f26e200]: 실행시간과 읽은 블록 수가 달라지는가. - generic [ref=f26e201]: - button "위로" [ref=f26e202] - button "아래로" [ref=f26e203] - button "삭제" [ref=f26e204] - generic [ref=f26e205]: - generic [ref=f26e206]: - generic [ref=f26e207]: 남은 미지수 5 - textbox "남은 미지수 5" [ref=f26e208]: 이 차이가 지금까지의 결론을 바꾸는가. 왕복 수와 전송 행수에 관한 판단은 통계와 무관하다. - generic [ref=f26e209]: - button "위로" [ref=f26e210] - button "아래로" [ref=f26e211] - button "삭제" [ref=f26e212] - generic [ref=f26e213]: - generic [ref=f26e214]: - generic [ref=f26e215]: 남은 미지수 6 - textbox "남은 미지수 6" [ref=f26e216]: 운영에서 대량 적재 후 통계 갱신을 절차에 넣을 것인가. - generic [ref=f26e217]: - button "위로" [ref=f26e218] - button "아래로" [disabled] [ref=f26e219] - button "삭제" [ref=f26e220] - button "남은 미지수 추가" [ref=f26e221] - group "제약" [ref=f26e222]: - generic [ref=f26e224]: - generic [ref=f26e225]: - generic [ref=f26e226]: 제약 1 - textbox "제약 1" [ref=f26e227]: 통계 갱신 전후를 비교하려면 같은 데이터에서 연속으로 재야 한다. 캐시 상태가 섞이면 비교가 흐려진다. - generic [ref=f26e228]: - button "위로" [disabled] [ref=f26e229] - button "아래로" [ref=f26e230] - button "삭제" [ref=f26e231] - generic [ref=f26e232]: - generic [ref=f26e233]: - generic [ref=f26e234]: 제약 2 - textbox "제약 2" [ref=f26e235]: 지금까지 기록한 실행계획은 모두 갱신 전 값이다. 갱신 후 값과 섞어 읽지 않도록 표기를 구분해야 한다. - generic [ref=f26e236]: - button "위로" [ref=f26e237] - button "아래로" [ref=f26e238] - button "삭제" [ref=f26e239] - generic [ref=f26e240]: - generic [ref=f26e241]: - generic [ref=f26e242]: 제약 3 - textbox "제약 3" [ref=f26e243]: 실행하기 전에는 수치를 채우지 않는다. - generic [ref=f26e244]: - button "위로" [ref=f26e245] - button "아래로" [disabled] [ref=f26e246] - button "삭제" [ref=f26e247] - button "제약 추가" [ref=f26e248] - group "검토한 선택지" [ref=f26e249]: - generic [ref=f26e251]: - generic [ref=f26e252]: - generic [ref=f26e253]: 선택지 1 제목 - textbox "선택지 1 제목" [ref=f26e254]: 통계를 갱신하고 전후를 비교한다 - generic [ref=f26e255]: - generic [ref=f26e256]: 선택지 1 설명 - textbox "선택지 1 설명" [ref=f26e257]: 같은 데이터에서 갱신 전후 계획을 나란히 기록한다. 추정 행수, 스캔 방식, 읽은 블록, 실행시간을 대조한다. 측정이 한 번 더 필요하지만 가설을 닫을 수 있다. - generic [ref=f26e258]: - button "위로" [disabled] [ref=f26e259] - button "아래로" [ref=f26e260] - button "삭제" [ref=f26e261] - generic [ref=f26e262]: - generic [ref=f26e263]: - generic [ref=f26e264]: 선택지 2 제목 - textbox "선택지 2 제목" [ref=f26e265]: 통계 대상 수까지 조절해 본다 - generic [ref=f26e266]: - generic [ref=f26e267]: 선택지 2 설명 - textbox "선택지 2 설명" [ref=f26e268]: 편중이 큰 컬럼의 통계 대상 수를 늘린 뒤 다시 잰다. 기본값으로 부족한지 확인한다. 변수가 하나 더 늘어 비교가 복잡해진다. - generic [ref=f26e269]: - button "위로" [ref=f26e270] - button "아래로" [ref=f26e271] - button "삭제" [ref=f26e272] - generic [ref=f26e273]: - generic [ref=f26e274]: - generic [ref=f26e275]: 선택지 3 제목 - textbox "선택지 3 제목" [ref=f26e276]: 갱신 전 값만 두고 넘어간다 - generic [ref=f26e277]: - generic [ref=f26e278]: 선택지 3 설명 - textbox "선택지 3 설명" [ref=f26e279]: 왕복 수와 전송 행수에 관한 결론은 통계와 무관하다. 추정 차이를 한계로만 적고 진행한다. 플래너가 다른 계획을 고를 가능성을 확인하지 못한 채 남는다. - generic [ref=f26e280]: - button "위로" [ref=f26e281] - button "아래로" [disabled] [ref=f26e282] - button "삭제" [ref=f26e283] - button "선택지 추가" [ref=f26e284] - generic [ref=f26e285]: - generic [ref=f26e286]: 다음 검증 - textbox "다음 검증" [ref=f26e287]: 1. 대량 시드 직후 현재 계획을 다시 기록한다. 갱신 전 값임을 명시한다. 2. 통계를 갱신한다. 3. 같은 쿼리를 같은 실행 안에서 다시 EXPLAIN한다. 추정 행수, 스캔 방식, 읽은 블록, 실행시간을 기록한다. 4. 두 계획을 나란히 두고 무엇이 달라졌는지 적는다. 5. 계획이 바뀌었다면 지금까지의 결론 중 영향을 받는 항목이 있는지 확인한다. 6. 운영 절차에 대량 적재 후 통계 갱신을 넣을지 판단한다. - region [ref=f26e288]: - generic [ref=f26e289]: - paragraph [ref=f26e290]: LIVE - heading "즉시 미리보기" [level=2] [ref=f26e291] - generic [ref=f26e294]: - generic [ref=f26e295]: - navigation "문서 경로" [ref=f26e296]: - link "열린 질문" [ref=f26e297] [cursor=pointer]: - /url: /explore/questions - generic [aria-hidden] [ref=f26e298]: / - generic [ref=f26e299]: JPA 피드 조회 성능 - generic [aria-hidden] [ref=f26e300]: / - link "Liner N + 1문제" [ref=f26e301] [cursor=pointer]: - /url: /projects/liner-n-plus-1 - heading "ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가" [level=1] [ref=f26e302] - paragraph [ref=f26e303]: 반복되는 하이라이트 조회의 실행계획에서 추정 행수는 1이고 실제 행수는 500이었다. 대량 데이터를 넣은 직후 통계를 갱신하지 않아 편중을 담지 못했다는 가설을 세웠지만 아직 검증하지 않았다. - generic [ref=f26e304]: - generic [ref=f26e305]: - term [ref=f26e306]: 유형 - definition [ref=f26e307]: 열린 질문 - generic [ref=f26e308]: - term [ref=f26e309]: 프로젝트 - definition [ref=f26e310]: Liner N + 1문제 - generic [ref=f26e311]: - term [ref=f26e312]: 게시 - definition [ref=f26e313]: 게시 전 - paragraph [ref=f26e314]: OPEN - article [ref=f26e315]: - region [ref=f26e316]: - heading "확인한 사실" [level=2] [ref=f26e317] - list [ref=f26e318]: - listitem [ref=f26e319]: - paragraph [ref=f26e320]: 대량 시드 직후 측정한 계획에서 추정 rows는 1, 실제 rows는 500이었다. 500배 차이다. - listitem [ref=f26e321]: - paragraph [ref=f26e322]: 이 계획은 feed_item_id 조건을 인덱스로 처리했고 실행시간은 0.173 ms였다. - listitem [ref=f26e323]: - paragraph [ref=f26e324]: 읽은 블록은 모두 캐시에서 왔다. 디스크 읽기는 0이었다. - listitem [ref=f26e325]: - paragraph [ref=f26e326]: 하이라이트 개수는 순위 기반 편중 분포라 feed_item_id별 자식 수가 크게 다르다. 상한 500, 하한 1이다. - listitem [ref=f26e327]: - paragraph [ref=f26e328]: fetch join 조인 계획에서도 추정 4,202와 실제 1,961의 차이가 있었다. - listitem [ref=f26e329]: - paragraph [ref=f26e330]: 시드 직후 통계 갱신 명령을 실행하지 않았다. - region [ref=f26e331]: - heading "가정" [level=2] [ref=f26e332] - list [ref=f26e333]: - listitem [ref=f26e334]: - paragraph [ref=f26e335]: 통계를 갱신하면 feed_item_id별 분포가 반영되어 추정이 실제에 가까워진다. - listitem [ref=f26e336]: - paragraph [ref=f26e337]: 추정이 달라지면 플래너가 다른 계획을 고를 수 있다. - listitem [ref=f26e338]: - paragraph [ref=f26e339]: 편중이 큰 컬럼은 기본 통계 대상 수로 부족할 수 있다. - region [ref=f26e340]: - heading "남은 미지수" [level=2] [ref=f26e341] - list [ref=f26e342]: - listitem [ref=f26e343]: - paragraph [ref=f26e344]: 통계를 갱신한 뒤 추정 행수가 실제에 얼마나 가까워지는가. - listitem [ref=f26e345]: - paragraph [ref=f26e346]: 추정이 바뀌면 스캔 방식이 바뀌는가. 인덱스에서 순차 스캔으로, 또는 그 반대로 뒤집히는가. - listitem [ref=f26e347]: - paragraph [ref=f26e348]: 편중이 큰 컬럼에 통계 대상 수를 늘리면 추정이 더 좋아지는가. - listitem [ref=f26e349]: - paragraph [ref=f26e350]: 실행시간과 읽은 블록 수가 달라지는가. - listitem [ref=f26e351]: - paragraph [ref=f26e352]: 이 차이가 지금까지의 결론을 바꾸는가. 왕복 수와 전송 행수에 관한 판단은 통계와 무관하다. - listitem [ref=f26e353]: - paragraph [ref=f26e354]: 운영에서 대량 적재 후 통계 갱신을 절차에 넣을 것인가. - region [ref=f26e355]: - heading "제약" [level=2] [ref=f26e356] - list [ref=f26e357]: - listitem [ref=f26e358]: - paragraph [ref=f26e359]: 통계 갱신 전후를 비교하려면 같은 데이터에서 연속으로 재야 한다. 캐시 상태가 섞이면 비교가 흐려진다. - listitem [ref=f26e360]: - paragraph [ref=f26e361]: 지금까지 기록한 실행계획은 모두 갱신 전 값이다. 갱신 후 값과 섞어 읽지 않도록 표기를 구분해야 한다. - listitem [ref=f26e362]: - paragraph [ref=f26e363]: 실행하기 전에는 수치를 채우지 않는다. - region [ref=f26e364]: - heading "검토한 선택지" [level=2] [ref=f26e365] - list [ref=f26e366]: - listitem [ref=f26e367]: - heading "통계를 갱신하고 전후를 비교한다" [level=3] [ref=f26e368] - paragraph [ref=f26e369]: 같은 데이터에서 갱신 전후 계획을 나란히 기록한다. 추정 행수, 스캔 방식, 읽은 블록, 실행시간을 대조한다. - paragraph [ref=f26e370]: 측정이 한 번 더 필요하지만 가설을 닫을 수 있다. - listitem [ref=f26e371]: - heading "통계 대상 수까지 조절해 본다" [level=3] [ref=f26e372] - paragraph [ref=f26e373]: 편중이 큰 컬럼의 통계 대상 수를 늘린 뒤 다시 잰다. 기본값으로 부족한지 확인한다. - paragraph [ref=f26e374]: 변수가 하나 더 늘어 비교가 복잡해진다. - listitem [ref=f26e375]: - heading "갱신 전 값만 두고 넘어간다" [level=3] [ref=f26e376] - paragraph [ref=f26e377]: 왕복 수와 전송 행수에 관한 결론은 통계와 무관하다. 추정 차이를 한계로만 적고 진행한다. - paragraph [ref=f26e378]: 플래너가 다른 계획을 고를 가능성을 확인하지 못한 채 남는다. - region [ref=f26e379]: - paragraph [ref=f26e380]: Next - heading "다음 검증" [level=2] [ref=f26e381] - paragraph [ref=f26e382]: 1. 대량 시드 직후 현재 계획을 다시 기록한다. 갱신 전 값임을 명시한다. - paragraph [ref=f26e383]: 2. 통계를 갱신한다. - paragraph [ref=f26e384]: 3. 같은 쿼리를 같은 실행 안에서 다시 EXPLAIN한다. 추정 행수, 스캔 방식, 읽은 블록, 실행시간을 기록한다. - paragraph [ref=f26e385]: 4. 두 계획을 나란히 두고 무엇이 달라졌는지 적는다. - paragraph [ref=f26e386]: 5. 계획이 바뀌었다면 지금까지의 결론 중 영향을 받는 항목이 있는지 확인한다. - paragraph [ref=f26e387]: 6. 운영 절차에 대량 적재 후 통계 갱신을 넣을지 판단한다. - region [ref=f26e388]: - paragraph [ref=f26e389]: Next - heading "다음에 읽을 것" [level=2] [ref=f26e390] - list [ref=f26e391]: - listitem [ref=f26e392]: - link "검증 기록 Fetch 타입이 아닌 조회 방식으로 인한 N+1" [ref=f26e393] [cursor=pointer]: - /url: /cases/eager-toone-nplus1-without-access - generic [ref=f26e394]: 검증 기록 - strong [ref=f26e396]: Fetch 타입이 아닌 조회 방식으로 인한 N+1 - generic [aria-hidden] [ref=f26e397]: ↗ - complementary [ref=f26e398]: - heading "작업 상태" [level=2] [ref=f26e399] - status "편집 상태" [ref=f26e400]: 저장되지 않음 - generic [ref=f26e401]: - generic [ref=f26e402]: - term [ref=f26e403]: 저장 버전 - definition [ref=f26e404]: "6" - generic [ref=f26e405]: - term [ref=f26e406]: 종류 - definition [ref=f26e407]: 열린 질문 - paragraph [ref=f26e408]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다. - generic [ref=f26e409]: - button "저장" [ref=f26e410] - button "게시" [ref=f26e411] - paragraph [ref=f26e412]