{ "version": "1.1", "title": "컬렉션 하나만 fetch join하면 전송 행이 자식 수만큼 늘어난다", "question": "컬렉션 하나만 fetch join했을 때 DB가 전달하는 행은 왜 늘어나는가?", "type": "data-flow", "direction": "LR", "audience": [ "JPA 조회 전략을 고르는 백엔드 개발자" ], "summary": "조인이 부모를 자식 수만큼 반복해 전송하고, 결과 리스트는 루트 중복 제거로 줄어들어 전송량이 가려진다.", "alt": "feed_items와 highlights가 fetch join으로 합쳐져 전송 조인 행이 되고, 그 행이 결과 리스트로 갈 때만 루트 엔티티가 중복 제거되는 흐름.", "long_description": "왼쪽에서 feed_items와 highlights 두 테이블이 highlights fetch join으로 들어간다. 조인은 부모 행을 자식 수만큼 반복한 전송 조인 행을 만든다. 그 행이 결과 리스트로 넘어갈 때 Hibernate가 루트 엔티티를 중복 제거하므로 리스트 크기만 줄고 전송 단계의 카테시안은 남는다.", "composition": { "profile": "component-flow", "diagram_only": true, "reference_ids": [ "payment-event-flow" ], "rationale": "두 테이블에서 조인을 거쳐 전송 행과 결과 리스트로 이어지는 방향이 있는 데이터 경로이고, 그 경로의 어느 구간에서 행이 늘고 어느 구간에서 줄어드는지가 물음이다.", "focus_node": "transferred-rows" }, "groups": [], "nodes": [ { "id": "feed-items", "label": "feed_items", "kind": "store", "role": "store", "shape": "cylinder", "description": "부모 테이블", "evidence": [ { "start_line": 713, "end_line": 714 } ], "assumption": false }, { "id": "highlights", "label": "highlights", "kind": "store", "role": "store", "shape": "cylinder", "description": "자식 테이블", "evidence": [ { "start_line": 713, "end_line": 714 } ], "assumption": false }, { "id": "fetch-join", "label": "highlights fetch join", "kind": "service", "role": "service", "shape": "box", "description": "컬렉션 하나만 fetch join한 루트 쿼리", "evidence": [ { "start_line": 713, "end_line": 715 } ], "assumption": false }, { "id": "transferred-rows", "label": "전송 조인 행", "kind": "queue", "role": "queue", "shape": "box", "details": [ "Σ highlights" ], "emphasis": "primary", "description": "DB가 애플리케이션에 전달한 조인 카디널리티", "evidence": [ { "start_line": 714, "end_line": 715 }, { "start_line": 732, "end_line": 736 } ], "assumption": false }, { "id": "result-list", "label": "결과 리스트", "kind": "sink", "role": "sink", "shape": "box", "details": [ "루트 엔티티 중복 제거" ], "description": "Hibernate 6+ 가 루트를 dedup한 뒤의 목록", "evidence": [ { "start_line": 717, "end_line": 719 } ], "assumption": false } ], "edges": [ { "id": "parent-into-join", "from": "feed-items", "to": "fetch-join", "label": "부모 행", "kind": "read", "style": "solid", "evidence": [ { "start_line": 713, "end_line": 714 } ], "assumption": false }, { "id": "child-into-join", "from": "highlights", "to": "fetch-join", "label": "자식 행", "kind": "read", "style": "solid", "evidence": [ { "start_line": 713, "end_line": 714 } ], "assumption": false }, { "id": "join-multiplies", "from": "fetch-join", "to": "transferred-rows", "label": "카테시안 곱", "kind": "transfer", "style": "solid", "evidence": [ { "start_line": 713, "end_line": 715 }, { "start_line": 732, "end_line": 734 } ], "assumption": false }, { "id": "dedup-to-list", "from": "transferred-rows", "to": "result-list", "label": "루트 dedup", "kind": "transform", "style": "dashed", "evidence": [ { "start_line": 717, "end_line": 719 } ], "assumption": false } ], "legend": [], "metadata": { "rationale": "쿼리 수가 아니라 전송 행수가 늘어난 자리를 보여야 하므로, 테이블에서 결과 리스트까지의 데이터 경로를 한 단계 추상화로 그린다." }, "source_context": { "document": "docs/n+1liner/final/document.md", "document_sha256": "385ca44db5bf763cb1ff28a67d531d68876402b35d21dc54f2bc702e8bda0053", "anchor": { "kind": "heading", "value": "9.3 실패 ② 컬렉션 하나만 fetch join → 카테시안으로 전송 행수 증가", "line": 711 } }, "id": "cartesian-row-multiplication" }