4.9 KiB
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 |
|
|
|
2026-07-13 | resolved |
error: hibernate-dto-projection-explain-width-not-narrower-2026-07-13
Layer:
raw/errors/— 작업 중 마주친 지표 의미 오해(측정 정정) 기록.
Parent / 부모
- raw/branch-notes/experiment-nplus1-highlight-feed — N+1 랩 L6(DTO 프로젝션) 실행 중 EXPLAIN
width가 문서 모델과 반대로 나와 발견.
증상 / 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) EXPLAINwidth= 2088. 대조 L5 엔티티 페이징(SELECT fi.*, 단일 테이블)width= 1194. 즉 프로젝션이 오히려 넓다. - 만약 "프로젝션은 width가 좁다"를 회귀가드/발표 논거로 썼다면 거짓이었다.
- 발생 환경: Java 21 · Spring Boot 4.0.0 · Hibernate ORM 7.1.8 · PostgreSQL 16(Testcontainers).
- 재현 가능 여부:
always.
재현 절차 / Reproduction
- 부모 프로젝션 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. - 대조 엔티티 페이징(L5 (a)) EXPLAIN:
EXPLAIN ... SELECT fi.* FROM feed_items fi ORDER BY ... LIMIT 20→ Limit 노드width=1194. - 결론: 프로젝션 width(2088) > 엔티티 단일 테이블 width(1194).
근본 원인 / Root cause
- 직접 원인: (1) 프로젝션이
users·pages를 조인하므로 그 테이블 행폭(각 seq scanwidth=1048)이 상위 노드로 흘러든다 — 최종 Limit 노드 width는 조인된 행 전체를 반영한다. (2) PostgreSQL의 EXPLAINwidth는 실제 전송 바이트가 아니라 컬럼 타입 평균폭 추정치다.varchar(길이 미지정 → varchar(255))는 크게 추정되므로, "선택한 컬럼 수"가 아니라 "조인된 행폭 추정"을 반영한다. - 근본 원인: EXPLAIN
width를 "SELECT 컬럼 수의 프록시"로 가정. 실제로는 조인 카디널리티·컬럼 타입 추정의 함수라, 프로젝션이 조인을 쓰면 단일-테이블 엔티티 스캔보다 넓게 나올 수 있다. - 트리거 조건: 여러 테이블을 조인하는 스칼라 프로젝션을, 단일 테이블 엔티티 스캔과 width로 비교.
Sources / 근거
- 로컬 실측:
ca-tmpl:app-bootstrapFeedProjectionIT.l6ExplainProjectionHasLimitAndSemiJoinNotNarrowerWidth— 부모 프로젝션width=2088(build/lab-results/feed-nplus1-l6.md), 대조 L5SELECT fi.*width=1194.:app-bootstrap:test97/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).
권고 해결 / Recommended resolution (적용됨)
- 적용: 문서 모델을 정정 — "프로젝션의 이득은 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 "
HHH000104→HHH90003004"(로그 코드 드리프트), L5 "collectionFetch=ceil(N/batch)"(초기화 수≠fetch 연산 수), L6 여기(EXPLAIN width≠컬럼 수 절감). ORM/DB 지표는 이름·직관과 집계 단위가 다를 수 있으므로 실측으로 재확인이 원칙.