Files
llm-wiki/raw/errors/hibernate-dto-projection-explain-width-not-narrower-2026-07-13.md

4.9 KiB

title, source_type, status, related_branches, related_projects, tags, created, status_label
title source_type status related_branches related_projects tags created status_label
error / hibernate-dto-projection-explain-width-not-narrower-2026-07-13 error-note raw
experiment-nplus1-highlight-feed
ca-skeleton
error
ca-skeleton
hibernate
dto-projection
explain
width
n-plus-one
metric-semantics
resolved
2026-07-13 resolved

error: hibernate-dto-projection-explain-width-not-narrower-2026-07-13

Layer: raw/errors/ — 작업 중 마주친 지표 의미 오해(측정 정정) 기록.

Parent / 부모

증상 / Symptom

  • 문서 모델(L6 가이드 초안 §0.3 D2·§2.4): "DTO 프로젝션은 필요 컬럼만 SELECT하니 EXPLAIN width가 엔티티 SELECT fi.*보다 좁다."
  • 실측(FeedProjectionIT.l6ExplainProjectionHasLimitAndSemiJoinNotNarrowerWidth, seed 100): 부모 스칼라 프로젝션(SELECT fi.id, u.name, u.username, p.url, p.title, fi.first_highlighted_at + JOIN users JOIN pages) EXPLAIN width = 2088. 대조 L5 엔티티 페이징(SELECT fi.*, 단일 테이블) width = 1194. 즉 프로젝션이 오히려 넓다.
  • 만약 "프로젝션은 width가 좁다"를 회귀가드/발표 논거로 썼다면 거짓이었다.
  • 발생 환경: Java 21 · Spring Boot 4.0.0 · Hibernate ORM 7.1.8 · PostgreSQL 16(Testcontainers).
  • 재현 가능 여부: always.

재현 절차 / Reproduction

  1. 부모 프로젝션 EXPLAIN: EXPLAIN (ANALYZE, BUFFERS) SELECT fi.id, u.name, u.username, p.url, p.title, fi.first_highlighted_at FROM feed_items fi JOIN users u ON u.id = fi.user_id JOIN pages p ON p.id = fi.page_id ORDER BY fi.first_highlighted_at DESC, fi.id ASC LIMIT 20 → Limit 노드 width=2088.
  2. 대조 엔티티 페이징(L5 (a)) EXPLAIN: EXPLAIN ... SELECT fi.* FROM feed_items fi ORDER BY ... LIMIT 20 → Limit 노드 width=1194.
  3. 결론: 프로젝션 width(2088) > 엔티티 단일 테이블 width(1194).

근본 원인 / Root cause

  • 직접 원인: (1) 프로젝션이 users·pages조인하므로 그 테이블 행폭(각 seq scan width=1048)이 상위 노드로 흘러든다 — 최종 Limit 노드 width는 조인된 행 전체를 반영한다. (2) PostgreSQL의 EXPLAIN width는 실제 전송 바이트가 아니라 컬럼 타입 평균폭 추정치다. varchar(길이 미지정 → varchar(255))는 크게 추정되므로, "선택한 컬럼 수"가 아니라 "조인된 행폭 추정"을 반영한다.
  • 근본 원인: EXPLAIN width를 "SELECT 컬럼 수의 프록시"로 가정. 실제로는 조인 카디널리티·컬럼 타입 추정의 함수라, 프로젝션이 조인을 쓰면 단일-테이블 엔티티 스캔보다 넓게 나올 수 있다.
  • 트리거 조건: 여러 테이블을 조인하는 스칼라 프로젝션을, 단일 테이블 엔티티 스캔과 width로 비교.

Sources / 근거

  • 로컬 실측: ca-tmpl:app-bootstrap FeedProjectionIT.l6ExplainProjectionHasLimitAndSemiJoinNotNarrowerWidth — 부모 프로젝션 width=2088(build/lab-results/feed-nplus1-l6.md), 대조 L5 SELECT fi.* width=1194. :app-bootstrap:test 97/97 GREEN.
  • 문서: ca-tmpl:docs/notes/L6.md §"실측 정정" + 발표 문서 topic-arrange/n+1liner/n+1liner.md §12.4 "★ 실측 정정" 콜아웃 + evidence/metrics/l6-explain-width.csv(hash-anchor C18/C19).
  • 적용: 문서 모델을 정정 — "프로젝션의 이득은 EXPLAIN width에 안 보인다(오히려 조인 탓 넓다). 진짜 이득은 ORM/JVM 층: getEntityLoadCount()==0(영속 엔티티 미생성)·영속성 컨텍스트 미적재·더티체킹 0·힙 할당 급감 — Statistics로만 관측된다."
  • 회귀가드는 "프로젝션 width가 좁다"를 전제하지 않는다. 대신 프로젝션-불변 단언 getEntityLoadCount()==0·prepared==2(상수)를 쓴다.

교훈 / Lesson

  • EXPLAIN width는 "SELECT한 컬럼 수"의 프록시가 아니다. 조인 카디널리티 + 컬럼 타입 평균폭 추정의 함수라, 여러 테이블을 조인하는 프로젝션은 단일 테이블 엔티티 스캔보다 넓게 나올 수 있다. DTO 프로젝션의 이득은 DB 플랜이 아니라 애플리케이션(ORM/JVM) 층에 있다 — 영속 엔티티 미생성·영속성 컨텍스트 미적재·더티체킹 0. 이건 EXPLAIN이 아니라 Statistics.getEntityLoadCount()로 측정해야 한다.
  • 같은 결의 정정이 이 랩에 넷: L3 "Hibernate 6+ 루트 dedup"(리스트 크기≠전송 행수), L4 "HHH000104HHH90003004"(로그 코드 드리프트), L5 "collectionFetch=ceil(N/batch)"(초기화 수≠fetch 연산 수), L6 여기(EXPLAIN width≠컬럼 수 절감). ORM/DB 지표는 이름·직관과 집계 단위가 다를 수 있으므로 실측으로 재확인이 원칙.