refactor: 분리되어 관리하고 있던 문서 시스템을 하나로 통일
This commit is contained in:
@@ -238,7 +238,7 @@ highlightCount(i) = max(1, round(500 / (i+1)^1.15)) // 상한 500, 하한 1
|
||||
|
||||
Zipf의 법칙은 "순위 `r`인 항목의 빈도 ∝ `1/r^s`"이고 고전적 지프는 지수 `s=1`이라 1위가 2위의 두 배입니다. 저는 조금 더 가파르게 줄어들도록 `s=1.15`를 사용했습니다. 이때 1위는 2위의 `2^1.15≈2.2`배가 됩니다. 단어 빈도·도시 인구·웹페이지 조회 수 같은 heavy-tailed 편중이 이 계열입니다. 다만 이 분포가 실제 라이너 데이터와 같다고 주장하는 것은 아닙니다. "일부 페이지에 하이라이트가 매우 많을 수 있음"을 통제된 방식으로 재현하려고 만든 스트레스 분포입니다. `max(1, …)`로 바닥값을 두었으므로 전 구간이 순수한 멱법칙을 따르지는 않고 floor를 적용한 truncated Zipf-like 분포에 가깝습니다.
|
||||
|
||||
공식을 대입한 순위별 실제 생성 개수(원본: [`evidence/metrics/l1-skew-distribution.csv`](./evidence/metrics/l1-skew-distribution.csv)):
|
||||
공식을 대입한 순위별 실제 생성 개수(원본: [`evidence/raw/metrics/l1-skew-distribution.csv`](./evidence/raw/metrics/l1-skew-distribution.csv)):
|
||||
|
||||
| 순위(rank) | 1 | 2 | 3 | 5 | 10 | 50 | 100 | 꼬리(≈150위~) |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
@@ -553,7 +553,7 @@ EAGER의 secondary SELECT 구조가 추가 조회의 가능성을 만듭니다.
|
||||
### 6.4 각 조회는 "빠르다" — 그런데도 느리다
|
||||
|
||||
반복되는 하이라이트 조회 하나를 실행계획으로 확인했습니다. 아래는 **Plan A — 대량 시드 직후,
|
||||
`ANALYZE` 실행 전**의 계획입니다(원문: [`evidence/terminal/explain/highlights-child-plan-A.txt`](./evidence/terminal/explain/highlights-child-plan-A.txt)).
|
||||
`ANALYZE` 실행 전**의 계획입니다(원문: [`evidence/raw/explain/highlights-child-plan-A.txt`](./evidence/raw/explain/highlights-child-plan-A.txt)).
|
||||
|
||||
```text
|
||||
Index Scan using ix_highlights_feed_items_created on highlights
|
||||
@@ -634,7 +634,7 @@ EXPLAIN 수치를 읽을 때 주의할 두 가지가 더 있습니다.
|
||||
| 100 | **100** | 20 | 120 | 100 | 222 |
|
||||
| 1,000 | **1,000** | 20 | 1,020 | 1,000 | 2,022 |
|
||||
|
||||
성격: 측정값(직접) — 출처 `FeedPersistenceIT.l2ToOneEagerHiddenNPlusOneCurve`(콘솔 `>>> LAB L2 [eager toOne curve …]`, 리포트 `app-bootstrap/build/lab-results/feed-nplus1.md`). 원본: [`evidence/metrics/l2-toone-split.csv`](./evidence/metrics/l2-toone-split.csv).
|
||||
성격: 측정값(직접) — 출처 `FeedPersistenceIT.l2ToOneEagerHiddenNPlusOneCurve`(콘솔 `>>> LAB L2 [eager toOne curve …]`, 리포트 `app-bootstrap/build/lab-results/feed-nplus1.md`). 원본: [`evidence/raw/metrics/l2-toone-split.csv`](./evidence/raw/metrics/l2-toone-split.csv).
|
||||
|
||||
검산(6.3절 파생과 일치): `entityFetch = pageFetch + userFetch` → `10+3=13` · `100+20=120` · `1000+20=1020` ✓. 회계 항등식으로도 `총 PreparedStatement − 컬렉션 N − content(1) − count(1) = entityFetch` → `25−10−2=13` · `222−100−2=120` · `2022−1000−2=1020` ✓. 앞서 6.3절에서 역산했던 13 / 120 / 1,020을 직접 측정이 그대로 재현했습니다 — 파생 예측이 실측으로 확정됐습니다.
|
||||
|
||||
@@ -669,7 +669,7 @@ EAGER 기본값 때문에 생긴 N+1이었습니다. 같은 조건에서 LAZY
|
||||
|
||||
6.4절에서 자식 컬렉션 쿼리를 확인한 것처럼, 이번에는 N2를 만드는 **반복되는 ToOne 부모 쿼리**
|
||||
(`SELECT * FROM pages WHERE id = ?`, `… FROM users WHERE id = ?`)를 실행계획으로
|
||||
확인했습니다. 아래는 seed(100) 직후의 계획입니다(원문: [`evidence/terminal/explain/toone-pages-plan.txt`](./evidence/terminal/explain/toone-pages-plan.txt) · [`toone-users-plan.txt`](./evidence/terminal/explain/toone-users-plan.txt)).
|
||||
확인했습니다. 아래는 seed(100) 직후의 계획입니다(원문: [`evidence/raw/explain/toone-pages-plan.txt`](./evidence/raw/explain/toone-pages-plan.txt) · [`toone-users-plan.txt`](./evidence/raw/explain/toone-users-plan.txt)).
|
||||
|
||||
```text
|
||||
-- pages
|
||||
@@ -805,7 +805,7 @@ java.lang.IllegalArgumentException <- org.hibernate.loader.MultipleBagFetchExcep
|
||||
> `SELECT count(*) FROM feed_items fi JOIN highlights h ON h.feed_item_id = fi.id`를
|
||||
> 측정했습니다. 이 문제는 EXPLAIN actual rows나 조인 count로 확인해야 합니다.
|
||||
|
||||
**측정값(직접 측정).** 출처 `FeedPersistenceIT.l3SingleCollectionFetchJoinExplodesTransferredRows`(N=10/100/1000). 원본: [`evidence/metrics/l3-cartesian.csv`](./evidence/metrics/l3-cartesian.csv).
|
||||
**측정값(직접 측정).** 출처 `FeedPersistenceIT.l3SingleCollectionFetchJoinExplodesTransferredRows`(N=10/100/1000). 원본: [`evidence/raw/metrics/l3-cartesian.csv`](./evidence/raw/metrics/l3-cartesian.csv).
|
||||
|
||||
| N | 전송 행수(★조인 카디널리티) | 리스트 크기(Hib6 dedup) | distinct 아이템 | 시드 하이라이트 | 폭발 배수 | 총 PreparedStatement |
|
||||
|---:|---:|---:|---:|---:|---:|---:|
|
||||
@@ -842,7 +842,7 @@ fetch join한 쿼리는 **121개**였습니다. 쿼리 수만 보면 개선처
|
||||
|
||||
앞서 6.4절에서는 반복되는 자식 단건 쿼리를, 7.4절에서는 부모 단건 쿼리를 확인했습니다. 이번
|
||||
차례는 fetch join이 만든 조인 하나입니다. 아래는 seed(100) 직후 같은 형태의 쿼리를
|
||||
EXPLAIN한 결과입니다(원문: [`evidence/terminal/explain/l3-cartesian-join-plan.txt`](./evidence/terminal/explain/l3-cartesian-join-plan.txt)).
|
||||
EXPLAIN한 결과입니다(원문: [`evidence/raw/explain/l3-cartesian-join-plan.txt`](./evidence/raw/explain/l3-cartesian-join-plan.txt)).
|
||||
|
||||
```text
|
||||
Hash Join (cost=77.18..512.34 rows=4202 width=32) (actual time=0.589..0.894 rows=1961 loops=1)
|
||||
@@ -906,7 +906,7 @@ join한 상태에서 페이징을 적용해 보았습니다.
|
||||
**N개 전부** 로드되었습니다. `EntityStatistics.getLoadCount()`로 FeedItem 로드 수를 따로
|
||||
읽어 응답 크기와 실제 적재량을 비교했습니다.
|
||||
|
||||
**측정값(직접 측정·파생).** `returned`·`feedItemLoaded`는 결정적(리스트 크기·Hibernate 통계로 확정), over-fetch 배수는 `feedItemLoaded / returned`로 파생합니다. 출처 `FeedPersistenceIT.l4CollectionFetchJoinPagingLoadsWholeDatasetInMemory`. 원본: [`evidence/metrics/l4-inmemory-paging.csv`](./evidence/metrics/l4-inmemory-paging.csv).
|
||||
**측정값(직접 측정·파생).** `returned`·`feedItemLoaded`는 결정적(리스트 크기·Hibernate 통계로 확정), over-fetch 배수는 `feedItemLoaded / returned`로 파생합니다. 출처 `FeedPersistenceIT.l4CollectionFetchJoinPagingLoadsWholeDatasetInMemory`. 원본: [`evidence/raw/metrics/l4-inmemory-paging.csv`](./evidence/raw/metrics/l4-inmemory-paging.csv).
|
||||
|
||||
| N | returned(페이지) | feedItemLoaded(★ = N) | over-fetch 배수 | 시드 하이라이트 |
|
||||
|---:|---:|---:|---:|---:|
|
||||
@@ -942,7 +942,7 @@ join한 상태에서 페이징을 적용해 보았습니다.
|
||||
|
||||
응답은 한 페이지인데 비용은 N에 비례하는지 측정했습니다. 아래 값은 문서 첫머리에서 밝힌 대로
|
||||
**단일 스레드·warm-cache 상대값**입니다. 절대값이 아니라 N에 따른 변화 방향만 비교했습니다
|
||||
(원본: [`evidence/metrics/l4-cost-curve.csv`](./evidence/metrics/l4-cost-curve.csv)).
|
||||
(원본: [`evidence/raw/metrics/l4-cost-curve.csv`](./evidence/raw/metrics/l4-cost-curve.csv)).
|
||||
|
||||
| N | 지연 중앙값(5회) | 지연 최댓값(5회) | 스레드 누적 할당 |
|
||||
|---:|---:|---:|---:|
|
||||
@@ -972,7 +972,7 @@ join한 상태에서 페이징을 적용해 보았습니다.
|
||||
### 10.4 발행 SQL엔 LIMIT이 없다 — 인메모리 페이징의 스모킹건
|
||||
|
||||
인메모리 페이징을 실행계획에서도 확인했습니다. fetch join이 발행한 SQL(a)과 엔티티만 페이징한
|
||||
SQL(b)을 seed(100)에서 EXPLAIN으로 비교했습니다(원문: [`evidence/terminal/explain/l4-collection-join-no-limit.txt`](./evidence/terminal/explain/l4-collection-join-no-limit.txt) · [`l4-entity-paging-limit.txt`](./evidence/terminal/explain/l4-entity-paging-limit.txt)).
|
||||
SQL(b)을 seed(100)에서 EXPLAIN으로 비교했습니다(원문: [`evidence/raw/explain/l4-collection-join-no-limit.txt`](./evidence/raw/explain/l4-collection-join-no-limit.txt) · [`l4-entity-paging-limit.txt`](./evidence/raw/explain/l4-entity-paging-limit.txt)).
|
||||
|
||||
```text
|
||||
-- (a) 컬렉션 fetch join의 조인 — Limit 노드 없음
|
||||
@@ -1035,7 +1035,7 @@ LAZY 연관에 접근합니다. 앞서 N+1을 만들었던 그 코드가 이 설
|
||||
|
||||
### 11.2 실측 — 배치 적용 전후의 쿼리 수
|
||||
|
||||
`loadFeed(0, n)`(기준선과 정확히 같은 호출)을 배치 세션에서 재면 SQL 총량이 순진의 `1+N`에서 급감합니다. before = 기준선 실측, after = `FeedBatchFetchIT.l5BatchFetchCollapsesQueryCount`. 원본: [`evidence/metrics/l5-batch-resolution.csv`](./evidence/metrics/l5-batch-resolution.csv).
|
||||
`loadFeed(0, n)`(기준선과 정확히 같은 호출)을 배치 세션에서 재면 SQL 총량이 순진의 `1+N`에서 급감합니다. before = 기준선 실측, after = `FeedBatchFetchIT.l5BatchFetchCollapsesQueryCount`. 원본: [`evidence/raw/metrics/l5-batch-resolution.csv`](./evidence/raw/metrics/l5-batch-resolution.csv).
|
||||
|
||||
| N | before: 순진 총 PreparedStatement | after: 배치 총 PreparedStatement | 붕괴 | before: 컬렉션 fetch | after: 컬렉션 fetch |
|
||||
|---:|---:|---:|---:|---:|---:|
|
||||
@@ -1069,7 +1069,7 @@ fetch join에서는 `feedItemLoaded`가 N까지 늘었지만 배치 적용 뒤
|
||||
|
||||
### 11.4 EXPLAIN — 페이징엔 Limit 노드, 배치 IN엔 곱셈 없음 (카테시안·인메모리 페이징 둘 다 해소)
|
||||
|
||||
앞 절의 스모킹건은 "(a) fetch join 조인 SQL엔 Limit 노드가 없다"였습니다. 이번에는 정반대 — 엔티티만 페이징하니 Limit 노드가 붙고 자식은 `IN` 배치라 행을 안 곱한다(seed(100), 원문: [`evidence/terminal/explain/l5-entity-paging-limit.txt`](./evidence/terminal/explain/l5-entity-paging-limit.txt) · [`l5-batch-in-semijoin.txt`](./evidence/terminal/explain/l5-batch-in-semijoin.txt)).
|
||||
앞 절의 스모킹건은 "(a) fetch join 조인 SQL엔 Limit 노드가 없다"였습니다. 이번에는 정반대 — 엔티티만 페이징하니 Limit 노드가 붙고 자식은 `IN` 배치라 행을 안 곱한다(seed(100), 원문: [`evidence/raw/explain/l5-entity-paging-limit.txt`](./evidence/raw/explain/l5-entity-paging-limit.txt) · [`l5-batch-in-semijoin.txt`](./evidence/raw/explain/l5-batch-in-semijoin.txt)).
|
||||
|
||||
```text
|
||||
-- (a) 엔티티만 페이징 — Limit 노드 존재 (fetch join 조인엔 없었다)
|
||||
@@ -1102,7 +1102,7 @@ join 대신 배치를 사용하기로 했습니다.
|
||||
배치로 쿼리 수와 페이징 문제는 풀었지만 엔티티는 여전히 통째로 하이드레이트했습니다.
|
||||
`FeedBatchFetchIT.l5ProbeBatchStillHydratesFullEntities`에서 seed 1,000의 첫 페이지 20건을
|
||||
조회하자 FeedItem·User·Page·Highlight를 합해 **1,569개 엔티티**가 영속 객체로 올라왔습니다
|
||||
(원본: [`evidence/metrics/l5-hydration-probe.csv`](./evidence/metrics/l5-hydration-probe.csv)).
|
||||
(원본: [`evidence/raw/metrics/l5-hydration-probe.csv`](./evidence/raw/metrics/l5-hydration-probe.csv)).
|
||||
화면에는 일부 컬럼만 필요했으므로 다음에는 DTO 프로젝션으로 적재 대상을 줄였습니다.
|
||||
|
||||
---
|
||||
@@ -1142,7 +1142,7 @@ CQRS-lite 계약으로 노출합니다.
|
||||
|
||||
seed 1,000에서 `loadFeedProjection(0, 20)`을 실행하고 앞 절의 배치 조회와 비교했습니다.
|
||||
프로젝션은 하이드레이트한 엔티티가 0개였습니다. 원본:
|
||||
[`evidence/metrics/l6-projection-resolution.csv`](./evidence/metrics/l6-projection-resolution.csv).
|
||||
[`evidence/raw/metrics/l6-projection-resolution.csv`](./evidence/raw/metrics/l6-projection-resolution.csv).
|
||||
|
||||
| 지표 | before: 배치 | after: 프로젝션 |
|
||||
|---|---:|---:|
|
||||
@@ -1172,7 +1172,7 @@ N을 10, 100, 1,000으로 바꿔 다시 측정해도 prepared는 **항상 2개**
|
||||
|
||||
### 12.4 EXPLAIN — Limit·semi-join은 있으나 width는 좁아지지 않는다 (★ 실측 정정)
|
||||
|
||||
앞서 11절의 D2는 "엔티티 페이징엔 Limit 노드"였습니다. 프로젝션도 (a) 부모 페이징에 `Limit`이 있고 (b) 자식 IN은 semi-join이라 행을 안 곱한다(원문: [`evidence/terminal/explain/l6-parent-projection.txt`](./evidence/terminal/explain/l6-parent-projection.txt) · [`l6-child-projection.txt`](./evidence/terminal/explain/l6-child-projection.txt)).
|
||||
앞서 11절의 D2는 "엔티티 페이징엔 Limit 노드"였습니다. 프로젝션도 (a) 부모 페이징에 `Limit`이 있고 (b) 자식 IN은 semi-join이라 행을 안 곱한다(원문: [`evidence/raw/explain/l6-parent-projection.txt`](./evidence/raw/explain/l6-parent-projection.txt) · [`l6-child-projection.txt`](./evidence/raw/explain/l6-child-projection.txt)).
|
||||
|
||||
```text
|
||||
-- (a) 부모 스칼라 프로젝션 — Limit 존재하나 width=2088 (users·pages 조인이 행폭에 흘러든다)
|
||||
@@ -1187,7 +1187,7 @@ Hash Semi Join (... rows=1509 loops=1) ← 페이지 20 부모의 하이라
|
||||
|
||||
> **★ 실측 정정** — 필요한 컬럼만 선택하면 EXPLAIN의 `width`도 줄어들 것으로 예상했지만
|
||||
> 부모 프로젝션의 width는 2088로 엔티티 조회의 1194보다 컸습니다(원본:
|
||||
> [`evidence/metrics/l6-explain-width.csv`](./evidence/metrics/l6-explain-width.csv)).
|
||||
> [`evidence/raw/metrics/l6-explain-width.csv`](./evidence/raw/metrics/l6-explain-width.csv)).
|
||||
> `users`와 `pages` 조인의 행폭이 반영되고 PostgreSQL의 `width`가 실제 전송 바이트가 아니라
|
||||
> 컬럼 타입의 평균폭 추정치이기 때문입니다. 프로젝션의 효과는 SQL 플랜의 width가 아니라
|
||||
> `Statistics.getEntityLoadCount()`에서 확인했습니다.
|
||||
@@ -1204,7 +1204,7 @@ Carrier(f.id, u.name, …)`는 영속 엔티티를 만들지 않으므로 1차
|
||||
|
||||
프로젝션은 엔티티 과적재를 없앴지만 자식 IN 쿼리는 페이지 부모의 하이라이트를 **전부**
|
||||
가져왔습니다. seed 1,000의 첫 페이지 20건에서 자식 행은 1,509개였습니다(원본:
|
||||
[`evidence/metrics/l6-projection-resolution.csv`](./evidence/metrics/l6-projection-resolution.csv)).
|
||||
[`evidence/raw/metrics/l6-projection-resolution.csv`](./evidence/raw/metrics/l6-projection-resolution.csv)).
|
||||
화면에는 부모당 최신 3개, 최대 60개만 필요했습니다. 단순한 `IN` 쿼리의 `LIMIT`은 부모별로
|
||||
적용되지 않으므로 다음 단계에서 Top-N-per-group을 SQL로 구현했습니다.
|
||||
|
||||
@@ -1248,7 +1248,7 @@ SELECT h.feed_item_id, h.color, h.text, h.created_at FROM highlights h
|
||||
|
||||
### 13.2 실측 — 세 방법의 결과와 단순 LIMIT의 오작동
|
||||
|
||||
`FeedTopNIT.l14ThreeStrategiesReturnTopThreePerParentAndNaiveLimitIsWrong`·`l14TransferAcrossStrategies`(seed 1,000, page 20). 원본: [`evidence/metrics/l14-topn-resolution.csv`](./evidence/metrics/l14-topn-resolution.csv).
|
||||
`FeedTopNIT.l14ThreeStrategiesReturnTopThreePerParentAndNaiveLimitIsWrong`·`l14TransferAcrossStrategies`(seed 1,000, page 20). 원본: [`evidence/raw/metrics/l14-topn-resolution.csv`](./evidence/raw/metrics/l14-topn-resolution.csv).
|
||||
|
||||
| 전략 | 반환 행 | 커버한 부모 | 부모당 최대 |
|
||||
|---|---:|---:|---:|
|
||||
@@ -1264,10 +1264,10 @@ SELECT h.feed_item_id, h.color, h.text, h.created_at FROM highlights h
|
||||
### 13.3 결과는 같지만 I/O는 달랐다
|
||||
|
||||
세 SQL은 캐시 상태를 맞추기 위해 같은 테스트 실행에서 `EXPLAIN (ANALYZE, BUFFERS)`로
|
||||
측정했습니다. 원문: [`l14-window-plan.txt`](./evidence/terminal/explain/l14-window-plan.txt) ·
|
||||
[`l14-lateral-plan.txt`](./evidence/terminal/explain/l14-lateral-plan.txt) ·
|
||||
[`l14-twostep-plan.txt`](./evidence/terminal/explain/l14-twostep-plan.txt). 요약:
|
||||
[`evidence/metrics/l14-plan-compare.csv`](./evidence/metrics/l14-plan-compare.csv).
|
||||
측정했습니다. 원문: [`l14-window-plan.txt`](./evidence/raw/explain/l14-window-plan.txt) ·
|
||||
[`l14-lateral-plan.txt`](./evidence/raw/explain/l14-lateral-plan.txt) ·
|
||||
[`l14-twostep-plan.txt`](./evidence/raw/explain/l14-twostep-plan.txt). 요약:
|
||||
[`evidence/raw/metrics/l14-plan-compare.csv`](./evidence/raw/metrics/l14-plan-compare.csv).
|
||||
|
||||
| 전략 | 최상위 노드 (스캔·조인) | 반환 행 | buffers shared hit | exec |
|
||||
|---|---|---:|---:|---:|
|
||||
@@ -1297,8 +1297,8 @@ WindowAgg Run Condition: (row_number() OVER (?) <= 3) Buffers: shared hit=43
|
||||
|
||||
LATERAL의 buffers가 작은 이유가 복합 인덱스인지 확인했습니다. 같은 쿼리를 두고 인덱스를
|
||||
제거한 뒤 다시 만들면서 측정했습니다. 원본:
|
||||
[`l14-lateral-no-index.txt`](./evidence/terminal/explain/l14-lateral-no-index.txt) ·
|
||||
[`evidence/metrics/l14-index-toggle.csv`](./evidence/metrics/l14-index-toggle.csv).
|
||||
[`l14-lateral-no-index.txt`](./evidence/raw/explain/l14-lateral-no-index.txt) ·
|
||||
[`evidence/raw/metrics/l14-index-toggle.csv`](./evidence/raw/metrics/l14-index-toggle.csv).
|
||||
|
||||
| variant | 자식 접근 | buffers shared hit | exec |
|
||||
|---|---|---:|---:|
|
||||
@@ -1314,7 +1314,7 @@ buffers는 168에서 4,446으로 약 26배, 실행시간은 0.336 ms에서 5.472
|
||||
### 13.5 그룹 크기가 승자를 가른다 — K 곡선
|
||||
|
||||
세 방식의 차이가 그룹 크기에 따라 달라지는지도 확인했습니다. seed 1,000에서 top-K를
|
||||
3·50·500으로 바꿔 측정했습니다(원본: [`evidence/metrics/l14-group-size.csv`](./evidence/metrics/l14-group-size.csv)).
|
||||
3·50·500으로 바꿔 측정했습니다(원본: [`evidence/raw/metrics/l14-group-size.csv`](./evidence/raw/metrics/l14-group-size.csv)).
|
||||
|
||||
| K | 윈도우 반환 | 윈도우 buffers | LATERAL 반환 | LATERAL buffers |
|
||||
|---:|---:|---:|---:|---:|
|
||||
@@ -1377,7 +1377,7 @@ SELECT fi.id, fi.first_highlighted_at FROM feed_items fi
|
||||
### 14.2 실측 — OFFSET은 깊이에 비례하고 keyset은 일정하다
|
||||
|
||||
seed 2,000에서 두 방식에 같은 정렬키 인덱스를 사용했습니다. "훑은 행"은 `Limit` 하위의
|
||||
actual rows로 계산했습니다(원본: [`evidence/metrics/l15-depth-curve.csv`](./evidence/metrics/l15-depth-curve.csv)).
|
||||
actual rows로 계산했습니다(원본: [`evidence/raw/metrics/l15-depth-curve.csv`](./evidence/raw/metrics/l15-depth-curve.csv)).
|
||||
|
||||
| 페이지 (offset) | OFFSET 훑은 행 | keyset 훑은 행 |
|
||||
|---:|---:|---:|
|
||||
@@ -1391,7 +1391,7 @@ keyset은 20행만 읽었습니다. 무한 스크롤의 뒤쪽 페이지가 느
|
||||
|
||||
### 14.3 EXPLAIN — scan-then-discard vs index seek, 그리고 정렬키 인덱스가 전제
|
||||
|
||||
`FeedKeysetIT.l15ExplainOffsetScansThenDiscardsKeysetSeeksAndNeedsIndex`(깊은 페이지 offset 1980, 한 실행). 원문: [`l15-offset-deep-page.txt`](./evidence/terminal/explain/l15-offset-deep-page.txt) · [`l15-keyset-index-seek.txt`](./evidence/terminal/explain/l15-keyset-index-seek.txt) · [`l15-keyset-no-index.txt`](./evidence/terminal/explain/l15-keyset-no-index.txt). 요약: [`evidence/metrics/l15-deep-page-compare.csv`](./evidence/metrics/l15-deep-page-compare.csv).
|
||||
`FeedKeysetIT.l15ExplainOffsetScansThenDiscardsKeysetSeeksAndNeedsIndex`(깊은 페이지 offset 1980, 한 실행). 원문: [`l15-offset-deep-page.txt`](./evidence/raw/explain/l15-offset-deep-page.txt) · [`l15-keyset-index-seek.txt`](./evidence/raw/explain/l15-keyset-index-seek.txt) · [`l15-keyset-no-index.txt`](./evidence/raw/explain/l15-keyset-no-index.txt). 요약: [`evidence/raw/metrics/l15-deep-page-compare.csv`](./evidence/raw/metrics/l15-deep-page-compare.csv).
|
||||
|
||||
| 변형 | 플랜 | 훑은 행 | buffers | exec |
|
||||
|---|---|---:|---:|---:|
|
||||
@@ -1426,7 +1426,7 @@ range scan합니다. `first_highlighted_at`이 같은 행도 안정적으로 넘
|
||||
|
||||
### 14.5 keyset이 못 푸는 것 — 가시성 OR
|
||||
|
||||
keyset은 페이지 깊이를 풀었지만 실서비스 피드는 가시성으로 필터해야 한다(`public` + 내가 멘션된 것 + 내 비공개). 그 필터를 keyset과 같은 쿼리에 얹으면(`FeedKeysetIT.l15ProbeVisibilityOrBreaksKeysetIndex`) 플래너는 정렬키 인덱스 `ix_feed_items_keyset`를 **더 이상 쓰지 못하고** 가시성 3분기를 각각 인덱스로 스캔한 `BitmapOr`로 떨어집니다. 원문: [`l15-visibility-or-probe.txt`](./evidence/terminal/explain/l15-visibility-or-probe.txt).
|
||||
keyset은 페이지 깊이를 풀었지만 실서비스 피드는 가시성으로 필터해야 한다(`public` + 내가 멘션된 것 + 내 비공개). 그 필터를 keyset과 같은 쿼리에 얹으면(`FeedKeysetIT.l15ProbeVisibilityOrBreaksKeysetIndex`) 플래너는 정렬키 인덱스 `ix_feed_items_keyset`를 **더 이상 쓰지 못하고** 가시성 3분기를 각각 인덱스로 스캔한 `BitmapOr`로 떨어집니다. 원문: [`l15-visibility-or-probe.txt`](./evidence/raw/explain/l15-visibility-or-probe.txt).
|
||||
|
||||
```text
|
||||
-- 가시성 OR 을 얹으면: 정렬키 Index Only Scan 이 사라지고 BitmapOr + 별도 Sort 로
|
||||
@@ -1477,7 +1477,7 @@ SELECT fi.id, fi.first_highlighted_at FROM feed_items fi
|
||||
seed 2,000에서 user008이 볼 수 있는 피드를 조회했습니다. 세 방식이 같은 20개 feed_item을
|
||||
반환하는지는 `l16ThreeApproachesReturnSameVisibleSet`으로 먼저 확인한 뒤 가시성 조건을
|
||||
처리하는 실행계획을 비교했습니다(원본:
|
||||
[`evidence/metrics/l16-plan-compare.csv`](./evidence/metrics/l16-plan-compare.csv)).
|
||||
[`evidence/raw/metrics/l16-plan-compare.csv`](./evidence/raw/metrics/l16-plan-compare.csv)).
|
||||
|
||||
| 안 | 최상위/스캔 | Sort | 멘션 | 훑는 후보 | buffers |
|
||||
|---|---|---|---|---:|---:|
|
||||
@@ -1503,7 +1503,7 @@ Limit -> Merge Append
|
||||
Limit -> Index Only Scan using ix_feed_visible (Index Cond: viewer_id=:me) Heap Fetches: 20
|
||||
```
|
||||
|
||||
원문: [`l16-single-or-plan.txt`](./evidence/terminal/explain/l16-single-or-plan.txt) · [`l16-union-decompose-plan.txt`](./evidence/terminal/explain/l16-union-decompose-plan.txt) · [`l16-precompute-plan.txt`](./evidence/terminal/explain/l16-precompute-plan.txt) · [`l16-union-branches.txt`](./evidence/terminal/explain/l16-union-branches.txt).
|
||||
원문: [`l16-single-or-plan.txt`](./evidence/raw/explain/l16-single-or-plan.txt) · [`l16-union-decompose-plan.txt`](./evidence/raw/explain/l16-union-decompose-plan.txt) · [`l16-precompute-plan.txt`](./evidence/raw/explain/l16-precompute-plan.txt) · [`l16-union-branches.txt`](./evidence/raw/explain/l16-union-branches.txt).
|
||||
|
||||
### 15.4 UNION과 사전계산의 차이
|
||||
|
||||
@@ -1552,7 +1552,7 @@ SELECT p.pid, top3.color, top3.text, top3.created_at
|
||||
|
||||
### 16.2 실측 — 세 기법을 합친 실행계획
|
||||
|
||||
`FeedCrownIT.crownUnifiedPlanStacksVisibilityKeysetAndTopN`(seed 2,000, 뷰어 user008, page 1). 사전계산 부모선택 위의 통합 쿼리는 세 기법을 재정렬 없이 한 플랜에 겹칩니다. 원본: [`crown-unified-precompute-plan.txt`](./evidence/terminal/explain/crown-unified-precompute-plan.txt).
|
||||
`FeedCrownIT.crownUnifiedPlanStacksVisibilityKeysetAndTopN`(seed 2,000, 뷰어 user008, page 1). 사전계산 부모선택 위의 통합 쿼리는 세 기법을 재정렬 없이 한 플랜에 겹칩니다. 원본: [`crown-unified-precompute-plan.txt`](./evidence/raw/explain/crown-unified-precompute-plan.txt).
|
||||
|
||||
```text
|
||||
Nested Loop (rows=60) ← LATERAL (상관 조인)
|
||||
@@ -1568,7 +1568,7 @@ Nested Loop (rows=60) ← LATERAL
|
||||
|
||||
### 16.3 간섭 시험 — 사전계산 위에선 겹치고, 단일 OR 위에선 매 페이지 재해소
|
||||
|
||||
`crownDeepPageKeysetSeeksFewerRowsWithPrecomputeThanSingleOr`(가장 깊은 페이지, 커서 = visible−20). user008에게 보이는 `1,500` 중 마지막 페이지에서 부모선택을 사전계산으로 두느냐 단일 OR로 두느냐가 갈립니다. 원본: [`crown-deep-keyset-precompute.txt`](./evidence/terminal/explain/crown-deep-keyset-precompute.txt) · [`crown-deep-keyset-single-or.txt`](./evidence/terminal/explain/crown-deep-keyset-single-or.txt).
|
||||
`crownDeepPageKeysetSeeksFewerRowsWithPrecomputeThanSingleOr`(가장 깊은 페이지, 커서 = visible−20). user008에게 보이는 `1,500` 중 마지막 페이지에서 부모선택을 사전계산으로 두느냐 단일 OR로 두느냐가 갈립니다. 원본: [`crown-deep-keyset-precompute.txt`](./evidence/raw/explain/crown-deep-keyset-precompute.txt) · [`crown-deep-keyset-single-or.txt`](./evidence/raw/explain/crown-deep-keyset-single-or.txt).
|
||||
|
||||
| 부모선택 | 최상위 | 훑는 행 | feed_visible | 부모 buffers |
|
||||
|---|---|---:|---|---:|
|
||||
@@ -1673,76 +1673,76 @@ cd src
|
||||
```
|
||||
|
||||
- 곡선(N1): `l1CollectionNPlusOneGrowsLinearlyWithN` (N=10/100/1000), `collectionFetches == N` 확인.
|
||||
- 실행계획(N1): `l1ExplainRepeatedHighlightChildQuery`, 반복되는 하이라이트 조회의 Index Scan 확인(→ [`evidence/terminal/explain/highlights-child-plan-A.txt`](./evidence/terminal/explain/highlights-child-plan-A.txt)).
|
||||
- 실행계획(N1): `l1ExplainRepeatedHighlightChildQuery`, 반복되는 하이라이트 조회의 Index Scan 확인(→ [`evidence/raw/explain/highlights-child-plan-A.txt`](./evidence/raw/explain/highlights-child-plan-A.txt)).
|
||||
- 곡선(N2): `l2ToOneEagerHiddenNPlusOneCurve` (N=10/100/1000), `pageFetch == N`(선형)·`userFetch ≤ 20`(평탄)·`entityFetch == pageFetch + userFetch` 확인.
|
||||
- 접근 0 증명(N2): `l2EagerToOneFiresEvenWithZeroFieldAccess`, 접근 0인데 `pageFetch == 100`·`collectionFetch == 0`(EAGER는 나가고 LAZY는 안 나감).
|
||||
- 실행계획(N2): `l2ExplainRepeatedPageToOneQuery`, pages·users의 pk Index Scan 확인(→ [`evidence/terminal/explain/toone-pages-plan.txt`](./evidence/terminal/explain/toone-pages-plan.txt) · [`toone-users-plan.txt`](./evidence/terminal/explain/toone-users-plan.txt)).
|
||||
- 실행계획(N2): `l2ExplainRepeatedPageToOneQuery`, pages·users의 pk Index Scan 확인(→ [`evidence/raw/explain/toone-pages-plan.txt`](./evidence/raw/explain/toone-pages-plan.txt) · [`toone-users-plan.txt`](./evidence/raw/explain/toone-users-plan.txt)).
|
||||
- 다중 컬렉션 실패(9절): `l3TwoBagFetchJoinThrowsMultipleBagFetchException`, 두 bag 동시 fetch join이 `MultipleBagFetchException`(`IllegalArgumentException`으로 래핑)을 던지는 것 확인.
|
||||
- 카테시안(9절): `l3SingleCollectionFetchJoinExplodesTransferredRows` (N=10/100/1000), 리스트 크기 = N(Hibernate 6+ dedup)인데 조인 카디널리티 = Σ highlights로 폭발하는 것 확인(→ [`evidence/metrics/l3-cartesian.csv`](./evidence/metrics/l3-cartesian.csv)).
|
||||
- 실행계획(9절): `l3ExplainCollectionJoinRowMultiplication`, 조인(Hash Join) 노드 actual rows = Σ highlights 확인(→ [`evidence/terminal/explain/l3-cartesian-join-plan.txt`](./evidence/terminal/explain/l3-cartesian-join-plan.txt)).
|
||||
- 인메모리 페이징(10절): `l4CollectionFetchJoinPagingLoadsWholeDatasetInMemory` (N=10/100/1000), `returned == min(20, N)`인데 `feedItemLoaded == N`(전체 로드)임을 확인(→ [`evidence/metrics/l4-inmemory-paging.csv`](./evidence/metrics/l4-inmemory-paging.csv)).
|
||||
- 카테시안(9절): `l3SingleCollectionFetchJoinExplodesTransferredRows` (N=10/100/1000), 리스트 크기 = N(Hibernate 6+ dedup)인데 조인 카디널리티 = Σ highlights로 폭발하는 것 확인(→ [`evidence/raw/metrics/l3-cartesian.csv`](./evidence/raw/metrics/l3-cartesian.csv)).
|
||||
- 실행계획(9절): `l3ExplainCollectionJoinRowMultiplication`, 조인(Hash Join) 노드 actual rows = Σ highlights 확인(→ [`evidence/raw/explain/l3-cartesian-join-plan.txt`](./evidence/raw/explain/l3-cartesian-join-plan.txt)).
|
||||
- 인메모리 페이징(10절): `l4CollectionFetchJoinPagingLoadsWholeDatasetInMemory` (N=10/100/1000), `returned == min(20, N)`인데 `feedItemLoaded == N`(전체 로드)임을 확인(→ [`evidence/raw/metrics/l4-inmemory-paging.csv`](./evidence/raw/metrics/l4-inmemory-paging.csv)).
|
||||
- HHH000104 경고(10절): `l4EmitsHhh000104InMemoryPagingWarning`, `HHH90003004: ... collection fetch; applying in memory` WARN을 ListAppender로 캡처(코드 번호가 아니라 문구로 매칭).
|
||||
- EXPLAIN 대조(10절): `l4ExplainCollectionJoinHasNoLimitButEntityPagingDoes`, (a) 조인 SQL엔 Limit 노드 없음 / (b) 엔티티 페이징엔 있음 확인(→ [`evidence/terminal/explain/l4-collection-join-no-limit.txt`](./evidence/terminal/explain/l4-collection-join-no-limit.txt) · [`l4-entity-paging-limit.txt`](./evidence/terminal/explain/l4-entity-paging-limit.txt)).
|
||||
- 배치 해결(11절): `FeedBatchFetchIT`(신규, 격리 클래스 `default_batch_fetch_size=100`) `l5BatchFetchCollapsesQueryCount` (N=10/100/1000), `prepared < N`(순진 `1+N`에서 붕괴)·`collectionFetch == ceil(N/batch)` 확인(→ [`evidence/metrics/l5-batch-resolution.csv`](./evidence/metrics/l5-batch-resolution.csv)).
|
||||
- 페이징 정상(11절): `l5EntityPagingLoadsOnlyThePageNotWholeDataset`, `feedItemLoaded == min(20, N)`(10절 over-fetch 소멸). EXPLAIN `l5ExplainEntityPagingHasLimitAndBatchInHasNoRowMultiplication`, (a) 엔티티 페이징엔 Limit 노드 존재 / (b) 배치 IN은 semi-join(행 안 곱함)(→ [`evidence/terminal/explain/l5-entity-paging-limit.txt`](./evidence/terminal/explain/l5-entity-paging-limit.txt) · [`l5-batch-in-semijoin.txt`](./evidence/terminal/explain/l5-batch-in-semijoin.txt)).
|
||||
- 잔여 비용(11절): `l5ProbeBatchStillHydratesFullEntities`, 페이지 20건인데 `entitiesLoaded == 1,569`(엔티티 과적재 → 프로젝션 단계)(→ [`evidence/metrics/l5-hydration-probe.csv`](./evidence/metrics/l5-hydration-probe.csv)).
|
||||
- 프로젝션 해결(12절): `FeedProjectionIT`(신규, 격리 클래스, 배치 설정 없음) `l6ProjectionHydratesZeroEntities` (N=10/100/1000), `entitiesLoaded == 0`(11절의 1,569 소멸)·`prepared == 2`(N 무관 상수)·`collectionFetch == 0` 확인. 형태 동치 `l6ProjectionReturnsSameShapeAsNaiveLoadFeed`(프로젝션 vs 순진 loadFeed 같은 결과)(→ [`evidence/metrics/l6-projection-resolution.csv`](./evidence/metrics/l6-projection-resolution.csv)).
|
||||
- EXPLAIN·width 정정(12절): `l6ExplainProjectionHasLimitAndSemiJoinNotNarrowerWidth`, (a) 부모 프로젝션 Limit 노드 존재하나 width 안 좁아짐(2088 > 엔티티 1194) / (b) 자식 IN semi-join(행 안 곱함). 프로젝션 이득은 EXPLAIN 아니라 ORM 층(→ [`evidence/terminal/explain/l6-parent-projection.txt`](./evidence/terminal/explain/l6-parent-projection.txt) · [`l6-child-projection.txt`](./evidence/terminal/explain/l6-child-projection.txt) · [`evidence/metrics/l6-explain-width.csv`](./evidence/metrics/l6-explain-width.csv)).
|
||||
- 잔여 비용(12절): `l6ProbeProjectionStillFetchesAllHighlightsNotTopN`, 페이지 20건인데 자식 행 `1,509`(부모당 전량, top-3 아님 → Top-N 단계)(→ [`evidence/metrics/l6-projection-resolution.csv`](./evidence/metrics/l6-projection-resolution.csv)).
|
||||
- 정확성·전송(13절): **별도 클래스 `FeedTopNIT`**(IT-only, native SQL) `l14ThreeStrategiesReturnTopThreePerParentAndNaiveLimitIsWrong`·`l14TransferAcrossStrategies`, 윈도우·LATERAL은 부모당 3개(반환 60·부모 20), 2단계는 앱컷 전 전량 `1,509`, 순진 `LIMIT 3`은 전체 3행(부모 1개만 = 오작동) 확인(→ [`evidence/metrics/l14-topn-resolution.csv`](./evidence/metrics/l14-topn-resolution.csv)).
|
||||
- 플랜 대조(13절): `l14ExplainThreeWayPlanCompareIsTheCrownJewel`, 세 방법 `EXPLAIN (ANALYZE, BUFFERS)` — LATERAL은 `Index Scan`(buffers 204)·윈도우/2단계는 같은 `Hash Semi Join`(buffers 430, 전량 1,509) 확인(→ [`l14-lateral-plan.txt`](./evidence/terminal/explain/l14-lateral-plan.txt) · [`l14-window-plan.txt`](./evidence/terminal/explain/l14-window-plan.txt) · [`l14-twostep-plan.txt`](./evidence/terminal/explain/l14-twostep-plan.txt) · [`evidence/metrics/l14-plan-compare.csv`](./evidence/metrics/l14-plan-compare.csv)).
|
||||
- 인덱스 토글(13절): `l14LateralDependsOnCompositeIndex`, 같은 LATERAL을 `ix_highlights_feed_items_created` DROP 후 측정→`finally` 복구 — 인덱스 없으면 `Seq Scan`(Rows Removed by Filter 2842/loop)으로 buffers 168→4446(약 26배) 확인(→ [`l14-lateral-no-index.txt`](./evidence/terminal/explain/l14-lateral-no-index.txt) · [`evidence/metrics/l14-index-toggle.csv`](./evidence/metrics/l14-index-toggle.csv)).
|
||||
- 그룹 크기 곡선(13절): `l14GroupSizeCurveWindowVsLateral`(K=3/50/500), 반환 60/695/1,509이고 LATERAL buffers가 모든 K에서 윈도우보다 작음(작은 K일수록 격차↑) 확인(→ [`evidence/metrics/l14-group-size.csv`](./evidence/metrics/l14-group-size.csv)).
|
||||
- EXPLAIN 대조(10절): `l4ExplainCollectionJoinHasNoLimitButEntityPagingDoes`, (a) 조인 SQL엔 Limit 노드 없음 / (b) 엔티티 페이징엔 있음 확인(→ [`evidence/raw/explain/l4-collection-join-no-limit.txt`](./evidence/raw/explain/l4-collection-join-no-limit.txt) · [`l4-entity-paging-limit.txt`](./evidence/raw/explain/l4-entity-paging-limit.txt)).
|
||||
- 배치 해결(11절): `FeedBatchFetchIT`(신규, 격리 클래스 `default_batch_fetch_size=100`) `l5BatchFetchCollapsesQueryCount` (N=10/100/1000), `prepared < N`(순진 `1+N`에서 붕괴)·`collectionFetch == ceil(N/batch)` 확인(→ [`evidence/raw/metrics/l5-batch-resolution.csv`](./evidence/raw/metrics/l5-batch-resolution.csv)).
|
||||
- 페이징 정상(11절): `l5EntityPagingLoadsOnlyThePageNotWholeDataset`, `feedItemLoaded == min(20, N)`(10절 over-fetch 소멸). EXPLAIN `l5ExplainEntityPagingHasLimitAndBatchInHasNoRowMultiplication`, (a) 엔티티 페이징엔 Limit 노드 존재 / (b) 배치 IN은 semi-join(행 안 곱함)(→ [`evidence/raw/explain/l5-entity-paging-limit.txt`](./evidence/raw/explain/l5-entity-paging-limit.txt) · [`l5-batch-in-semijoin.txt`](./evidence/raw/explain/l5-batch-in-semijoin.txt)).
|
||||
- 잔여 비용(11절): `l5ProbeBatchStillHydratesFullEntities`, 페이지 20건인데 `entitiesLoaded == 1,569`(엔티티 과적재 → 프로젝션 단계)(→ [`evidence/raw/metrics/l5-hydration-probe.csv`](./evidence/raw/metrics/l5-hydration-probe.csv)).
|
||||
- 프로젝션 해결(12절): `FeedProjectionIT`(신규, 격리 클래스, 배치 설정 없음) `l6ProjectionHydratesZeroEntities` (N=10/100/1000), `entitiesLoaded == 0`(11절의 1,569 소멸)·`prepared == 2`(N 무관 상수)·`collectionFetch == 0` 확인. 형태 동치 `l6ProjectionReturnsSameShapeAsNaiveLoadFeed`(프로젝션 vs 순진 loadFeed 같은 결과)(→ [`evidence/raw/metrics/l6-projection-resolution.csv`](./evidence/raw/metrics/l6-projection-resolution.csv)).
|
||||
- EXPLAIN·width 정정(12절): `l6ExplainProjectionHasLimitAndSemiJoinNotNarrowerWidth`, (a) 부모 프로젝션 Limit 노드 존재하나 width 안 좁아짐(2088 > 엔티티 1194) / (b) 자식 IN semi-join(행 안 곱함). 프로젝션 이득은 EXPLAIN 아니라 ORM 층(→ [`evidence/raw/explain/l6-parent-projection.txt`](./evidence/raw/explain/l6-parent-projection.txt) · [`l6-child-projection.txt`](./evidence/raw/explain/l6-child-projection.txt) · [`evidence/raw/metrics/l6-explain-width.csv`](./evidence/raw/metrics/l6-explain-width.csv)).
|
||||
- 잔여 비용(12절): `l6ProbeProjectionStillFetchesAllHighlightsNotTopN`, 페이지 20건인데 자식 행 `1,509`(부모당 전량, top-3 아님 → Top-N 단계)(→ [`evidence/raw/metrics/l6-projection-resolution.csv`](./evidence/raw/metrics/l6-projection-resolution.csv)).
|
||||
- 정확성·전송(13절): **별도 클래스 `FeedTopNIT`**(IT-only, native SQL) `l14ThreeStrategiesReturnTopThreePerParentAndNaiveLimitIsWrong`·`l14TransferAcrossStrategies`, 윈도우·LATERAL은 부모당 3개(반환 60·부모 20), 2단계는 앱컷 전 전량 `1,509`, 순진 `LIMIT 3`은 전체 3행(부모 1개만 = 오작동) 확인(→ [`evidence/raw/metrics/l14-topn-resolution.csv`](./evidence/raw/metrics/l14-topn-resolution.csv)).
|
||||
- 플랜 대조(13절): `l14ExplainThreeWayPlanCompareIsTheCrownJewel`, 세 방법 `EXPLAIN (ANALYZE, BUFFERS)` — LATERAL은 `Index Scan`(buffers 204)·윈도우/2단계는 같은 `Hash Semi Join`(buffers 430, 전량 1,509) 확인(→ [`l14-lateral-plan.txt`](./evidence/raw/explain/l14-lateral-plan.txt) · [`l14-window-plan.txt`](./evidence/raw/explain/l14-window-plan.txt) · [`l14-twostep-plan.txt`](./evidence/raw/explain/l14-twostep-plan.txt) · [`evidence/raw/metrics/l14-plan-compare.csv`](./evidence/raw/metrics/l14-plan-compare.csv)).
|
||||
- 인덱스 토글(13절): `l14LateralDependsOnCompositeIndex`, 같은 LATERAL을 `ix_highlights_feed_items_created` DROP 후 측정→`finally` 복구 — 인덱스 없으면 `Seq Scan`(Rows Removed by Filter 2842/loop)으로 buffers 168→4446(약 26배) 확인(→ [`l14-lateral-no-index.txt`](./evidence/raw/explain/l14-lateral-no-index.txt) · [`evidence/raw/metrics/l14-index-toggle.csv`](./evidence/raw/metrics/l14-index-toggle.csv)).
|
||||
- 그룹 크기 곡선(13절): `l14GroupSizeCurveWindowVsLateral`(K=3/50/500), 반환 60/695/1,509이고 LATERAL buffers가 모든 K에서 윈도우보다 작음(작은 K일수록 격차↑) 확인(→ [`evidence/raw/metrics/l14-group-size.csv`](./evidence/raw/metrics/l14-group-size.csv)).
|
||||
- 잔여 비용(13절): `l14ProbeParentPagingStillUsesOffsetNotKeyset`, 부모 페이징이 아직 `OFFSET 900`이라 앞 900행 scan-then-discard(→ keyset 페이징 단계).
|
||||
- 깊이 곡선(14절): **별도 클래스 `FeedKeysetIT`**(IT-only, native SQL) `l15DeepPageOffsetOverScansButKeysetStaysFlat`(offset 0/980/1980), OFFSET 훑은 행 = offset+20(20/`1,000`/`2,000`)인데 keyset은 20으로 일정함(page 100에서 100× over-scan) 확인(→ [`evidence/metrics/l15-depth-curve.csv`](./evidence/metrics/l15-depth-curve.csv)).
|
||||
- EXPLAIN·인덱스 유무(14절): `l15ExplainOffsetScansThenDiscardsKeysetSeeksAndNeedsIndex`, OFFSET `Seq Scan`+`Sort`(2,000, buffers 141) vs keyset `Index Only Scan`(20, buffers 1); 인덱스 없으면 keyset도 `Seq Scan`(buffers 141) 확인(→ [`l15-offset-deep-page.txt`](./evidence/terminal/explain/l15-offset-deep-page.txt) · [`l15-keyset-index-seek.txt`](./evidence/terminal/explain/l15-keyset-index-seek.txt) · [`l15-keyset-no-index.txt`](./evidence/terminal/explain/l15-keyset-no-index.txt) · [`evidence/metrics/l15-deep-page-compare.csv`](./evidence/metrics/l15-deep-page-compare.csv)).
|
||||
- 깊이 곡선(14절): **별도 클래스 `FeedKeysetIT`**(IT-only, native SQL) `l15DeepPageOffsetOverScansButKeysetStaysFlat`(offset 0/980/1980), OFFSET 훑은 행 = offset+20(20/`1,000`/`2,000`)인데 keyset은 20으로 일정함(page 100에서 100× over-scan) 확인(→ [`evidence/raw/metrics/l15-depth-curve.csv`](./evidence/raw/metrics/l15-depth-curve.csv)).
|
||||
- EXPLAIN·인덱스 유무(14절): `l15ExplainOffsetScansThenDiscardsKeysetSeeksAndNeedsIndex`, OFFSET `Seq Scan`+`Sort`(2,000, buffers 141) vs keyset `Index Only Scan`(20, buffers 1); 인덱스 없으면 keyset도 `Seq Scan`(buffers 141) 확인(→ [`l15-offset-deep-page.txt`](./evidence/raw/explain/l15-offset-deep-page.txt) · [`l15-keyset-index-seek.txt`](./evidence/raw/explain/l15-keyset-index-seek.txt) · [`l15-keyset-no-index.txt`](./evidence/raw/explain/l15-keyset-no-index.txt) · [`evidence/raw/metrics/l15-deep-page-compare.csv`](./evidence/raw/metrics/l15-deep-page-compare.csv)).
|
||||
- 정확성(14절): `l15KeysetWalkMatchesOffsetPages`, keyset 커서로 넘긴 page 2 == OFFSET page 2(같은 20 id·같은 순서).
|
||||
- 가시성 probe(14절 → 가시성 인덱싱 단계): `l15ProbeVisibilityOrBreaksKeysetIndex`, keyset에 가시성 `OR`+`EXISTS`를 얹으면 정렬키 인덱스 미사용·`BitmapOr`+`Sort` 재등장(순서 seek 이점 소멸) 확인(→ [`l15-visibility-or-probe.txt`](./evidence/terminal/explain/l15-visibility-or-probe.txt)).
|
||||
- 정확성(15절): **별도 클래스 `FeedVisibilityIT`**(IT-only, native SQL·신규 인덱스/feed_visible 토글) `l16ThreeApproachesReturnSameVisibleSet`, 단일 OR == UNION 분해 == 사전계산이 같은 20 feed_item(답 동일, 플랜만 다름) 확인(→ [`evidence/metrics/l16-plan-compare.csv`](./evidence/metrics/l16-plan-compare.csv)).
|
||||
- 3안 플랜 대조(15절): `l16ExplainThreeWayPlanCompare`, 단일 OR(`BitmapOr`+top-N `Sort`+hashed SubPlan, 후보 `1,500`, buffers 122) vs UNION(`Merge Append`+`Hash Join`, buffers 200) vs 사전계산(`Index Only Scan` on feed_visible, Sort 없음, buffers 1) 확인(→ [`l16-single-or-plan.txt`](./evidence/terminal/explain/l16-single-or-plan.txt) · [`l16-union-decompose-plan.txt`](./evidence/terminal/explain/l16-union-decompose-plan.txt) · [`l16-precompute-plan.txt`](./evidence/terminal/explain/l16-precompute-plan.txt)).
|
||||
- 분기별 인덱스(15절): `l16LowSelectivityBranchesRideTheirIndex`, mentioned 분기=`ix_mentions_user` 조인·private 분기=`ix_feed_items_private` partial의 `Index Only Scan` 확인(→ [`l16-union-branches.txt`](./evidence/terminal/explain/l16-union-branches.txt)).
|
||||
- 사전계산=CQRS(15절 → CQRS-lite 읽기 모델 단계): `l16PrecomputeIsSingleIndexScanNoOrNoSort`, `feed_visible` 단일 `Index Only Scan`·Sort 없음·buffers 1 확인(→ [`l16-precompute-plan.txt`](./evidence/terminal/explain/l16-precompute-plan.txt)).
|
||||
- 통합 정확성·shape(16절): **별도 클래스 `FeedCrownIT`**(IT-only, native SQL·신규 인덱스/feed_visible 토글) `crownUnifiedReturnsSameShapeAcrossParentPaths`, 세 부모선택(단일 OR/UNION 분해/사전계산)이 같은 20 부모(unionEq·precomputeEq 참)·통합 결과 부모 20·총 60행·부모당 top-3 확인(→ [`evidence/metrics/crown-unified-plan.csv`](./evidence/metrics/crown-unified-plan.csv)).
|
||||
- 한 플랜 세 기법(16절): `crownUnifiedPlanStacksVisibilityKeysetAndTopN`, 사전계산 부모선택 통합 쿼리가 `Index Only Scan`(ix_feed_visible) + `Nested Loop` LATERAL `Index Scan`(ix_highlights_feed_items_created)로 세 기법을 재정렬(Sort) 없이 한 플랜에 겹침 확인(→ [`crown-unified-precompute-plan.txt`](./evidence/terminal/explain/crown-unified-precompute-plan.txt)).
|
||||
- 간섭 시험(16절): `crownDeepPageKeysetSeeksFewerRowsWithPrecomputeThanSingleOr`, 가장 깊은 페이지(보이는 `1,500` 중 마지막)에서 사전계산 부모선택은 `ix_feed_visible` 인덱스 range 로 19 행만, 단일 OR 부모선택은 feed_visible 미사용·`BitmapOr`+멘션 hashed SubPlan 으로 200 행 훑음(★ 실측정정: 깊은 커서에선 둘 다 남은 19 행 작은 Sort) 확인(→ [`crown-deep-keyset-precompute.txt`](./evidence/terminal/explain/crown-deep-keyset-precompute.txt) · [`crown-deep-keyset-single-or.txt`](./evidence/terminal/explain/crown-deep-keyset-single-or.txt)).
|
||||
- 가시성 probe(14절 → 가시성 인덱싱 단계): `l15ProbeVisibilityOrBreaksKeysetIndex`, keyset에 가시성 `OR`+`EXISTS`를 얹으면 정렬키 인덱스 미사용·`BitmapOr`+`Sort` 재등장(순서 seek 이점 소멸) 확인(→ [`l15-visibility-or-probe.txt`](./evidence/raw/explain/l15-visibility-or-probe.txt)).
|
||||
- 정확성(15절): **별도 클래스 `FeedVisibilityIT`**(IT-only, native SQL·신규 인덱스/feed_visible 토글) `l16ThreeApproachesReturnSameVisibleSet`, 단일 OR == UNION 분해 == 사전계산이 같은 20 feed_item(답 동일, 플랜만 다름) 확인(→ [`evidence/raw/metrics/l16-plan-compare.csv`](./evidence/raw/metrics/l16-plan-compare.csv)).
|
||||
- 3안 플랜 대조(15절): `l16ExplainThreeWayPlanCompare`, 단일 OR(`BitmapOr`+top-N `Sort`+hashed SubPlan, 후보 `1,500`, buffers 122) vs UNION(`Merge Append`+`Hash Join`, buffers 200) vs 사전계산(`Index Only Scan` on feed_visible, Sort 없음, buffers 1) 확인(→ [`l16-single-or-plan.txt`](./evidence/raw/explain/l16-single-or-plan.txt) · [`l16-union-decompose-plan.txt`](./evidence/raw/explain/l16-union-decompose-plan.txt) · [`l16-precompute-plan.txt`](./evidence/raw/explain/l16-precompute-plan.txt)).
|
||||
- 분기별 인덱스(15절): `l16LowSelectivityBranchesRideTheirIndex`, mentioned 분기=`ix_mentions_user` 조인·private 분기=`ix_feed_items_private` partial의 `Index Only Scan` 확인(→ [`l16-union-branches.txt`](./evidence/raw/explain/l16-union-branches.txt)).
|
||||
- 사전계산=CQRS(15절 → CQRS-lite 읽기 모델 단계): `l16PrecomputeIsSingleIndexScanNoOrNoSort`, `feed_visible` 단일 `Index Only Scan`·Sort 없음·buffers 1 확인(→ [`l16-precompute-plan.txt`](./evidence/raw/explain/l16-precompute-plan.txt)).
|
||||
- 통합 정확성·shape(16절): **별도 클래스 `FeedCrownIT`**(IT-only, native SQL·신규 인덱스/feed_visible 토글) `crownUnifiedReturnsSameShapeAcrossParentPaths`, 세 부모선택(단일 OR/UNION 분해/사전계산)이 같은 20 부모(unionEq·precomputeEq 참)·통합 결과 부모 20·총 60행·부모당 top-3 확인(→ [`evidence/raw/metrics/crown-unified-plan.csv`](./evidence/raw/metrics/crown-unified-plan.csv)).
|
||||
- 한 플랜 세 기법(16절): `crownUnifiedPlanStacksVisibilityKeysetAndTopN`, 사전계산 부모선택 통합 쿼리가 `Index Only Scan`(ix_feed_visible) + `Nested Loop` LATERAL `Index Scan`(ix_highlights_feed_items_created)로 세 기법을 재정렬(Sort) 없이 한 플랜에 겹침 확인(→ [`crown-unified-precompute-plan.txt`](./evidence/raw/explain/crown-unified-precompute-plan.txt)).
|
||||
- 간섭 시험(16절): `crownDeepPageKeysetSeeksFewerRowsWithPrecomputeThanSingleOr`, 가장 깊은 페이지(보이는 `1,500` 중 마지막)에서 사전계산 부모선택은 `ix_feed_visible` 인덱스 range 로 19 행만, 단일 OR 부모선택은 feed_visible 미사용·`BitmapOr`+멘션 hashed SubPlan 으로 200 행 훑음(★ 실측정정: 깊은 커서에선 둘 다 남은 19 행 작은 Sort) 확인(→ [`crown-deep-keyset-precompute.txt`](./evidence/raw/explain/crown-deep-keyset-precompute.txt) · [`crown-deep-keyset-single-or.txt`](./evidence/raw/explain/crown-deep-keyset-single-or.txt)).
|
||||
- CQRS-lite 읽기 모델(17절): **프로덕션 경로**(시리즈 첫 프로덕션 코드, IT-only 아님) `GetFeedReadModelUseCase` → `FeedReadModelQueryPort` → `FeedReadModelQueryAdapter`(신규). `FeedReadModelUseCaseIT`(seed N∈{10, 100})가 유스케이스 경로에서 엔티티 로드 0·발행 쿼리 상수 2(N 무관)·부모당 top-3(12절 프로젝션 + 13절 window 결합, 12절 잔여 `1,509` → ≤60 해소) 반환 확인. ArchUnit `query_ports_do_not_leak…`·의존 방향·`./gradlew check` GREEN.
|
||||
|
||||
> 개별 테스트만 돌릴 때는 Gradle 와일드카드가 `*`임에 주의(`...`은 매칭 0). 예) `--tests '*FeedPersistenceIT.l2*'`. 초록불을 다시 돌리려면 `--rerun-tasks`(안 그러면 UP-TO-DATE로 건너뜀). 콘솔 측정 라인(`>>> LAB …`)은 `build/lab-results/feed-nplus1.md`에도 표로 적재됩니다.
|
||||
|
||||
원시 데이터 자산:
|
||||
|
||||
- [`evidence/metrics/l1-query-growth.csv`](./evidence/metrics/l1-query-growth.csv) — N, 초기화 컬렉션, 총 PreparedStatement, ToOne 몫.
|
||||
- [`evidence/metrics/l1-skew-distribution.csv`](./evidence/metrics/l1-skew-distribution.csv) — 순위별 하이라이트 수.
|
||||
- [`evidence/metrics/l2-toone-split.csv`](./evidence/metrics/l2-toone-split.csv) — N, Page·User·entity fetch, 초기화 컬렉션, 총 PreparedStatement(N2 직접 측정).
|
||||
- [`evidence/terminal/explain/highlights-child-plan-A.txt`](./evidence/terminal/explain/highlights-child-plan-A.txt) — N1 Plan A EXPLAIN 원문.
|
||||
- [`evidence/terminal/explain/toone-pages-plan.txt`](./evidence/terminal/explain/toone-pages-plan.txt) · [`evidence/terminal/explain/toone-users-plan.txt`](./evidence/terminal/explain/toone-users-plan.txt) — N2 반복 ToOne 부모 쿼리 EXPLAIN 원문.
|
||||
- [`evidence/metrics/l3-cartesian.csv`](./evidence/metrics/l3-cartesian.csv) — N, 전송 행수(조인 카디널리티), 리스트 크기(Hib6 dedup), distinct, 시드 하이라이트, 폭발 배수, 총 PreparedStatement(9절 카테시안).
|
||||
- [`evidence/terminal/explain/l3-cartesian-join-plan.txt`](./evidence/terminal/explain/l3-cartesian-join-plan.txt) — 9절 컬렉션 fetch join 조인의 EXPLAIN 원문(Hash Join actual rows = Σ highlights).
|
||||
- [`evidence/metrics/l4-inmemory-paging.csv`](./evidence/metrics/l4-inmemory-paging.csv) — N, returned(페이지), feedItemLoaded(=N), over-fetch 배수, 시드 하이라이트(10절 인메모리 페이징, 결정적·hash-anchor).
|
||||
- [`evidence/metrics/l4-cost-curve.csv`](./evidence/metrics/l4-cost-curve.csv) — N, 지연 p50/p99(ms), 스레드 누적 할당(KB). 측정 범위상 환경 의존 상대값이라 anchor가 아니라 whitelist(N에 따른 방향만 읽음).
|
||||
- [`evidence/terminal/explain/l4-collection-join-no-limit.txt`](./evidence/terminal/explain/l4-collection-join-no-limit.txt) · [`evidence/terminal/explain/l4-entity-paging-limit.txt`](./evidence/terminal/explain/l4-entity-paging-limit.txt) — 10절 (a) 조인 SQL(Limit 노드 부재) / (b) 엔티티 페이징(Limit 노드 존재) EXPLAIN 원문.
|
||||
- [`evidence/metrics/l5-batch-resolution.csv`](./evidence/metrics/l5-batch-resolution.csv) — N, before/after PreparedStatement·컬렉션 fetch, feedItemLoaded(페이지), 붕괴 배수(11절 배치 해결, 결정적·hash-anchor).
|
||||
- [`evidence/metrics/l5-hydration-probe.csv`](./evidence/metrics/l5-hydration-probe.csv) — 페이지 20건 조회의 엔티티 하이드레이트 총수(11절 잔여 과적재 → 프로젝션 단계).
|
||||
- [`evidence/terminal/explain/l5-entity-paging-limit.txt`](./evidence/terminal/explain/l5-entity-paging-limit.txt) · [`evidence/terminal/explain/l5-batch-in-semijoin.txt`](./evidence/terminal/explain/l5-batch-in-semijoin.txt) — 11절 (a) 엔티티 페이징(Limit 노드 존재) / (b) 배치 IN(semi-join, 곱셈 없음) EXPLAIN 원문.
|
||||
- [`evidence/metrics/l6-projection-resolution.csv`](./evidence/metrics/l6-projection-resolution.csv) — before(11절 배치)/after(12절 프로젝션) 엔티티 로드·PreparedStatement·컬렉션 fetch·자식 행수(12절 프로젝션 해결, 결정적·hash-anchor).
|
||||
- [`evidence/metrics/l6-explain-width.csv`](./evidence/metrics/l6-explain-width.csv) — 부모 프로젝션 width vs 엔티티 페이징 width(12.4절 실측 정정: 프로젝션이 오히려 넓습니다).
|
||||
- [`evidence/terminal/explain/l6-parent-projection.txt`](./evidence/terminal/explain/l6-parent-projection.txt) · [`evidence/terminal/explain/l6-child-projection.txt`](./evidence/terminal/explain/l6-child-projection.txt) — 12절 (a) 부모 스칼라 프로젝션(Limit 존재, width 2088) / (b) 자식 스칼라 IN(semi-join, 행 안 곱함) EXPLAIN 원문.
|
||||
- [`evidence/metrics/l14-topn-resolution.csv`](./evidence/metrics/l14-topn-resolution.csv) — 전략별(윈도우/LATERAL/2단계/순진) 반환 행·커버 부모·부모당 최대(13절 정확성·전송, 결정적·hash-anchor).
|
||||
- [`evidence/metrics/l14-plan-compare.csv`](./evidence/metrics/l14-plan-compare.csv) — 3안 최상위 노드·반환 행·buffers(shared hit)·exec(13절 플랜 대조). buffers·exec는 워밍 캐시 상대값이라 anchor가 아니라 whitelist(같은 실행 내 상대 대조로만).
|
||||
- [`evidence/metrics/l14-group-size.csv`](./evidence/metrics/l14-group-size.csv) — K∈{3, 50, 500}별 윈도우/LATERAL 반환 행·buffers(13절 그룹 크기 곡선; 반환은 결정적, buffers는 whitelist).
|
||||
- [`evidence/metrics/l14-index-toggle.csv`](./evidence/metrics/l14-index-toggle.csv) — LATERAL 인덱스 유무 buffers·exec(13절 인덱스 의존; 환경 의존 상대값 whitelist).
|
||||
- [`evidence/terminal/explain/l14-lateral-plan.txt`](./evidence/terminal/explain/l14-lateral-plan.txt) · [`evidence/terminal/explain/l14-window-plan.txt`](./evidence/terminal/explain/l14-window-plan.txt) · [`evidence/terminal/explain/l14-twostep-plan.txt`](./evidence/terminal/explain/l14-twostep-plan.txt) — 13절 세 해법 EXPLAIN 원문(LATERAL Index Scan / 윈도우 WindowAgg / 2단계 Hash Semi Join).
|
||||
- [`evidence/terminal/explain/l14-lateral-no-index.txt`](./evidence/terminal/explain/l14-lateral-no-index.txt) — 13절 인덱스 DROP 후 같은 LATERAL EXPLAIN 원문(부모별 Seq Scan, buffers 폭증).
|
||||
- [`evidence/metrics/l15-depth-curve.csv`](./evidence/metrics/l15-depth-curve.csv) — 페이지 깊이(offset)별 OFFSET/keyset 훑은 행·buffers(14절 깊이 곡선; OFFSET=offset+20 결정적·hash-anchor, buffers는 whitelist).
|
||||
- [`evidence/metrics/l15-deep-page-compare.csv`](./evidence/metrics/l15-deep-page-compare.csv) — 깊은 페이지(offset 1980) OFFSET/keyset(+인덱스)/keyset(−인덱스) 최상위 노드·훑은 행·buffers·exec(14절; buffers·exec는 환경 의존 whitelist).
|
||||
- [`evidence/terminal/explain/l15-offset-deep-page.txt`](./evidence/terminal/explain/l15-offset-deep-page.txt) · [`evidence/terminal/explain/l15-keyset-index-seek.txt`](./evidence/terminal/explain/l15-keyset-index-seek.txt) · [`evidence/terminal/explain/l15-keyset-no-index.txt`](./evidence/terminal/explain/l15-keyset-no-index.txt) — 14절 OFFSET(Seq Scan+Sort) / keyset(Index Only Scan) / keyset 인덱스 없음(Seq Scan) EXPLAIN 원문.
|
||||
- [`evidence/terminal/explain/l15-visibility-or-probe.txt`](./evidence/terminal/explain/l15-visibility-or-probe.txt) — 14절 keyset + 가시성 OR/EXISTS EXPLAIN 원문(BitmapOr + Sort, 정렬키 인덱스 미사용 → 가시성 조건 인덱싱 단계).
|
||||
- [`evidence/metrics/l16-plan-compare.csv`](./evidence/metrics/l16-plan-compare.csv) — 가시성 3안(단일 OR/UNION 분해/사전계산) 최상위 노드·Sort·멘션 처리·훑는 후보·buffers·exec(15절; 훑는 후보 1500은 결정적·hash-anchor, buffers·exec는 환경 의존 whitelist).
|
||||
- [`evidence/terminal/explain/l16-single-or-plan.txt`](./evidence/terminal/explain/l16-single-or-plan.txt) · [`evidence/terminal/explain/l16-union-decompose-plan.txt`](./evidence/terminal/explain/l16-union-decompose-plan.txt) · [`evidence/terminal/explain/l16-precompute-plan.txt`](./evidence/terminal/explain/l16-precompute-plan.txt) — 15절 단일 OR(BitmapOr+Sort+hashed SubPlan) / UNION 분해(Merge Append+Hash Join) / 사전계산(단일 Index Only Scan) EXPLAIN 원문.
|
||||
- [`evidence/terminal/explain/l16-union-branches.txt`](./evidence/terminal/explain/l16-union-branches.txt) — 15절 UNION 각 분기(mentioned=ix_mentions_user 조인 / private=partial 인덱스 / public=고선택도 bitmap) EXPLAIN 원문.
|
||||
- [`evidence/metrics/crown-unified-plan.csv`](./evidence/metrics/crown-unified-plan.csv) — 통합(16절/Task 4) 부모선택별(사전계산/단일 OR) page 1·깊은 페이지 부모 수·행수·훑는 행·buffers·뷰어 가시 집합(부모/행/훑는 행은 결정적, buffers 는 환경 의존 whitelist).
|
||||
- [`evidence/terminal/explain/crown-unified-precompute-plan.txt`](./evidence/terminal/explain/crown-unified-precompute-plan.txt) — 16절 사전계산 부모선택 통합 쿼리 EXPLAIN 원문(Index Only Scan feed_visible + Nested Loop LATERAL, Sort 없음 — 한 플랜 세 기법).
|
||||
- [`evidence/terminal/explain/crown-deep-keyset-precompute.txt`](./evidence/terminal/explain/crown-deep-keyset-precompute.txt) · [`evidence/terminal/explain/crown-deep-keyset-single-or.txt`](./evidence/terminal/explain/crown-deep-keyset-single-or.txt) — 16절 깊은 페이지 keyset 간섭 시험 EXPLAIN 원문(사전계산 인덱스 range 19행 vs 단일 OR BitmapOr+멘션 SubPlan 200행).
|
||||
- [`evidence/raw/metrics/l1-query-growth.csv`](./evidence/raw/metrics/l1-query-growth.csv) — N, 초기화 컬렉션, 총 PreparedStatement, ToOne 몫.
|
||||
- [`evidence/raw/metrics/l1-skew-distribution.csv`](./evidence/raw/metrics/l1-skew-distribution.csv) — 순위별 하이라이트 수.
|
||||
- [`evidence/raw/metrics/l2-toone-split.csv`](./evidence/raw/metrics/l2-toone-split.csv) — N, Page·User·entity fetch, 초기화 컬렉션, 총 PreparedStatement(N2 직접 측정).
|
||||
- [`evidence/raw/explain/highlights-child-plan-A.txt`](./evidence/raw/explain/highlights-child-plan-A.txt) — N1 Plan A EXPLAIN 원문.
|
||||
- [`evidence/raw/explain/toone-pages-plan.txt`](./evidence/raw/explain/toone-pages-plan.txt) · [`evidence/raw/explain/toone-users-plan.txt`](./evidence/raw/explain/toone-users-plan.txt) — N2 반복 ToOne 부모 쿼리 EXPLAIN 원문.
|
||||
- [`evidence/raw/metrics/l3-cartesian.csv`](./evidence/raw/metrics/l3-cartesian.csv) — N, 전송 행수(조인 카디널리티), 리스트 크기(Hib6 dedup), distinct, 시드 하이라이트, 폭발 배수, 총 PreparedStatement(9절 카테시안).
|
||||
- [`evidence/raw/explain/l3-cartesian-join-plan.txt`](./evidence/raw/explain/l3-cartesian-join-plan.txt) — 9절 컬렉션 fetch join 조인의 EXPLAIN 원문(Hash Join actual rows = Σ highlights).
|
||||
- [`evidence/raw/metrics/l4-inmemory-paging.csv`](./evidence/raw/metrics/l4-inmemory-paging.csv) — N, returned(페이지), feedItemLoaded(=N), over-fetch 배수, 시드 하이라이트(10절 인메모리 페이징, 결정적·hash-anchor).
|
||||
- [`evidence/raw/metrics/l4-cost-curve.csv`](./evidence/raw/metrics/l4-cost-curve.csv) — N, 지연 p50/p99(ms), 스레드 누적 할당(KB). 측정 범위상 환경 의존 상대값이라 anchor가 아니라 whitelist(N에 따른 방향만 읽음).
|
||||
- [`evidence/raw/explain/l4-collection-join-no-limit.txt`](./evidence/raw/explain/l4-collection-join-no-limit.txt) · [`evidence/raw/explain/l4-entity-paging-limit.txt`](./evidence/raw/explain/l4-entity-paging-limit.txt) — 10절 (a) 조인 SQL(Limit 노드 부재) / (b) 엔티티 페이징(Limit 노드 존재) EXPLAIN 원문.
|
||||
- [`evidence/raw/metrics/l5-batch-resolution.csv`](./evidence/raw/metrics/l5-batch-resolution.csv) — N, before/after PreparedStatement·컬렉션 fetch, feedItemLoaded(페이지), 붕괴 배수(11절 배치 해결, 결정적·hash-anchor).
|
||||
- [`evidence/raw/metrics/l5-hydration-probe.csv`](./evidence/raw/metrics/l5-hydration-probe.csv) — 페이지 20건 조회의 엔티티 하이드레이트 총수(11절 잔여 과적재 → 프로젝션 단계).
|
||||
- [`evidence/raw/explain/l5-entity-paging-limit.txt`](./evidence/raw/explain/l5-entity-paging-limit.txt) · [`evidence/raw/explain/l5-batch-in-semijoin.txt`](./evidence/raw/explain/l5-batch-in-semijoin.txt) — 11절 (a) 엔티티 페이징(Limit 노드 존재) / (b) 배치 IN(semi-join, 곱셈 없음) EXPLAIN 원문.
|
||||
- [`evidence/raw/metrics/l6-projection-resolution.csv`](./evidence/raw/metrics/l6-projection-resolution.csv) — before(11절 배치)/after(12절 프로젝션) 엔티티 로드·PreparedStatement·컬렉션 fetch·자식 행수(12절 프로젝션 해결, 결정적·hash-anchor).
|
||||
- [`evidence/raw/metrics/l6-explain-width.csv`](./evidence/raw/metrics/l6-explain-width.csv) — 부모 프로젝션 width vs 엔티티 페이징 width(12.4절 실측 정정: 프로젝션이 오히려 넓습니다).
|
||||
- [`evidence/raw/explain/l6-parent-projection.txt`](./evidence/raw/explain/l6-parent-projection.txt) · [`evidence/raw/explain/l6-child-projection.txt`](./evidence/raw/explain/l6-child-projection.txt) — 12절 (a) 부모 스칼라 프로젝션(Limit 존재, width 2088) / (b) 자식 스칼라 IN(semi-join, 행 안 곱함) EXPLAIN 원문.
|
||||
- [`evidence/raw/metrics/l14-topn-resolution.csv`](./evidence/raw/metrics/l14-topn-resolution.csv) — 전략별(윈도우/LATERAL/2단계/순진) 반환 행·커버 부모·부모당 최대(13절 정확성·전송, 결정적·hash-anchor).
|
||||
- [`evidence/raw/metrics/l14-plan-compare.csv`](./evidence/raw/metrics/l14-plan-compare.csv) — 3안 최상위 노드·반환 행·buffers(shared hit)·exec(13절 플랜 대조). buffers·exec는 워밍 캐시 상대값이라 anchor가 아니라 whitelist(같은 실행 내 상대 대조로만).
|
||||
- [`evidence/raw/metrics/l14-group-size.csv`](./evidence/raw/metrics/l14-group-size.csv) — K∈{3, 50, 500}별 윈도우/LATERAL 반환 행·buffers(13절 그룹 크기 곡선; 반환은 결정적, buffers는 whitelist).
|
||||
- [`evidence/raw/metrics/l14-index-toggle.csv`](./evidence/raw/metrics/l14-index-toggle.csv) — LATERAL 인덱스 유무 buffers·exec(13절 인덱스 의존; 환경 의존 상대값 whitelist).
|
||||
- [`evidence/raw/explain/l14-lateral-plan.txt`](./evidence/raw/explain/l14-lateral-plan.txt) · [`evidence/raw/explain/l14-window-plan.txt`](./evidence/raw/explain/l14-window-plan.txt) · [`evidence/raw/explain/l14-twostep-plan.txt`](./evidence/raw/explain/l14-twostep-plan.txt) — 13절 세 해법 EXPLAIN 원문(LATERAL Index Scan / 윈도우 WindowAgg / 2단계 Hash Semi Join).
|
||||
- [`evidence/raw/explain/l14-lateral-no-index.txt`](./evidence/raw/explain/l14-lateral-no-index.txt) — 13절 인덱스 DROP 후 같은 LATERAL EXPLAIN 원문(부모별 Seq Scan, buffers 폭증).
|
||||
- [`evidence/raw/metrics/l15-depth-curve.csv`](./evidence/raw/metrics/l15-depth-curve.csv) — 페이지 깊이(offset)별 OFFSET/keyset 훑은 행·buffers(14절 깊이 곡선; OFFSET=offset+20 결정적·hash-anchor, buffers는 whitelist).
|
||||
- [`evidence/raw/metrics/l15-deep-page-compare.csv`](./evidence/raw/metrics/l15-deep-page-compare.csv) — 깊은 페이지(offset 1980) OFFSET/keyset(+인덱스)/keyset(−인덱스) 최상위 노드·훑은 행·buffers·exec(14절; buffers·exec는 환경 의존 whitelist).
|
||||
- [`evidence/raw/explain/l15-offset-deep-page.txt`](./evidence/raw/explain/l15-offset-deep-page.txt) · [`evidence/raw/explain/l15-keyset-index-seek.txt`](./evidence/raw/explain/l15-keyset-index-seek.txt) · [`evidence/raw/explain/l15-keyset-no-index.txt`](./evidence/raw/explain/l15-keyset-no-index.txt) — 14절 OFFSET(Seq Scan+Sort) / keyset(Index Only Scan) / keyset 인덱스 없음(Seq Scan) EXPLAIN 원문.
|
||||
- [`evidence/raw/explain/l15-visibility-or-probe.txt`](./evidence/raw/explain/l15-visibility-or-probe.txt) — 14절 keyset + 가시성 OR/EXISTS EXPLAIN 원문(BitmapOr + Sort, 정렬키 인덱스 미사용 → 가시성 조건 인덱싱 단계).
|
||||
- [`evidence/raw/metrics/l16-plan-compare.csv`](./evidence/raw/metrics/l16-plan-compare.csv) — 가시성 3안(단일 OR/UNION 분해/사전계산) 최상위 노드·Sort·멘션 처리·훑는 후보·buffers·exec(15절; 훑는 후보 1500은 결정적·hash-anchor, buffers·exec는 환경 의존 whitelist).
|
||||
- [`evidence/raw/explain/l16-single-or-plan.txt`](./evidence/raw/explain/l16-single-or-plan.txt) · [`evidence/raw/explain/l16-union-decompose-plan.txt`](./evidence/raw/explain/l16-union-decompose-plan.txt) · [`evidence/raw/explain/l16-precompute-plan.txt`](./evidence/raw/explain/l16-precompute-plan.txt) — 15절 단일 OR(BitmapOr+Sort+hashed SubPlan) / UNION 분해(Merge Append+Hash Join) / 사전계산(단일 Index Only Scan) EXPLAIN 원문.
|
||||
- [`evidence/raw/explain/l16-union-branches.txt`](./evidence/raw/explain/l16-union-branches.txt) — 15절 UNION 각 분기(mentioned=ix_mentions_user 조인 / private=partial 인덱스 / public=고선택도 bitmap) EXPLAIN 원문.
|
||||
- [`evidence/raw/metrics/crown-unified-plan.csv`](./evidence/raw/metrics/crown-unified-plan.csv) — 통합(16절/Task 4) 부모선택별(사전계산/단일 OR) page 1·깊은 페이지 부모 수·행수·훑는 행·buffers·뷰어 가시 집합(부모/행/훑는 행은 결정적, buffers 는 환경 의존 whitelist).
|
||||
- [`evidence/raw/explain/crown-unified-precompute-plan.txt`](./evidence/raw/explain/crown-unified-precompute-plan.txt) — 16절 사전계산 부모선택 통합 쿼리 EXPLAIN 원문(Index Only Scan feed_visible + Nested Loop LATERAL, Sort 없음 — 한 플랜 세 기법).
|
||||
- [`evidence/raw/explain/crown-deep-keyset-precompute.txt`](./evidence/raw/explain/crown-deep-keyset-precompute.txt) · [`evidence/raw/explain/crown-deep-keyset-single-or.txt`](./evidence/raw/explain/crown-deep-keyset-single-or.txt) — 16절 깊은 페이지 keyset 간섭 시험 EXPLAIN 원문(사전계산 인덱스 range 19행 vs 단일 OR BitmapOr+멘션 SubPlan 200행).
|
||||
|
||||
### B. 측정 환경·출처(provenance)
|
||||
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ assets:
|
||||
- key: in-memory-paging
|
||||
file: ../../../final/assets/tech-log-studio/in-memory-paging.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/terminal/explain/l4-entity-paging-limit.txt
|
||||
- ../../../final/evidence/terminal/explain/l5-entity-paging-limit.txt
|
||||
- ../../../final/evidence/raw/explain/l4-entity-paging-limit.txt
|
||||
- ../../../final/evidence/raw/explain/l5-entity-paging-limit.txt
|
||||
---
|
||||
|
||||
# Collection Fetch Join Pagination의 In-memory Paging
|
||||
|
||||
+3
-3
@@ -11,9 +11,9 @@ assets:
|
||||
- key: eager-lazy-query-sequence
|
||||
file: ../../../final/assets/tech-log-studio/eager-lazy-query-sequence.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/terminal/explain/highlights-child-plan-A.txt
|
||||
- ../../../final/evidence/terminal/explain/toone-pages-plan.txt
|
||||
- ../../../final/evidence/terminal/explain/toone-users-plan.txt
|
||||
- ../../../final/evidence/raw/explain/highlights-child-plan-A.txt
|
||||
- ../../../final/evidence/raw/explain/toone-pages-plan.txt
|
||||
- ../../../final/evidence/raw/explain/toone-users-plan.txt
|
||||
---
|
||||
|
||||
# Fetch 타입이 아니라 조회 방식이 만든 ToOne N+1
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ assets:
|
||||
- key: cartesian-row-multiplication
|
||||
file: ../../../final/assets/tech-log-studio/cartesian-row-multiplication.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/terminal/explain/l3-cartesian-join-plan.txt
|
||||
- ../../../final/evidence/raw/explain/l3-cartesian-join-plan.txt
|
||||
---
|
||||
|
||||
# Fetch Join으로 N+1을 해결하다 만난 MultiBag과 행 폭증
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ assets:
|
||||
- key: projection-row-over-fetch
|
||||
file: ../../../final/assets/tech-log-studio/projection-row-over-fetch.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/terminal/explain/l6-parent-projection.txt
|
||||
- ../../../final/evidence/raw/explain/l6-parent-projection.txt
|
||||
---
|
||||
|
||||
# Projection 이후에도 1,509행을 읽은 Row Over-fetch
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ assets:
|
||||
- key: keyset-vs-offset
|
||||
file: ../../../final/assets/tech-log-studio/keyset-vs-offset.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/terminal/explain/l15-keyset-no-index.txt
|
||||
- ../../../final/evidence/terminal/explain/l15-offset-deep-page.txt
|
||||
- ../../../final/evidence/raw/explain/l15-keyset-no-index.txt
|
||||
- ../../../final/evidence/raw/explain/l15-offset-deep-page.txt
|
||||
---
|
||||
|
||||
# Visibility OR이 Keyset Index를 깨뜨린 문제
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"project": "n+1liner",
|
||||
"ssot": "final/document.md",
|
||||
"ssotSha256": "a32d9e8ba07129fc26deeef2ce07e3624ce6f34888d6cf503d0b7f226bbb4b6b",
|
||||
"generatedAt": "2026-09-04",
|
||||
"note": "글감 목록이다. file 이 있으면 이미 쓴 기록이고, 없으면 아직 쓰지 않은 글감이다.",
|
||||
"note": "글감 목록이다. file 이 있으면 이미 쓴 기록이고, 없으면 아직 쓰지 않은 글감이다. ssotSha256 이 지금 final/document.md 와 다르면 SSOT 가 바뀐 뒤 트리를 다시 보지 않은 것이다.",
|
||||
"readinessValues": [
|
||||
"READY",
|
||||
"NEEDS_EVIDENCE",
|
||||
"BLOCKED",
|
||||
"REJECTED"
|
||||
],
|
||||
"topics": {
|
||||
"jpa-feed-query-performance": {
|
||||
"topic": "jpa-feed-query-performance",
|
||||
@@ -12,46 +20,100 @@
|
||||
"title": "Collection Fetch Join Pagination의 In-memory Paging",
|
||||
"slug": "collection-fetch-join-in-memory-paging",
|
||||
"file": "jpa-feed-query-performance/case/case-collection-fetch-join-in-memory-paging.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "c1158754-e3d2-47b8-bb41-81787c0ca84b",
|
||||
"assets": 1,
|
||||
"evidence": 2
|
||||
"assets": [
|
||||
"in-memory-paging"
|
||||
],
|
||||
"evidence": [
|
||||
"../../../final/evidence/raw/explain/l4-entity-paging-limit.txt",
|
||||
"../../../final/evidence/raw/explain/l5-entity-paging-limit.txt"
|
||||
],
|
||||
"relations": [
|
||||
"Collection Fetch Join과 Pagination을 같이 사용하지 않는다",
|
||||
"Fetch Join으로 N+1을 해결하다 만난 MultiBag과 행 폭증",
|
||||
"Fetch Join · Batch · Projection 선택 기준"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Fetch 타입이 아니라 조회 방식이 만든 ToOne N+1",
|
||||
"slug": "eager-toone-nplus1-without-access",
|
||||
"file": "jpa-feed-query-performance/case/case-eager-toone-nplus1-without-access.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "32d0be7d-d88e-4760-8d91-35d3a233a99a",
|
||||
"assets": 1,
|
||||
"evidence": 3
|
||||
"assets": [
|
||||
"eager-lazy-query-sequence"
|
||||
],
|
||||
"evidence": [
|
||||
"../../../final/evidence/raw/explain/highlights-child-plan-A.txt",
|
||||
"../../../final/evidence/raw/explain/toone-pages-plan.txt",
|
||||
"../../../final/evidence/raw/explain/toone-users-plan.txt"
|
||||
],
|
||||
"relations": [
|
||||
"Fetch Type과 Fetch Strategy 구분",
|
||||
"Projection 이후에도 1,509행을 읽은 Row Over-fetch",
|
||||
"JPA N+1 정량 진단 기준"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Fetch Join으로 N+1을 해결하다 만난 MultiBag과 행 폭증",
|
||||
"slug": "fetch-join-multibag-and-row-explosion",
|
||||
"file": "jpa-feed-query-performance/case/case-fetch-join-multibag-and-row-explosion.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "7ed75172-fd56-42bf-956a-8f9fc1cca235",
|
||||
"assets": 1,
|
||||
"evidence": 1
|
||||
"assets": [
|
||||
"cartesian-row-multiplication"
|
||||
],
|
||||
"evidence": [
|
||||
"../../../final/evidence/raw/explain/l3-cartesian-join-plan.txt"
|
||||
],
|
||||
"relations": [
|
||||
"Fetch Join · Batch · Projection 선택 기준",
|
||||
"Fetch 타입이 아니라 조회 방식이 만든 ToOne N+1",
|
||||
"Collection Fetch Join Pagination의 In-memory Paging"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Projection 이후에도 1,509행을 읽은 Row Over-fetch",
|
||||
"slug": "projection-row-over-fetch",
|
||||
"file": "jpa-feed-query-performance/case/case-projection-row-over-fetch.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "4c9c3b90-bc89-4300-9334-088ea95d37d8",
|
||||
"assets": 1,
|
||||
"evidence": 1
|
||||
"assets": [
|
||||
"projection-row-over-fetch"
|
||||
],
|
||||
"evidence": [
|
||||
"../../../final/evidence/raw/explain/l6-parent-projection.txt"
|
||||
],
|
||||
"relations": [
|
||||
"화면 조회는 Read Projection을 사용한다",
|
||||
"Top-N-per-group 선택 기준",
|
||||
"Fetch Join · Batch · Projection 선택 기준"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Visibility OR이 Keyset Index를 깨뜨린 문제",
|
||||
"slug": "visibility-or-breaks-keyset-index",
|
||||
"file": "jpa-feed-query-performance/case/case-visibility-or-breaks-keyset-index.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "e6715e81-6dbd-4287-8e19-946c334f38fb",
|
||||
"assets": 1,
|
||||
"evidence": 2
|
||||
"assets": [
|
||||
"keyset-vs-offset"
|
||||
],
|
||||
"evidence": [
|
||||
"../../../final/evidence/raw/explain/l15-keyset-no-index.txt",
|
||||
"../../../final/evidence/raw/explain/l15-offset-deep-page.txt"
|
||||
],
|
||||
"relations": [
|
||||
"Feed Visibility Query Pattern",
|
||||
"Keyset Pagination 설계 기준",
|
||||
"feed_visible을 Production CQRS로 승격할 것인가"
|
||||
]
|
||||
}
|
||||
],
|
||||
"concept": [],
|
||||
@@ -60,64 +122,105 @@
|
||||
"title": "Feed Visibility Query Pattern",
|
||||
"slug": "feed-visibility-query-pattern",
|
||||
"file": "jpa-feed-query-performance/reference/reference-feed-visibility-query-pattern.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "635fcedd-d402-4297-bcf3-9fcdf4200d28",
|
||||
"assets": 0,
|
||||
"evidence": 0
|
||||
"assets": [],
|
||||
"evidence": [],
|
||||
"relations": [
|
||||
"Visibility OR이 Keyset Index를 깨뜨린 문제",
|
||||
"feed_visible을 Production CQRS로 승격할 것인가",
|
||||
"Keyset Pagination 설계 기준"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Fetch Join · Batch · Projection 선택 기준",
|
||||
"slug": "fetch-strategy-selection",
|
||||
"file": "jpa-feed-query-performance/reference/reference-fetch-strategy-selection.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "db99cbc5-9123-4599-b368-39ff3170e81d",
|
||||
"assets": 0,
|
||||
"evidence": 0
|
||||
"assets": [],
|
||||
"evidence": [],
|
||||
"relations": [
|
||||
"Fetch Join으로 N+1을 해결하다 만난 MultiBag과 행 폭증",
|
||||
"Collection Fetch Join Pagination의 In-memory Paging",
|
||||
"Projection 이후에도 1,509행을 읽은 Row Over-fetch"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Fetch Type과 Fetch Strategy 구분",
|
||||
"slug": "fetch-type-vs-fetch-strategy",
|
||||
"file": "jpa-feed-query-performance/reference/reference-fetch-type-vs-fetch-strategy.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "51095f6e-2cc8-439c-8648-065033614215",
|
||||
"assets": 0,
|
||||
"evidence": 0
|
||||
"assets": [],
|
||||
"evidence": [],
|
||||
"relations": [
|
||||
"필드 접근 없이 발생한 EAGER ToOne N+1",
|
||||
"Fetch Join · Batch · Projection 선택 기준",
|
||||
"JPA N+1 정량 진단 기준"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Keyset Pagination 설계 기준",
|
||||
"slug": "keyset-pagination-design",
|
||||
"file": "jpa-feed-query-performance/reference/reference-keyset-pagination-design.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "06788903-3dfa-4f70-b159-f1224384fd0b",
|
||||
"assets": 0,
|
||||
"evidence": 0
|
||||
"assets": [],
|
||||
"evidence": [],
|
||||
"relations": [
|
||||
"Visibility OR이 Keyset Index를 깨뜨린 문제",
|
||||
"Feed Pagination은 Keyset을 사용한다",
|
||||
"PostgreSQL Query Plan 측정 기준"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "JPA N+1 정량 진단 기준",
|
||||
"slug": "nplus1-quantitative-diagnosis",
|
||||
"file": "jpa-feed-query-performance/reference/reference-nplus1-quantitative-diagnosis.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "b0b55ac9-c0a3-4c01-ba84-0aa478923ace",
|
||||
"assets": 0,
|
||||
"evidence": 0
|
||||
"assets": [],
|
||||
"evidence": [],
|
||||
"relations": [
|
||||
"Fetch 타입이 아니라 조회 방식이 만든 ToOne N+1",
|
||||
"PostgreSQL Query Plan 측정 기준"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "PostgreSQL Query Plan 측정 기준",
|
||||
"slug": "postgresql-query-plan-measurement",
|
||||
"file": "jpa-feed-query-performance/reference/reference-postgresql-query-plan-measurement.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "e8c2e9ea-cd87-46f8-9469-849dbd433d86",
|
||||
"assets": 0,
|
||||
"evidence": 0
|
||||
"assets": [],
|
||||
"evidence": [],
|
||||
"relations": [
|
||||
"Query Plan은 실제 PostgreSQL에서 측정한다",
|
||||
"JPA N+1 정량 진단 기준",
|
||||
"ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Top-N-per-group 선택 기준",
|
||||
"slug": "top-n-per-group-selection",
|
||||
"file": "jpa-feed-query-performance/reference/reference-top-n-per-group-selection.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "bf5f2462-0e94-4723-bdc8-f7dd709b2dbb",
|
||||
"assets": 0,
|
||||
"evidence": 0
|
||||
"assets": [],
|
||||
"evidence": [],
|
||||
"relations": [
|
||||
"Projection 이후에도 1,509행을 읽은 Row Over-fetch",
|
||||
"PostgreSQL Query Plan 측정 기준",
|
||||
"Fetch Join · Batch · Projection 선택 기준"
|
||||
]
|
||||
}
|
||||
],
|
||||
"question": [
|
||||
@@ -125,46 +228,72 @@
|
||||
"title": "ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가",
|
||||
"slug": "cardinality-estimate-after-analyze",
|
||||
"file": "jpa-feed-query-performance/question/question-cardinality-estimate-after-analyze.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "e1e0e2a0-c6b6-45bf-be42-f697ba5e2fff",
|
||||
"assets": 0,
|
||||
"evidence": 0
|
||||
"assets": [],
|
||||
"evidence": [],
|
||||
"relations": [
|
||||
"PostgreSQL Query Plan 측정 기준",
|
||||
"Fetch 타입이 아니라 조회 방식이 만든 ToOne N+1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "실제 동시 트래픽에서도 이 구조가 안정적인가",
|
||||
"slug": "concurrency-stability",
|
||||
"file": "jpa-feed-query-performance/question/question-concurrency-stability.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "6cbe963f-86f8-4df6-be5a-900712970d01",
|
||||
"assets": 0,
|
||||
"evidence": 0
|
||||
"assets": [],
|
||||
"evidence": [],
|
||||
"relations": [
|
||||
"PostgreSQL Query Plan 측정 기준",
|
||||
"feed_visible을 Production CQRS로 승격할 것인가"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Round Trip과 Row Volume을 독립 측정할 것인가",
|
||||
"slug": "isolate-round-trip-and-row-volume",
|
||||
"file": "jpa-feed-query-performance/question/question-isolate-round-trip-and-row-volume.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "5159c415-232d-424a-970a-b0db52746767",
|
||||
"assets": 0,
|
||||
"evidence": 0
|
||||
"assets": [],
|
||||
"evidence": [],
|
||||
"relations": [
|
||||
"JPA N+1 정량 진단 기준",
|
||||
"Fetch 타입이 아니라 조회 방식이 만든 ToOne N+1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Highlight 없는 FeedItem을 허용할 것인가",
|
||||
"slug": "nullable-first-highlighted-at",
|
||||
"file": "jpa-feed-query-performance/question/question-nullable-first-highlighted-at.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "b099ca65-bf9f-4d61-814c-74722453fa3c",
|
||||
"assets": 0,
|
||||
"evidence": 0
|
||||
"assets": [],
|
||||
"evidence": [],
|
||||
"relations": [
|
||||
"Keyset Pagination 설계 기준",
|
||||
"Visibility OR이 Keyset Index를 깨뜨린 문제"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "feed_visible을 Production CQRS로 승격할 것인가",
|
||||
"slug": "promote-feed-visible-to-cqrs",
|
||||
"file": "jpa-feed-query-performance/question/question-promote-feed-visible-to-cqrs.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "5088ce14-b096-41d3-abba-64b7afb48bb9",
|
||||
"assets": 0,
|
||||
"evidence": 0
|
||||
"assets": [],
|
||||
"evidence": [],
|
||||
"relations": [
|
||||
"Feed Visibility Query Pattern",
|
||||
"현재 Read Model은 CQRS-lite로 유지한다",
|
||||
"Visibility OR이 Keyset Index를 깨뜨린 문제"
|
||||
]
|
||||
}
|
||||
],
|
||||
"decision": [
|
||||
@@ -172,64 +301,106 @@
|
||||
"title": "Entity Graph 조회에는 Batch Fetch를 사용한다",
|
||||
"slug": "batch-fetch-for-entity-graph",
|
||||
"file": "jpa-feed-query-performance/decision/decision-batch-fetch-for-entity-graph.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "08a74b35-10c3-4874-8fbc-209b0b6e942e",
|
||||
"assets": 0,
|
||||
"evidence": 0
|
||||
"assets": [],
|
||||
"evidence": [],
|
||||
"relations": [
|
||||
"Fetch Join · Batch · Projection 선택 기준",
|
||||
"Collection Fetch Join Pagination의 In-memory Paging",
|
||||
"Projection 이후에도 1,509행을 읽은 Row Over-fetch"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "현재 Read Model은 CQRS-lite로 유지한다",
|
||||
"slug": "keep-read-model-as-cqrs-lite",
|
||||
"file": "jpa-feed-query-performance/decision/decision-keep-read-model-as-cqrs-lite.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "7f248f68-ce2b-43ec-94ce-82324d0bd1a7",
|
||||
"assets": 0,
|
||||
"evidence": 0
|
||||
"assets": [],
|
||||
"evidence": [],
|
||||
"relations": [
|
||||
"feed_visible을 Production CQRS로 승격할 것인가",
|
||||
"화면 조회는 Read Projection을 사용한다",
|
||||
"Feed Visibility Query Pattern"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Feed Pagination은 Keyset을 사용한다",
|
||||
"slug": "keyset-for-feed-pagination",
|
||||
"file": "jpa-feed-query-performance/decision/decision-keyset-for-feed-pagination.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "1dbce381-f0dc-4d49-ad68-bd31d205677e",
|
||||
"assets": 0,
|
||||
"evidence": 0
|
||||
"assets": [],
|
||||
"evidence": [],
|
||||
"relations": [
|
||||
"Keyset Pagination 설계 기준",
|
||||
"Visibility OR이 Keyset Index를 깨뜨린 문제",
|
||||
"Highlight 없는 FeedItem을 허용할 것인가"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Query Plan은 실제 PostgreSQL에서 측정한다",
|
||||
"slug": "measure-plan-on-real-postgresql",
|
||||
"file": "jpa-feed-query-performance/decision/decision-measure-plan-on-real-postgresql.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "ae6c9bea-d3a3-46e1-bbd4-8d580d336394",
|
||||
"assets": 0,
|
||||
"evidence": 0
|
||||
"assets": [],
|
||||
"evidence": [],
|
||||
"relations": [
|
||||
"PostgreSQL Query Plan 측정 기준",
|
||||
"Fetch 타입이 아니라 조회 방식이 만든 ToOne N+1",
|
||||
"Visibility OR이 Keyset Index를 깨뜨린 문제"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Collection Fetch Join과 Pagination을 같이 사용하지 않는다",
|
||||
"slug": "no-collection-fetch-join-with-pagination",
|
||||
"file": "jpa-feed-query-performance/decision/decision-no-collection-fetch-join-with-pagination.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "5e4d033c-d6fe-4257-a4dc-1ade44473c72",
|
||||
"assets": 0,
|
||||
"evidence": 0
|
||||
"assets": [],
|
||||
"evidence": [],
|
||||
"relations": [
|
||||
"Collection Fetch Join Pagination의 In-memory Paging",
|
||||
"Fetch Join으로 N+1을 해결하다 만난 MultiBag과 행 폭증",
|
||||
"Fetch Join · Batch · Projection 선택 기준"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Query Strategy는 FeedQueryPort 뒤에서 소유한다",
|
||||
"slug": "query-strategy-behind-port",
|
||||
"file": "jpa-feed-query-performance/decision/decision-query-strategy-behind-port.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "4e3200c8-eff5-4442-ae84-ae7b7fa92c8b",
|
||||
"assets": 0,
|
||||
"evidence": 0
|
||||
"assets": [],
|
||||
"evidence": [],
|
||||
"relations": [
|
||||
"Fetch Join · Batch · Projection 선택 기준",
|
||||
"Collection Fetch Join Pagination의 In-memory Paging",
|
||||
"화면 조회는 Read Projection을 사용한다"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "화면 조회는 Read Projection을 사용한다",
|
||||
"slug": "read-projection-for-screen-query",
|
||||
"file": "jpa-feed-query-performance/decision/decision-read-projection-for-screen-query.md",
|
||||
"readiness": "READY",
|
||||
"status": "게시 전",
|
||||
"studioId": "30a37f34-b406-4061-b924-e22e0be0c3bf",
|
||||
"assets": 0,
|
||||
"evidence": 0
|
||||
"assets": [],
|
||||
"evidence": [],
|
||||
"relations": [
|
||||
"Projection 이후에도 1,509행을 읽은 Row Over-fetch",
|
||||
"Fetch Join · Batch · Projection 선택 기준",
|
||||
"Query Strategy는 FeedQueryPort 뒤에서 소유한다"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user