--- id: e1e0e2a0-c6b6-45bf-be42-f697ba5e2fff kind: QUESTION slug: cardinality-estimate-after-analyze title: ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가 topic: jpa-feed-query-performance topicName: JPA 피드 조회 성능 — N+1 진단과 조회 전략의 진화 topicName: JPA 피드 조회 성능 project: Liner N + 1문제 status: 게시 전 studio: "https://hyeonworks.com/studio/documents/e1e0e2a0-c6b6-45bf-be42-f697ba5e2fff/edit" questionStatus: OPEN sourceRevision: n+1liner-lab@2026-08 source: - final/document.md#6-4 - final/document.md#4-6 --- # ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가 플래너가 계획을 고르기 전에 조건에 걸리는 행이 몇 개일지 어림한 값이 추정 행수(cardinality estimate)이고, PostgreSQL은 `ANALYZE`로 컬럼 분포 통계를 모아 이 값을 낸다. 반복되는 하이라이트 조회의 실행계획에서는 추정 행수가 1이었는데 실제로는 500행이 나왔다. 대량 시드 직후 `ANALYZE`를 실행하지 않아 통계가 `feed_item_id`별 편중을 담지 못했다는 가설을 세웠지만 아직 검증하지 않았다. ## 관계 - **PostgreSQL Query Plan 측정 기준** 추정 행수와 실제 행수가 벌어지면 통계를 갱신한 뒤 다시 재고 전후를 비교하라고 적어 두었다. - **Fetch 타입이 아니라 조회 방식이 만든 ToOne N+1** 이 실행계획을 `EXPLAIN`으로 확인하고 500배 차이를 처음 적었다. ## 사실 - 대량 시드 직후 잰 계획에서 추정 행수는 1인데 실제 행수는 500이었다. 500배 차이다. - 이 계획은 feed_item_id 조건을 ix_highlights_feed_items_created 인덱스로 처리했고(Index Scan) 실행시간은 0.173 ms였다. - 읽은 블록 14개가 모두 캐시에 있었고 디스크 읽기는 0이었다(shared hit=14, read=0). - 하이라이트 개수는 순위 기반 편중 분포라 feed_item_id마다 자식 행이 500개에서 1개까지 벌어진다. - fetch join 계획의 Hash Join 노드에서도 추정 rows=4202 에 실제 1,961로 어긋났다. - 대량 시드 직후 ANALYZE를 실행하지 않았다. ## 가정 - 통계를 갱신하면 feed_item_id별 분포가 반영되어 추정 행수가 실제에 가까워진다. - 추정 행수가 달라지면 플래너가 다른 계획을 고를 수 있다. - 편중이 큰 컬럼은 기본 통계 대상 수로 부족할 수 있다. ## 미지수 - ANALYZE highlights를 실행하고 같은 조회를 다시 EXPLAIN하면 추정 행수가 1에서 얼마로 바뀌는가. - 추정 행수가 바뀌면 Index Scan이 Seq Scan으로, 또는 그 반대로 뒤집히는가. - highlights.feed_item_id의 통계 대상 수를 늘리면 추정 행수가 실제 500에 더 가까워지는가. - 실행시간 0.173 ms와 읽은 블록 14개가 달라지는가. - 통계를 갱신하면 지금까지의 결론 중 무엇이 바뀌는가. 왕복 수와 전송 행수에 관한 판단은 통계와 무관하다. - 운영에서 대량 적재 뒤 ANALYZE를 절차에 넣을 것인가. ## 제약 - 통계 갱신 전후를 비교하려면 같은 데이터에서 연속으로 재야 한다. 캐시 상태가 섞이면 읽은 블록 수를 나란히 놓을 수 없다. - 지금까지 기록한 실행계획은 모두 ANALYZE 전 값이다. 갱신 후 값과 섞이지 않도록 ANALYZE 전을 Plan A, 후를 Plan B로 나눠 적는다. ## 선택지 ### 1. 통계를 갱신하고 전후를 비교한다 같은 데이터에서 ANALYZE highlights 전후 계획을 나란히 기록한다. 추정 행수, 스캔 방식, 읽은 블록, 실행시간을 대조한다. ### 2. 통계 대상 수까지 조절해 본다 highlights.feed_item_id의 통계 대상 수를 늘린 뒤 다시 잰다. 기본값으로 부족한지 확인한다. 변수가 하나 더 늘어 갱신 전후 비교와 통계 대상 수 비교가 섞인다. ### 3. 갱신 전 값만 두고 넘어간다 왕복 수와 전송 행수에 관한 결론은 통계와 무관하다. 추정 차이를 한계로만 적고 진행한다. 플래너가 다른 계획을 고르는지는 확인하지 못한다. ## 다음 검증 1. 대량 시드 직후 계획을 다시 기록한다. ANALYZE 전 값이므로 Plan A로 적는다. 2. ANALYZE highlights를 실행한다. 3. 같은 쿼리를 같은 테스트 실행 안에서 EXPLAIN (ANALYZE, BUFFERS)로 다시 잰다. 추정 행수, 스캔 방식, 읽은 블록, 실행시간을 기록한다. 4. Plan A와 Plan B를 나란히 두고 무엇이 달라졌는지 적는다. 5. 계획이 바뀌었다면 지금까지의 결론 중 영향을 받는 항목이 있는지 확인한다. 6. 운영 절차에 대량 적재 뒤 ANALYZE를 넣을지 판단한다.