컬렉션 하나만 fetch join한 조인의 실행계획 (Fetch Join 시도 — 카테시안 행 곱)
출처: FeedPersistenceIT.l3ExplainCollectionJoinRowMultiplication 콘솔 출력
조건: seed(100) 직후. warm buffer cache(shared read=0).
쿼리: EXPLAIN (ANALYZE, BUFFERS)
      SELECT fi.id, h.id FROM feed_items fi JOIN highlights h ON h.feed_item_id = fi.id
      (fetch join `select f from FeedItemJpaEntity f join fetch f.highlights`가 발행하는 조인과 같은 shape)

Hash Join  (cost=77.18..512.34 rows=4202 width=32) (actual time=0.589..0.894 rows=1961 loops=1)
  Hash Cond: (h.feed_item_id = fi.id)
  Buffers: shared hit=450
  ->  Seq Scan on highlights h  (cost=0.00..424.02 rows=4202 width=32) (actual time=0.471..0.570 rows=1961 loops=1)
        Buffers: shared hit=382
  ->  Hash  (cost=72.08..72.08 rows=408 width=16) (actual time=0.112..0.112 rows=100 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 13kB
        Buffers: shared hit=68
        ->  Seq Scan on feed_items fi  (cost=0.00..72.08 rows=408 width=16) (actual time=0.084..0.092 rows=100 loops=1)
              Buffers: shared hit=68
Planning Time: 0.099 ms
Execution Time: 0.959 ms

관찰(문서 §9):
- §6.4는 반복되는 자식 단건 쿼리를, §7.4는 반복되는 부모 단건 쿼리를 봤다. 여기서는 조인 한 방을 본다 —
  Hash Join 노드의 actual rows=1961이 카테시안의 실체다. 부모 feed_items는 100행(Hash 노드)인데,
  조인 결과는 1,961행(= Σ highlights)으로 부푼다. 쿼리는 하나인데 그 하나가 실어 나르는 행이 곱이다.
- 이 1,961이 N2 랩(§7)의 아이템 수 100이 아니라 자식 총량(1,961)과 같다는 게 핵심 — 전송 비용이
  '왕복 수'에서 '전송 행수'로 옮겨갔다.
- Buffers: shared read=0 → warm buffer cache. cold 디스크 I/O 실행시간으로 읽지 말 것.
- Execution Time 0.959 ms는 executor 내부 시간(§6.4 caveat와 동일). 애플리케이션 지연이 아니다.
- rows=4202(추정) vs rows=1961(실제)의 오차는 대량 시드 직후 ANALYZE 미실행 탓(§6.4 Plan A와 같은 통계 이슈).
