컬렉션 fetch join + 페이징이 발행하는 조인의 실행계획 (a) — LIMIT 노드 없음 출처: FeedPersistenceIT.l4ExplainCollectionJoinHasNoLimitButEntityPagingDoes 콘솔 출력 (a) 조건: seed(100) 직후. warm buffer cache(shared read=0). 쿼리: EXPLAIN (ANALYZE, BUFFERS) SELECT fi.*, h.* FROM feed_items fi JOIN highlights h ON h.feed_item_id = fi.id ORDER BY fi.first_highlighted_at DESC, fi.id ASC (fetch join `select f from FeedItemJpaEntity f join fetch f.highlights order by ...`가 페이징(setMaxResults) 시 발행하는 조인과 같은 shape — 단, SQL에 LIMIT이 붙지 않는다) Sort (cost=293.30..297.76 rows=1782 width=1904) (actual time=1.219..1.266 rows=1961 loops=1) Sort Key: fi.first_highlighted_at DESC, fi.id Sort Method: quicksort Memory: 445kB Buffers: shared hit=173 -> Hash Join (cost=12.48..197.08 rows=1782 width=1904) (actual time=0.279..0.636 rows=1961 loops=1) Hash Cond: (h.feed_item_id = fi.id) Buffers: shared hit=173 -> Seq Scan on highlights h (cost=0.00..179.82 rows=1782 width=710) (actual time=0.231..0.330 rows=1961 loops=1) Buffers: shared hit=162 -> Hash (cost=11.66..11.66 rows=66 width=1194) (actual time=0.035..0.036 rows=100 loops=1) Buckets: 1024 Batches: 1 Memory Usage: 22kB Buffers: shared hit=11 -> Seq Scan on feed_items fi (cost=0.00..11.66 rows=66 width=1194) (actual time=0.018..0.023 rows=100 loops=1) Buffers: shared hit=11 Planning Time: 0.135 ms Execution Time: 1.369 ms 관찰(문서 §10): - 계획 어디에도 Limit 노드가 없다 = DB가 페이징을 하지 않았다. 조인 결과 전체(actual rows=1961 = Σ highlights)를 quicksort로 445kB 정렬한 뒤 그대로 반환한다. 페이지 크기(20)로 자르는 일은 SQL 밖 — Hibernate가 메모리에서 한다. - 부모 feed_items는 100행(Hash 노드)인데 Hash Join 노드 actual rows=1961(= Σ highlights, §9.3)로 부푼다 — 컬렉션 fetch join의 카테시안이 그대로다. 그 곱해진 행에 DB LIMIT을 걸면 "20개 부모"가 아니라 "20개 조인 행"을 잘라 어떤 부모는 하이라이트가 잘린 반쪽으로 로드될 위험 → 그래서 Hibernate가 LIMIT을 빼고 인메모리 페이징한다. - Buffers: shared read=0 → warm buffer cache. cold 디스크 I/O 실행시간으로 읽지 말 것. - Execution Time 1.369 ms는 executor 내부 시간(§6.4 caveat와 동일). 애플리케이션 지연이 아니다. - 대조군은 l4-entity-paging-limit.txt (엔티티만 페이징 → Limit 노드 존재).