chore: 문서를 작성할 때 한국어의 표현 작성 스킬 추가 및 1인칭 관점의 글 작성 검증 테스트 추가
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
-- Crown Task 4 — deep-page keyset on the precompute parent path (index-range seek, ~19 rows)
|
||||
-- FeedCrownIT.crownDeepPageKeysetSeeksFewerRowsWithPrecomputeThanSingleOr (seed 2000, user008, deepest page)
|
||||
-- 읽기 포인트 (★ 실측 정정): 깊은 커서에선 사전계산 경로도 작은 Sort 가 붙는다 — Bitmap Index Scan on
|
||||
-- ix_feed_visible 이 남은 19행만 인덱스 range(Index Cond 에 ROW(...) < ROW(cursor))로 훑고, Bitmap 은
|
||||
-- 정렬 출력을 안 하므로 19행을 quicksort(26kB). 핵심: 훑는 행수 19, 부모 buffers 3 — 페이지 근방만 만진다
|
||||
-- (OFFSET 의 scan-then-discard 도, 단일 OR 의 전체 가시성 재해소도 아니다).
|
||||
|
||||
Nested Loop (cost=22.44..299.62 rows=60 width=686) (actual time=0.023..0.059 rows=19 loops=1)
|
||||
Buffers: shared hit=60
|
||||
-> Limit (cost=22.17..22.22 rows=20 width=24) (actual time=0.013..0.015 rows=19 loops=1)
|
||||
Buffers: shared hit=3
|
||||
-> Sort (cost=22.17..22.22 rows=21 width=24) (actual time=0.013..0.014 rows=19 loops=1)
|
||||
Sort Key: feed_visible.first_highlighted_at DESC, feed_visible.feed_item_id DESC
|
||||
Sort Method: quicksort Memory: 26kB
|
||||
Buffers: shared hit=3
|
||||
-> Bitmap Heap Scan on feed_visible (cost=4.49..21.71 rows=21 width=24) (actual time=0.005..0.006 rows=19 loops=1)
|
||||
Recheck Cond: ((viewer_id = 'e6a2f6da-c975-41ef-9b48-a4a6c1a72cc0'::uuid) AND (ROW(first_highlighted_at, feed_item_id) < ROW('2026-04-17 21:00:00+00'::timestamp with time zone, '759efcc1-efd0-4a24-94d6-7e83001fabb1'::uuid)))
|
||||
Heap Blocks: exact=1
|
||||
Buffers: shared hit=3
|
||||
-> Bitmap Index Scan on ix_feed_visible (cost=0.00..4.49 rows=21 width=0) (actual time=0.003..0.003 rows=19 loops=1)
|
||||
Index Cond: ((viewer_id = 'e6a2f6da-c975-41ef-9b48-a4a6c1a72cc0'::uuid) AND (ROW(first_highlighted_at, feed_item_id) < ROW('2026-04-17 21:00:00+00'::timestamp with time zone, '759efcc1-efd0-4a24-94d6-7e83001fabb1'::uuid)))
|
||||
Buffers: shared hit=2
|
||||
-> Limit (cost=0.28..13.83 rows=3 width=670) (actual time=0.002..0.002 rows=1 loops=19)
|
||||
Buffers: shared hit=57
|
||||
-> Index Scan using ix_highlights_feed_items_created on highlights h (cost=0.28..36.42 rows=8 width=670) (actual time=0.002..0.002 rows=1 loops=19)
|
||||
Index Cond: (feed_item_id = feed_visible.feed_item_id)
|
||||
Buffers: shared hit=57
|
||||
Planning Time: 0.075 ms
|
||||
Execution Time: 0.118 ms
|
||||
@@ -0,0 +1,47 @@
|
||||
-- Crown Task 4 — deep-page keyset on the single-OR parent path (re-resolves visibility every page)
|
||||
-- FeedCrownIT.crownDeepPageKeysetSeeksFewerRowsWithPrecomputeThanSingleOr (seed 2000, user008, deepest page)
|
||||
-- 읽기 포인트: 단일 OR 부모선택은 사전계산 읽기 모델(ix_feed_visible)을 못 쓴다(구조적). 매 페이지 가시성
|
||||
-- 3분기를 BitmapOr 로 다시 풀고(public/mentioned = ix_feed_items_visibility_sort, private = ix_feed_items_private),
|
||||
-- 멘션 EXISTS 는 hashed SubPlan 2 로 user008 의 멘션 200행을 materialize 한다 → 훑는 행수 200(사전계산 19 대비).
|
||||
-- 커서는 세 분기 Index Cond 에 ROW(...) < ROW(cursor) 로 들어가 seek 은 하나, 페이지마다 전체 가시성을 재계산한다.
|
||||
|
||||
Nested Loop (cost=94.10..163.18 rows=15 width=686) (actual time=0.128..0.161 rows=19 loops=1)
|
||||
Buffers: shared hit=88
|
||||
-> Limit (cost=93.82..93.83 rows=5 width=24) (actual time=0.118..0.120 rows=19 loops=1)
|
||||
Buffers: shared hit=31
|
||||
-> Sort (cost=93.82..93.83 rows=5 width=24) (actual time=0.117..0.119 rows=19 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id DESC
|
||||
Sort Method: quicksort Memory: 26kB
|
||||
Buffers: shared hit=31
|
||||
-> Bitmap Heap Scan on feed_items fi (cost=12.90..93.76 rows=5 width=24) (actual time=0.027..0.112 rows=19 loops=1)
|
||||
Recheck Cond: ((((visibility)::text = 'PUBLIC'::text) AND (ROW(first_highlighted_at, id) < ROW('2026-04-17 21:00:00+00'::timestamp with time zone, '759efcc1-efd0-4a24-94d6-7e83001fabb1'::uuid))) OR (((visibility)::text = 'MENTIONED'::text) AND (ROW(first_highlighted_at, id) < ROW('2026-04-17 21:00:00+00'::timestamp with time zone, '759efcc1-efd0-4a24-94d6-7e83001fabb1'::uuid))) OR ((user_id = 'e6a2f6da-c975-41ef-9b48-a4a6c1a72cc0'::uuid) AND (ROW(first_highlighted_at, id) < ROW('2026-04-17 21:00:00+00'::timestamp with time zone, '759efcc1-efd0-4a24-94d6-7e83001fabb1'::uuid)) AND ((visibility)::text = 'PRIVATE'::text)))
|
||||
Filter: (((visibility)::text = 'PUBLIC'::text) OR (((visibility)::text = 'MENTIONED'::text) AND (hashed SubPlan 2)) OR (((visibility)::text = 'PRIVATE'::text) AND (user_id = 'e6a2f6da-c975-41ef-9b48-a4a6c1a72cc0'::uuid)))
|
||||
Rows Removed by Filter: 4
|
||||
Heap Blocks: exact=6
|
||||
Buffers: shared hit=31
|
||||
-> BitmapOr (cost=12.90..12.90 rows=7 width=0) (actual time=0.019..0.019 rows=0 loops=1)
|
||||
Buffers: shared hit=6
|
||||
-> Bitmap Index Scan on ix_feed_items_visibility_sort (cost=0.00..4.31 rows=3 width=0) (actual time=0.013..0.014 rows=48 loops=1)
|
||||
Index Cond: (((visibility)::text = 'PUBLIC'::text) AND (ROW(first_highlighted_at, id) < ROW('2026-04-17 21:00:00+00'::timestamp with time zone, '759efcc1-efd0-4a24-94d6-7e83001fabb1'::uuid)))
|
||||
Buffers: shared hit=2
|
||||
-> Bitmap Index Scan on ix_feed_items_visibility_sort (cost=0.00..4.31 rows=3 width=0) (actual time=0.003..0.003 rows=18 loops=1)
|
||||
Index Cond: (((visibility)::text = 'MENTIONED'::text) AND (ROW(first_highlighted_at, id) < ROW('2026-04-17 21:00:00+00'::timestamp with time zone, '759efcc1-efd0-4a24-94d6-7e83001fabb1'::uuid)))
|
||||
Buffers: shared hit=2
|
||||
-> Bitmap Index Scan on ix_feed_items_private (cost=0.00..4.28 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=1)
|
||||
Index Cond: ((user_id = 'e6a2f6da-c975-41ef-9b48-a4a6c1a72cc0'::uuid) AND (ROW(first_highlighted_at, id) < ROW('2026-04-17 21:00:00+00'::timestamp with time zone, '759efcc1-efd0-4a24-94d6-7e83001fabb1'::uuid)))
|
||||
Buffers: shared hit=2
|
||||
SubPlan 2
|
||||
-> Bitmap Heap Scan on feed_item_mentions m (cost=4.33..24.04 rows=7 width=16) (actual time=0.014..0.038 rows=200 loops=1)
|
||||
Recheck Cond: (mentioned_user_id = 'e6a2f6da-c975-41ef-9b48-a4a6c1a72cc0'::uuid)
|
||||
Heap Blocks: exact=16
|
||||
Buffers: shared hit=19
|
||||
-> Bitmap Index Scan on ix_mentions_user (cost=0.00..4.33 rows=7 width=0) (actual time=0.011..0.011 rows=200 loops=1)
|
||||
Index Cond: (mentioned_user_id = 'e6a2f6da-c975-41ef-9b48-a4a6c1a72cc0'::uuid)
|
||||
Buffers: shared hit=3
|
||||
-> Limit (cost=0.28..13.83 rows=3 width=670) (actual time=0.002..0.002 rows=1 loops=19)
|
||||
Buffers: shared hit=57
|
||||
-> Index Scan using ix_highlights_feed_items_created on highlights h (cost=0.28..36.42 rows=8 width=670) (actual time=0.002..0.002 rows=1 loops=19)
|
||||
Index Cond: (feed_item_id = fi.id)
|
||||
Buffers: shared hit=57
|
||||
Planning Time: 0.153 ms
|
||||
Execution Time: 0.250 ms
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
-- Crown Task 4 — unified feed query (precompute parent path): visibility + keyset + Top-N in ONE plan
|
||||
-- FeedCrownIT.crownUnifiedPlanStacksVisibilityKeysetAndTopN (seed 2000, viewer user008, page 1)
|
||||
-- 읽기 포인트: 세 기법이 한 플랜에 재정렬(Sort) 없이 겹쳐 있다 —
|
||||
-- (1) 가시성+keyset = feed_visible 커버링 인덱스의 Index Only Scan (viewer_id 조건, Heap Fetches 20),
|
||||
-- (2) Top-N = 부모 20건당 ix_highlights_feed_items_created 로 top-3 index seek (Nested Loop = LATERAL),
|
||||
-- (3) Sort 노드 없음 — 두 순서(부모 keyset·자식 created_at)가 모두 인덱스에서 나온다.
|
||||
|
||||
Nested Loop (cost=0.56..271.10 rows=60 width=686) (actual time=0.036..0.092 rows=60 loops=1)
|
||||
Buffers: shared hit=65 read=2
|
||||
-> Limit (cost=0.28..1.84 rows=20 width=24) (actual time=0.025..0.028 rows=20 loops=1)
|
||||
Buffers: shared hit=1 read=2
|
||||
-> Index Only Scan using ix_feed_visible on feed_visible (cost=0.28..117.22 rows=1500 width=24) (actual time=0.025..0.027 rows=20 loops=1)
|
||||
Index Cond: (viewer_id = '9ed28556-7ab3-4f5a-b327-7e0dd9536bf8'::uuid)
|
||||
Heap Fetches: 20
|
||||
Buffers: shared hit=1 read=2
|
||||
-> Limit (cost=0.28..13.42 rows=3 width=670) (actual time=0.003..0.003 rows=3 loops=20)
|
||||
Buffers: shared hit=64
|
||||
-> Index Scan using ix_highlights_feed_items_created on highlights h (cost=0.28..48.47 rows=11 width=670) (actual time=0.002..0.003 rows=3 loops=20)
|
||||
Index Cond: (feed_item_id = feed_visible.feed_item_id)
|
||||
Buffers: shared hit=64
|
||||
Planning:
|
||||
Buffers: shared hit=11 read=1
|
||||
Planning Time: 0.122 ms
|
||||
Execution Time: 0.120 ms
|
||||
@@ -0,0 +1,20 @@
|
||||
Plan A — 반복되는 하이라이트 자식 쿼리의 실행계획
|
||||
출처: FeedPersistenceIT.l1ExplainRepeatedHighlightChildQuery 콘솔 출력
|
||||
조건: 대량 시드 직후, ANALYZE 미실행. warm buffer cache(shared read=0).
|
||||
쿼리: SELECT * FROM highlights WHERE feed_item_id = ? (ORDER BY / LIMIT 없음)
|
||||
|
||||
Index Scan using ix_highlights_feed_items_created on highlights
|
||||
(cost=0.27..8.29 rows=1 width=710) (actual time=0.026..0.123 rows=500 loops=1)
|
||||
Index Cond: (feed_item_id = '2b5b931f-...'::uuid)
|
||||
Buffers: shared hit=14
|
||||
Planning Time: 0.086 ms
|
||||
Execution Time: 0.173 ms
|
||||
|
||||
주의(문서 §6.4 caveat와 동일):
|
||||
- 플래너 추정 rows=1 vs 실제 rows=500 → 500배 오추정. 시드 후 ANALYZE 미실행으로 통계가
|
||||
feed_item_id별 편중을 반영하지 못한 것으로 보임. EXPLAIN 전 `ANALYZE highlights` 필요.
|
||||
- Buffers: shared hit=14, read=0 → warm buffer cache 결과. cold 디스크 I/O 실행시간으로 읽지 말 것.
|
||||
- Execution Time 0.173 ms는 PostgreSQL executor 내부 시간. ORM 엔티티 생성·JDBC 결과 전달·
|
||||
DTO 매핑·직렬화·HTTP를 포함하지 않으므로 애플리케이션 지연(§6.2)과 같은 지표가 아니다.
|
||||
|
||||
Plan B (`ANALYZE highlights` 실행 후 재측정) — 예정(pending). 아직 미실행이므로 값 없음.
|
||||
@@ -0,0 +1,29 @@
|
||||
-- L14 index toggle — the SAME LATERAL query with ix_highlights_feed_items_created DROPPED
|
||||
-- FeedTopNIT.l14LateralDependsOnCompositeIndex (seed 1000, page 20, K=3; index dropped then restored)
|
||||
-- 읽기 포인트: 인덱스가 없으면 부모마다 highlights 를 Seq Scan 하고(Rows Removed by Filter: 2842/loop)
|
||||
-- top-N heapsort 로 3개를 고른다 → child 쪽 buffers shared hit=4340, 전체 4446 (인덱스판 168 의 ~26배),
|
||||
-- Execution 5.472 ms (인덱스판 0.336 ms 의 ~16배). 결론: LATERAL 이 빠른 건 LATERAL 이 아니라
|
||||
-- (feed_item_id, created_at DESC) 인덱스 seek 덕. 인덱스가 없으면 LATERAL 도 무너진다.
|
||||
|
||||
Nested Loop (cost=376.28..5070.14 rows=60 width=686) (actual time=0.599..5.451 rows=60 loops=1)
|
||||
Buffers: shared hit=4446
|
||||
-> Limit (cost=129.28..129.33 rows=20 width=24) (actual time=0.218..0.221 rows=20 loops=1)
|
||||
Buffers: shared hit=106
|
||||
-> Sort (cost=129.28..130.87 rows=636 width=24) (actual time=0.218..0.219 rows=20 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id
|
||||
Sort Method: top-N heapsort Memory: 26kB
|
||||
Buffers: shared hit=106
|
||||
-> Seq Scan on feed_items fi (cost=0.00..112.36 rows=636 width=24) (actual time=0.086..0.150 rows=1000 loops=1)
|
||||
Buffers: shared hit=106
|
||||
-> Limit (cost=246.99..247.00 rows=3 width=670) (actual time=0.261..0.261 rows=3 loops=20)
|
||||
Buffers: shared hit=4340
|
||||
-> Sort (cost=246.99..247.02 rows=12 width=670) (actual time=0.259..0.259 rows=3 loops=20)
|
||||
Sort Key: h.created_at DESC
|
||||
Sort Method: top-N heapsort Memory: 25kB
|
||||
Buffers: shared hit=4340
|
||||
-> Seq Scan on highlights h (cost=0.00..246.84 rows=12 width=670) (actual time=0.171..0.249 rows=75 loops=20)
|
||||
Filter: (feed_item_id = fi.id)
|
||||
Rows Removed by Filter: 2842
|
||||
Buffers: shared hit=4340
|
||||
Planning Time: 0.079 ms
|
||||
Execution Time: 5.472 ms
|
||||
@@ -0,0 +1,26 @@
|
||||
-- L14 (b) LATERAL top-3 per parent — the winning strategy (index seek)
|
||||
-- FeedTopNIT.l14ExplainThreeWayPlanCompareIsTheCrownJewel (seed 1000, page 20, K=3)
|
||||
-- SELECT p.id, top3.color, top3.text, top3.created_at
|
||||
-- FROM (SELECT fi.id FROM feed_items fi ORDER BY fi.first_highlighted_at DESC, fi.id ASC LIMIT 20) p
|
||||
-- CROSS JOIN LATERAL (SELECT h.color, h.text, h.created_at FROM highlights h
|
||||
-- WHERE h.feed_item_id = p.id ORDER BY h.created_at DESC LIMIT 3) top3
|
||||
-- 읽기 포인트: 부모마다 ix_highlights_feed_items_created 를 Index Scan 하고 Limit 3 에서 멈춘다
|
||||
-- (loops=20, 각 rows=3). buffers shared hit=204 로 세 해법 중 최소.
|
||||
|
||||
Nested Loop (cost=172.25..432.10 rows=60 width=686) (actual time=0.257..0.310 rows=60 loops=1)
|
||||
Buffers: shared hit=204
|
||||
-> Limit (cost=171.97..172.02 rows=20 width=24) (actual time=0.239..0.240 rows=20 loops=1)
|
||||
Buffers: shared hit=141
|
||||
-> Sort (cost=171.97..174.09 rows=846 width=24) (actual time=0.238..0.239 rows=20 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id
|
||||
Sort Method: top-N heapsort Memory: 26kB
|
||||
Buffers: shared hit=141
|
||||
-> Seq Scan on feed_items fi (cost=0.00..149.46 rows=846 width=24) (actual time=0.111..0.175 rows=1000 loops=1)
|
||||
Buffers: shared hit=141
|
||||
-> Limit (cost=0.28..12.96 rows=3 width=670) (actual time=0.003..0.003 rows=3 loops=20)
|
||||
Buffers: shared hit=63
|
||||
-> Index Scan using ix_highlights_feed_items_created on highlights h (cost=0.28..80.61 rows=19 width=670) (actual time=0.003..0.003 rows=3 loops=20)
|
||||
Index Cond: (feed_item_id = fi.id)
|
||||
Buffers: shared hit=63
|
||||
Planning Time: 0.068 ms
|
||||
Execution Time: 0.323 ms
|
||||
@@ -0,0 +1,26 @@
|
||||
-- L14 (c) two-step IN + app-side cut — correct result but transfers ALL page-parent highlights
|
||||
-- FeedTopNIT.l14ExplainThreeWayPlanCompareIsTheCrownJewel (seed 1000, page 20)
|
||||
-- SELECT h.feed_item_id, h.color, h.text, h.created_at FROM highlights h
|
||||
-- WHERE h.feed_item_id IN (<page-20 ids>) ORDER BY h.feed_item_id, h.created_at DESC
|
||||
-- 읽기 포인트: window 와 동일한 Hash Semi Join(rows=1509) — 다만 위에 WindowAgg 가 없어 1509행을 전량
|
||||
-- 애플리케이션으로 전송한다(앱에서 부모별 top-3 컷). buffers shared hit=430 (window 과 동일 = 같은 스캔).
|
||||
-- = L6 프로젝션이 남긴 잔여(childRows=1509)의 정체. 전송 낭비: 60행이면 되는데 1509행.
|
||||
|
||||
Sort (cost=531.52..532.49 rows=388 width=686) (actual time=1.586..1.625 rows=1509 loops=1)
|
||||
Sort Key: h.feed_item_id, h.created_at DESC
|
||||
Sort Method: quicksort Memory: 155kB
|
||||
Buffers: shared hit=430
|
||||
-> Hash Semi Join (cost=172.47..514.84 rows=388 width=686) (actual time=0.493..0.833 rows=1509 loops=1)
|
||||
Hash Cond: (h.feed_item_id = "ANY_subquery".id)
|
||||
Buffers: shared hit=430
|
||||
-> Seq Scan on highlights h (cost=0.00..327.85 rows=3885 width=686) (actual time=0.239..0.363 rows=2917 loops=1)
|
||||
Buffers: shared hit=289
|
||||
-> Hash (cost=172.22..172.22 rows=20 width=16) (actual time=0.251..0.252 rows=20 loops=1)
|
||||
Buffers: shared hit=141
|
||||
-> Subquery Scan on "ANY_subquery" (cost=171.97..172.22 rows=20 width=16) (actual time=0.239..0.241 rows=20 loops=1)
|
||||
-> Limit (cost=171.97..172.02 rows=20 width=24) (actual time=0.238..0.240 rows=20 loops=1)
|
||||
-> Sort (cost=171.97..174.09 rows=846 width=24) (actual time=0.238..0.238 rows=20 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id
|
||||
-> Seq Scan on feed_items fi (cost=0.00..149.46 rows=846 width=24) (actual time=0.112..0.175 rows=1000 loops=1)
|
||||
Planning Time: 0.060 ms
|
||||
Execution Time: 1.686 ms
|
||||
@@ -0,0 +1,33 @@
|
||||
-- L14 (a) window row_number() <= 3 — cuts in the DB but scans the whole partition
|
||||
-- FeedTopNIT.l14ExplainThreeWayPlanCompareIsTheCrownJewel (seed 1000, page 20, K=3)
|
||||
-- SELECT t.feed_item_id, t.color, t.text, t.created_at FROM (
|
||||
-- SELECT h.feed_item_id, h.color, h.text, h.created_at,
|
||||
-- row_number() OVER (PARTITION BY h.feed_item_id ORDER BY h.created_at DESC) AS rn
|
||||
-- FROM highlights h WHERE h.feed_item_id IN (<page-20 ids>)) t WHERE t.rn <= 3
|
||||
-- 읽기 포인트: Hash Semi Join 이 페이지 부모들의 하이라이트 전량(rows=1509)을 읽고 Sort 한 뒤 WindowAgg 가
|
||||
-- 순번을 매긴다. PG 15+ 는 rn<=3 을 WindowAgg 의 Run Condition 으로 밀어넣지만, 파티션 정렬은
|
||||
-- 이미 1509행 전량을 훑는다. 반환은 60행이지만 buffers shared hit=430 (two-step 과 같다 = 같은 스캔).
|
||||
|
||||
Subquery Scan on t (cost=531.52..543.16 rows=388 width=686) (actual time=1.376..1.486 rows=60 loops=1)
|
||||
Buffers: shared hit=430
|
||||
-> WindowAgg (cost=531.52..539.28 rows=388 width=694) (actual time=1.375..1.482 rows=60 loops=1)
|
||||
Run Condition: (row_number() OVER (?) <= 3)
|
||||
Buffers: shared hit=430
|
||||
-> Sort (cost=531.52..532.49 rows=388 width=686) (actual time=1.369..1.406 rows=1509 loops=1)
|
||||
Sort Key: h.feed_item_id, h.created_at DESC
|
||||
Sort Method: quicksort Memory: 155kB
|
||||
Buffers: shared hit=430
|
||||
-> Hash Semi Join (cost=172.47..514.84 rows=388 width=686) (actual time=0.590..0.941 rows=1509 loops=1)
|
||||
Hash Cond: (h.feed_item_id = "ANY_subquery".id)
|
||||
Buffers: shared hit=430
|
||||
-> Seq Scan on highlights h (cost=0.00..327.85 rows=3885 width=686) (actual time=0.289..0.421 rows=2917 loops=1)
|
||||
Buffers: shared hit=289
|
||||
-> Hash (cost=172.22..172.22 rows=20 width=16) (actual time=0.287..0.288 rows=20 loops=1)
|
||||
Buffers: shared hit=141
|
||||
-> Subquery Scan on "ANY_subquery" (cost=171.97..172.22 rows=20 width=16) (actual time=0.277..0.280 rows=20 loops=1)
|
||||
-> Limit (cost=171.97..172.02 rows=20 width=24) (actual time=0.277..0.279 rows=20 loops=1)
|
||||
-> Sort (cost=171.97..174.09 rows=846 width=24) (actual time=0.276..0.277 rows=20 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id
|
||||
-> Seq Scan on feed_items fi (cost=0.00..149.46 rows=846 width=24) (actual time=0.146..0.212 rows=1000 loops=1)
|
||||
Planning Time: 0.123 ms
|
||||
Execution Time: 1.552 ms
|
||||
@@ -0,0 +1,16 @@
|
||||
-- L15 keyset WITH sort-key index (range seek) — same deep page, offset 1980 equivalent cursor
|
||||
-- FeedKeysetIT.l15ExplainOffsetScansThenDiscardsKeysetSeeksAndNeedsIndex (seed 2000)
|
||||
-- SELECT fi.id, fi.first_highlighted_at FROM feed_items fi
|
||||
-- WHERE (fi.first_highlighted_at, fi.id) < (TIMESTAMPTZ '...', '...'::uuid) -- cursor = 이전 페이지 마지막 행
|
||||
-- ORDER BY fi.first_highlighted_at DESC, fi.id DESC LIMIT 20
|
||||
-- 읽기 포인트: Index Only Scan(커버링) 으로 커서 이후 20행만 seek — actual rows=20, Heap Fetches=20, buffers 1(+2 read).
|
||||
-- 순서가 인덱스로 보장돼 Sort 노드가 없다. 페이지 깊이와 무관하게 상수(vs OFFSET 의 2000).
|
||||
|
||||
Limit (cost=0.28..18.14 rows=20 width=24) (actual time=0.054..0.060 rows=20 loops=1)
|
||||
Buffers: shared hit=1 read=2
|
||||
-> Index Only Scan using ix_feed_items_keyset on feed_items fi (cost=0.28..595.95 rows=667 width=24) (actual time=0.054..0.057 rows=20 loops=1)
|
||||
Index Cond: (ROW(first_highlighted_at, id) < ROW('2026-04-17 13:00:00+00'::timestamp with time zone, '17ab2b68-0981-43cc-a673-5757f7214899'::uuid))
|
||||
Heap Fetches: 20
|
||||
Buffers: shared hit=1 read=2
|
||||
Planning Time: 0.052 ms
|
||||
Execution Time: 0.076 ms
|
||||
@@ -0,0 +1,18 @@
|
||||
-- L15 keyset WITHOUT the sort-key index — same query, ix_feed_items_keyset absent
|
||||
-- FeedKeysetIT.l15ExplainOffsetScansThenDiscardsKeysetSeeksAndNeedsIndex (seed 2000)
|
||||
-- 읽기 포인트: 결과 행(20)은 필터로 같지만, 정렬키 인덱스가 없어 Seq Scan 으로 2000 heap 행을 훑고
|
||||
-- (Rows Removed by Filter: 1980) Sort 한다 → buffers shared hit=141 (Index Only Scan 판의 ~140배).
|
||||
-- OFFSET(141)과 같은 buffers = 둘 다 전량 heap 접근. 정렬키 인덱스가 keyset 의 전제라는 증거.
|
||||
|
||||
Limit (cost=204.50..204.55 rows=20 width=24) (actual time=0.338..0.341 rows=20 loops=1)
|
||||
Buffers: shared hit=141
|
||||
-> Sort (cost=204.50..206.72 rows=887 width=24) (actual time=0.337..0.339 rows=20 loops=1)
|
||||
Sort Key: first_highlighted_at DESC, id DESC
|
||||
Sort Method: quicksort Memory: 26kB
|
||||
Buffers: shared hit=141
|
||||
-> Seq Scan on feed_items fi (cost=0.00..180.90 rows=887 width=24) (actual time=0.314..0.317 rows=20 loops=1)
|
||||
Filter: (ROW(first_highlighted_at, id) < ROW('2026-04-17 13:00:00+00'::timestamp with time zone, '17ab2b68-0981-43cc-a673-5757f7214899'::uuid))
|
||||
Rows Removed by Filter: 1980
|
||||
Buffers: shared hit=141
|
||||
Planning Time: 0.074 ms
|
||||
Execution Time: 0.373 ms
|
||||
@@ -0,0 +1,19 @@
|
||||
-- L15 OFFSET deep page (scan-then-discard) — page 100 of 100, offset 1980
|
||||
-- FeedKeysetIT.l15ExplainOffsetScansThenDiscardsKeysetSeeksAndNeedsIndex (seed 2000, keyset index present)
|
||||
-- SELECT fi.id, fi.first_highlighted_at FROM feed_items fi
|
||||
-- ORDER BY fi.first_highlighted_at DESC, fi.id DESC OFFSET 1980 LIMIT 20
|
||||
-- 읽기 포인트: 정렬키 인덱스가 있어도 깊은 페이지에선 Seq Scan(2000)+Sort(2000) 로 전량을 훑고 20만 남긴다.
|
||||
-- Limit 하위 actual rows=2000 = 결과 20행을 위해 훑은 행(over-scan = offset+20). buffers shared hit=141.
|
||||
|
||||
Limit (cost=275.61..275.66 rows=20 width=24) (actual time=0.945..0.949 rows=20 loops=1)
|
||||
Buffers: shared hit=141
|
||||
-> Sort (cost=270.66..275.66 rows=2000 width=24) (actual time=0.759..0.865 rows=2000 loops=1)
|
||||
Sort Key: first_highlighted_at DESC, id DESC
|
||||
Sort Method: quicksort Memory: 189kB
|
||||
Buffers: shared hit=141
|
||||
-> Seq Scan on feed_items fi (cost=0.00..161.00 rows=2000 width=24) (actual time=0.124..0.369 rows=2000 loops=1)
|
||||
Buffers: shared hit=141
|
||||
Planning:
|
||||
Buffers: shared hit=5 read=1
|
||||
Planning Time: 0.137 ms
|
||||
Execution Time: 0.996 ms
|
||||
@@ -0,0 +1,33 @@
|
||||
-- L15 probe (→ L16): keyset + visibility OR/EXISTS — the sort-key index is lost
|
||||
-- FeedKeysetIT.l15ProbeVisibilityOrBreaksKeysetIndex (seed 2000, ix_feed_items_keyset present)
|
||||
-- SELECT fi.id, fi.first_highlighted_at FROM feed_items fi
|
||||
-- WHERE (fi.visibility='PUBLIC'
|
||||
-- OR (fi.visibility='MENTIONED' AND EXISTS(SELECT 1 FROM feed_item_mentions m WHERE m.feed_item_id=fi.id AND m.mentioned_user_id=:me))
|
||||
-- OR (fi.visibility='PRIVATE' AND fi.user_id=:me))
|
||||
-- AND (fi.first_highlighted_at, fi.id) < (:cursor)
|
||||
-- ORDER BY fi.first_highlighted_at DESC, fi.id DESC LIMIT 20
|
||||
-- 읽기 포인트: ix_feed_items_keyset(정렬키) 를 못 탄다. 대신 BitmapOr(visibility 3분기 각각 ix_feed_items_visibility_sort)
|
||||
-- + BitmapAnd(private = visibility ∩ user_id) + SubPlan(mentions EXISTS). bitmap 은 순서를 안 주므로
|
||||
-- Sort 노드가 재등장 = keyset 의 "순서 seek, Sort 없음" 이점 소멸 → L16(UNION 분해로 각 분기를 정렬 보장 인덱스로).
|
||||
|
||||
Limit (cost=100.32..100.34 rows=5 width=24) (actual time=0.189..0.193 rows=13 loops=1)
|
||||
Buffers: shared hit=26
|
||||
-> Sort (cost=100.32..100.34 rows=5 width=24) (actual time=0.188..0.190 rows=13 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id DESC
|
||||
-> Bitmap Heap Scan on feed_items fi (actual time=0.114..0.178 rows=13 loops=1)
|
||||
Recheck Cond: (((visibility='PUBLIC') AND (ROW(first_highlighted_at, id) < ROW(cursor)))
|
||||
OR ((visibility='MENTIONED') AND (ROW(first_highlighted_at, id) < ROW(cursor)))
|
||||
OR ((visibility='PRIVATE') AND (ROW(first_highlighted_at, id) < ROW(cursor)) AND (user_id = :me)))
|
||||
Filter: ((visibility='PUBLIC') OR ((visibility='MENTIONED') AND (SubPlan 1)) OR ((visibility='PRIVATE') AND (user_id = :me)))
|
||||
Rows Removed by Filter: 3
|
||||
-> BitmapOr (actual time=0.093..0.094 rows=0 loops=1)
|
||||
-> Bitmap Index Scan on ix_feed_items_visibility_sort (Index Cond: visibility='PUBLIC' AND ROW(...) < ROW(cursor))
|
||||
-> Bitmap Index Scan on ix_feed_items_visibility_sort (Index Cond: visibility='MENTIONED' AND ROW(...) < ROW(cursor))
|
||||
-> BitmapAnd
|
||||
-> Bitmap Index Scan on ix_feed_items_visibility_sort (Index Cond: visibility='PRIVATE' AND ROW(...) < ROW(cursor))
|
||||
-> Bitmap Index Scan on uq_feed_items_user_page (Index Cond: user_id = :me)
|
||||
SubPlan 1
|
||||
-> Index Only Scan using uq_feed_item_mentions on feed_item_mentions m (loops=4)
|
||||
Index Cond: ((feed_item_id = fi.id) AND (mentioned_user_id = :me))
|
||||
Planning Time: 0.319 ms
|
||||
Execution Time: 0.325 ms
|
||||
@@ -0,0 +1,18 @@
|
||||
-- L16 (c) precompute (CQRS read model) — per-viewer feed_visible table, single covering index scan
|
||||
-- FeedVisibilityIT.l16ExplainThreeWayPlanCompare / l16PrecomputeIsSingleIndexScanNoOrNoSort (seed 2000)
|
||||
-- CREATE TABLE feed_visible AS SELECT :me AS viewer_id, fi.id AS feed_item_id, fi.first_highlighted_at
|
||||
-- FROM feed_items fi WHERE <visible to me>;
|
||||
-- CREATE INDEX ix_feed_visible ON feed_visible (viewer_id, first_highlighted_at DESC, feed_item_id DESC);
|
||||
-- SELECT feed_item_id AS id, first_highlighted_at FROM feed_visible
|
||||
-- WHERE viewer_id=:me ORDER BY first_highlighted_at DESC, feed_item_id DESC LIMIT 20
|
||||
-- 읽기 포인트: 단일 Index Only Scan(커버링) — OR 도 조인도 Sort 도 없다. buffers shared hit=1(+2 read),
|
||||
-- 훑는 행 20. 단일 OR(122)·UNION(200) 대비 order-of-magnitude 적음 = CQRS 읽기 모델의 정체.
|
||||
|
||||
Limit (cost=0.28..1.84 rows=20 width=24) (actual time=0.021..0.025 rows=20 loops=1)
|
||||
Buffers: shared hit=1 read=2
|
||||
-> Index Only Scan using ix_feed_visible on feed_visible (cost=0.28..117.22 rows=1500 width=24) (actual time=0.021..0.023 rows=20 loops=1)
|
||||
Index Cond: (viewer_id = :me)
|
||||
Heap Fetches: 20
|
||||
Buffers: shared hit=1 read=2
|
||||
Planning Time: 0.102 ms
|
||||
Execution Time: 0.034 ms
|
||||
@@ -0,0 +1,30 @@
|
||||
-- L16 (a) single OR — the naive visibility filter: BitmapOr + top-N Sort + hashed SubPlan
|
||||
-- FeedVisibilityIT.l16ExplainThreeWayPlanCompare (seed 2000, viewer user008)
|
||||
-- SELECT fi.id, fi.first_highlighted_at FROM feed_items fi
|
||||
-- WHERE (fi.visibility='PUBLIC'
|
||||
-- OR (fi.visibility='MENTIONED' AND EXISTS(SELECT 1 FROM feed_item_mentions m WHERE m.feed_item_id=fi.id AND m.mentioned_user_id=:me))
|
||||
-- OR (fi.visibility='PRIVATE' AND fi.user_id=:me))
|
||||
-- ORDER BY fi.first_highlighted_at DESC, fi.id DESC LIMIT 20
|
||||
-- 읽기 포인트: seq scan 이 아니라 BitmapOr(3분기 인덱스)로 후보 1500 을 heap scan → top-N Sort(순서 손실)
|
||||
-- + 멘션 EXISTS 는 hashed SubPlan(후보마다 반복 아님). buffers shared hit=122.
|
||||
|
||||
Limit (cost=224.45..224.49 rows=15 width=24) (actual time=0.713..0.716 rows=20 loops=1)
|
||||
Buffers: shared hit=122
|
||||
-> Sort (cost=224.45..224.49 rows=15 width=24) (actual time=0.712..0.713 rows=20 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id DESC
|
||||
Sort Method: top-N heapsort Memory: 26kB
|
||||
Buffers: shared hit=122
|
||||
-> Bitmap Heap Scan on feed_items fi (actual time=0.256..0.590 rows=1500 loops=1)
|
||||
Recheck Cond: ((visibility='PUBLIC') OR (visibility='MENTIONED') OR ((user_id=:me) AND (visibility='PRIVATE')))
|
||||
Filter: ((visibility='PUBLIC') OR ((visibility='MENTIONED') AND (hashed SubPlan 2)) OR ((visibility='PRIVATE') AND (user_id=:me)))
|
||||
Rows Removed by Filter: 200
|
||||
Buffers: shared hit=122
|
||||
-> BitmapOr (actual time=0.185..0.185 rows=0 loops=1)
|
||||
-> Bitmap Index Scan on ix_feed_items_visibility_sort (Index Cond: visibility='PUBLIC') rows=2400
|
||||
-> Bitmap Index Scan on ix_feed_items_visibility_sort (Index Cond: visibility='MENTIONED') rows=800
|
||||
-> Bitmap Index Scan on ix_feed_items_private (Index Cond: user_id=:me) rows=100
|
||||
SubPlan 2
|
||||
-> Bitmap Heap Scan on feed_item_mentions m (Recheck Cond: mentioned_user_id=:me) rows=200
|
||||
-> Bitmap Index Scan on ix_mentions_user (Index Cond: mentioned_user_id=:me) rows=200
|
||||
Planning Time: 0.144 ms
|
||||
Execution Time: 0.808 ms
|
||||
@@ -0,0 +1,20 @@
|
||||
-- L16 union branches — each visibility branch rides its own optimal plan (what single-OR can't)
|
||||
-- FeedVisibilityIT.l16LowSelectivityBranchesRideTheirIndex (seed 2000, viewer user008)
|
||||
-- 읽기 포인트: 저선택도 분기는 자기 인덱스를 탄다 — mentioned=ix_mentions_user 조인, private=ix_feed_items_private
|
||||
-- partial 의 Index Only Scan. public(60% 고선택도)은 Bitmap Heap Scan+top-N Sort 가 최적.
|
||||
-- 단일 OR 은 3분기를 하나의 bitmap 으로 묶어 분기별 최적 플랜을 못 가진다.
|
||||
|
||||
== mentioned branch (JOIN feed_item_mentions on ix_mentions_user) ==
|
||||
Limit -> Sort (top-N) -> Hash Join (fi.id = m.feed_item_id)
|
||||
-> Bitmap Heap Scan on feed_items fi (visibility='MENTIONED')
|
||||
-> Hash -> Bitmap Heap Scan on feed_item_mentions m
|
||||
-> Bitmap Index Scan on ix_mentions_user (Index Cond: mentioned_user_id = :me) rows=200
|
||||
|
||||
== private branch (partial index ix_feed_items_private WHERE visibility='PRIVATE') ==
|
||||
Limit -> Incremental Sort (Presorted Key: first_highlighted_at)
|
||||
-> Index Only Scan using ix_feed_items_private on feed_items fi (Index Cond: user_id = :me) Heap Fetches: 21
|
||||
|
||||
== public branch (60% selectivity -> seq/bitmap + top-N is optimal, not an index range) ==
|
||||
Limit -> Sort (top-N heapsort)
|
||||
-> Bitmap Heap Scan on feed_items fi (Recheck Cond: visibility='PUBLIC')
|
||||
-> Bitmap Index Scan on ix_feed_items_visibility_sort (Index Cond: visibility='PUBLIC')
|
||||
@@ -0,0 +1,27 @@
|
||||
-- L16 (b) UNION decompose — 3 visibility branches, each index-ordered, Merge Append + Hash Join
|
||||
-- FeedVisibilityIT.l16ExplainThreeWayPlanCompare (seed 2000, viewer user008)
|
||||
-- (public branch) UNION ALL (mentioned branch: JOIN feed_item_mentions) UNION ALL (private branch: partial idx)
|
||||
-- ORDER BY first_highlighted_at DESC, id DESC LIMIT 20
|
||||
-- 읽기 포인트: Merge Append 가 미리 정렬된 분기 스트림을 병합(전체 재정렬 없음). 멘션 EXISTS 가 Hash Join(집합 기반)
|
||||
-- 으로, private 는 Index Only Scan(partial)+Incremental Sort 로. 구조는 우수하나 buffers 200(분기별 스캔).
|
||||
|
||||
Limit (cost=97.16..97.40 rows=13 width=24) (actual time=0.655..0.661 rows=20 loops=1)
|
||||
Buffers: shared hit=200
|
||||
-> Merge Append (cost=97.16..97.40 rows=13 width=24) (actual time=0.654..0.659 rows=20 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id DESC
|
||||
Buffers: shared hit=200
|
||||
-> Limit (rows=17) -- public branch
|
||||
-> Sort (top-N heapsort)
|
||||
-> Bitmap Heap Scan on feed_items fi (Recheck Cond: visibility='PUBLIC')
|
||||
-> Bitmap Index Scan on ix_feed_items_visibility_sort
|
||||
-> Limit (rows=3) -- mentioned branch: EXISTS -> Hash Join
|
||||
-> Sort (top-N heapsort)
|
||||
-> Hash Join (Hash Cond: fi_1.id = m.feed_item_id)
|
||||
-> Bitmap Heap Scan on feed_items fi_1 (visibility='MENTIONED')
|
||||
-> Hash -> Bitmap Heap Scan on feed_item_mentions m
|
||||
-> Bitmap Index Scan on ix_mentions_user (mentioned_user_id=:me)
|
||||
-> Limit (rows=2) -- private branch: partial index, index-only
|
||||
-> Incremental Sort (Presorted Key: fi_2.first_highlighted_at)
|
||||
-> Index Only Scan using ix_feed_items_private on feed_items fi_2 (user_id=:me) Heap Fetches: 21
|
||||
Planning Time: 0.398 ms
|
||||
Execution Time: 0.780 ms
|
||||
@@ -0,0 +1,29 @@
|
||||
컬렉션 하나만 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와 같은 통계 이슈).
|
||||
@@ -0,0 +1,35 @@
|
||||
컬렉션 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 노드 존재).
|
||||
@@ -0,0 +1,25 @@
|
||||
엔티티만 페이징한 SQL의 실행계획 (b) — Limit 노드 존재 (대조군)
|
||||
출처: FeedPersistenceIT.l4ExplainCollectionJoinHasNoLimitButEntityPagingDoes 콘솔 출력 (b)
|
||||
조건: seed(100) 직후. warm buffer cache(shared read=0).
|
||||
쿼리: EXPLAIN (ANALYZE, BUFFERS)
|
||||
SELECT fi.* FROM feed_items fi ORDER BY fi.first_highlighted_at DESC, fi.id ASC LIMIT 20
|
||||
(fetch join 없이 엔티티만 페이징한 SQL — DB가 정상적으로 페이징하는 모습)
|
||||
|
||||
Limit (cost=13.42..13.47 rows=20 width=1194) (actual time=0.038..0.040 rows=20 loops=1)
|
||||
Buffers: shared hit=11
|
||||
-> Sort (cost=13.42..13.58 rows=66 width=1194) (actual time=0.038..0.038 rows=20 loops=1)
|
||||
Sort Key: first_highlighted_at DESC, id
|
||||
Sort Method: top-N heapsort Memory: 28kB
|
||||
Buffers: shared hit=11
|
||||
-> Seq Scan on feed_items fi (cost=0.00..11.66 rows=66 width=1194) (actual time=0.015..0.019 rows=100 loops=1)
|
||||
Buffers: shared hit=11
|
||||
Planning Time: 0.029 ms
|
||||
Execution Time: 0.050 ms
|
||||
|
||||
관찰(문서 §10):
|
||||
- 계획 최상단에 Limit 노드가 있고 그 아래 Sort가 top-N heapsort(28kB)로 상위 20행만 취한다 = DB가 페이징을 했다.
|
||||
(a) l4-collection-join-no-limit.txt는 Limit 노드가 없어 전체 1961행을 quicksort(445kB)로 정렬했다 — 대조가 요점.
|
||||
- 28kB(top-N heapsort, 20행) vs 445kB(quicksort, 1961행): "DB 페이징 vs 인메모리 페이징"의 메모리 비용 차이가
|
||||
계획 레벨로 드러난다. (a)에 Limit이 없다는 것 자체가 "DB가 페이징을 안 했다 → Hibernate가 메모리에서 했다"의 증거.
|
||||
- 컬럼명·리터럴 하드코딩이라 인젝션 무관. PG 계획 문구는 버전·통계에 따라 흔들릴 수 있어 강가드 대신 눈 대조로 둔다.
|
||||
- Buffers: shared read=0 → warm buffer cache. Execution Time 0.050 ms는 executor 내부 시간(§6.4 caveat와 동일).
|
||||
@@ -0,0 +1,38 @@
|
||||
배치 IN 조회의 실행계획 (b) — 행을 곱하지 않는다 (카테시안 소멸)
|
||||
출처: FeedBatchFetchIT.l5ExplainEntityPagingHasLimitAndBatchInHasNoRowMultiplication 콘솔 출력 (b)
|
||||
조건: seed(100) 직후, default_batch_fetch_size=100 세션. warm buffer cache.
|
||||
쿼리: EXPLAIN (ANALYZE, BUFFERS)
|
||||
SELECT h.* FROM highlights h
|
||||
WHERE h.feed_item_id IN (SELECT fi.id FROM feed_items fi
|
||||
ORDER BY fi.first_highlighted_at DESC, fi.id ASC LIMIT 20)
|
||||
(배치 페치가 페이지 부모 20개의 highlights 를 IN 한 방으로 채우는 것과 같은 shape)
|
||||
|
||||
Hash Semi Join (cost=72.46..317.64 rows=234 width=710) (actual time=0.295..0.541 rows=1509 loops=1)
|
||||
Hash Cond: (h.feed_item_id = "ANY_subquery".id)
|
||||
Buffers: shared hit=272
|
||||
-> Seq Scan on highlights h (cost=0.00..236.43 rows=2343 width=710) (actual time=0.207..0.286 rows=1961 loops=1)
|
||||
Buffers: shared hit=213
|
||||
-> Hash (cost=72.21..72.21 rows=20 width=16) (actual time=0.084..0.085 rows=20 loops=1)
|
||||
Buckets: 1024 Batches: 1 Memory Usage: 9kB
|
||||
Buffers: shared hit=59
|
||||
-> Subquery Scan on "ANY_subquery" (cost=71.96..72.21 rows=20 width=16) (actual time=0.077..0.080 rows=20 loops=1)
|
||||
Buffers: shared hit=59
|
||||
-> Limit (cost=71.96..72.01 rows=20 width=24) (actual time=0.077..0.078 rows=20 loops=1)
|
||||
Buffers: shared hit=59
|
||||
-> Sort (cost=71.96..72.84 rows=354 width=24) (actual time=0.076..0.077 rows=20 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id
|
||||
Sort Method: top-N heapsort Memory: 26kB
|
||||
Buffers: shared hit=59
|
||||
-> Seq Scan on feed_items fi (cost=0.00..62.54 rows=354 width=24) (actual time=0.053..0.059 rows=100 loops=1)
|
||||
Buffers: shared hit=59
|
||||
Planning:
|
||||
Buffers: shared hit=28
|
||||
Planning Time: 0.206 ms
|
||||
Execution Time: 0.592 ms
|
||||
|
||||
관찰(문서 §11):
|
||||
- Semi Join 이 반환하는 행 = 1509(페이지 20개 부모의 highlights). 부모 M행 × 자식 = M×K 로 곱하지 않는다.
|
||||
L3 카테시안(조인이 feed_items ⋈ highlights 를 1961행으로 곱함)과 정반대 — 자식 K행만 반환(합, 곱 아님).
|
||||
- 배치 페치가 하는 일이 이 shape다: 페이지 부모 키를 모아 WHERE feed_item_id IN (…) 로 한 방에 채운다.
|
||||
Hibernate 는 이를 default_batch_fetch_size 만큼 쪼개 ceil(pageItems/batch) 번 발행한다.
|
||||
- Buffers: read≈0 → warm buffer cache. Execution Time 0.592 ms 는 executor 내부 시간(§6.4 caveat와 동일).
|
||||
@@ -0,0 +1,26 @@
|
||||
엔티티만 페이징한 SQL의 실행계획 (a) — Limit 노드 존재 (배치 페치 해법의 페이징)
|
||||
출처: FeedBatchFetchIT.l5ExplainEntityPagingHasLimitAndBatchInHasNoRowMultiplication 콘솔 출력 (a)
|
||||
조건: seed(100) 직후, default_batch_fetch_size=100 세션. warm buffer cache.
|
||||
쿼리: EXPLAIN (ANALYZE, BUFFERS)
|
||||
SELECT fi.* FROM feed_items fi ORDER BY fi.first_highlighted_at DESC, fi.id ASC LIMIT 20
|
||||
(배치 해법은 fetch join을 버리고 엔티티만 페이징한다 → DB가 정상 페이징)
|
||||
|
||||
Limit (cost=71.96..72.01 rows=20 width=1194) (actual time=0.108..0.109 rows=20 loops=1)
|
||||
Buffers: shared hit=65
|
||||
-> Sort (cost=71.96..72.84 rows=354 width=1194) (actual time=0.107..0.108 rows=20 loops=1)
|
||||
Sort Key: first_highlighted_at DESC, id
|
||||
Sort Method: top-N heapsort Memory: 28kB
|
||||
Buffers: shared hit=65
|
||||
-> Seq Scan on feed_items fi (cost=0.00..62.54 rows=354 width=1194) (actual time=0.075..0.080 rows=100 loops=1)
|
||||
Buffers: shared hit=59
|
||||
Planning:
|
||||
Buffers: shared hit=14 read=1
|
||||
Planning Time: 0.085 ms
|
||||
Execution Time: 0.118 ms
|
||||
|
||||
관찰(문서 §11):
|
||||
- 계획 최상단에 Limit 노드가 있다 = DB가 페이징을 했다. top-N heapsort 28kB로 상위 20행만 취한다.
|
||||
- L4 (a)(컬렉션 fetch join)는 Limit 노드가 없어 전체 1961행을 quicksort(445kB)로 정렬했다 — 정반대.
|
||||
fetch join을 버리니(엔티티만 페이징) 페이징이 DB로 내려간다(HHH000104 인메모리 페이징 소멸).
|
||||
- 대조군: l5-batch-in-semijoin.txt (페이지 부모들의 highlights 를 IN 한 방으로 — 행을 곱하지 않는다).
|
||||
- Buffers: read≈0 → warm buffer cache. Execution Time 0.118 ms 는 executor 내부 시간(§6.4 caveat와 동일).
|
||||
@@ -0,0 +1,27 @@
|
||||
-- L6 (b) child scalar IN projection — SELECT new carrier(h.feedItem.id, h.color, h.text, h.createdAt)
|
||||
-- FeedProjectionIT.l6ExplainProjectionHasLimitAndSemiJoinNotNarrowerWidth (seed 100, page size 20)
|
||||
-- SELECT h.feed_item_id, h.color, h.text, h.created_at FROM highlights h
|
||||
-- WHERE h.feed_item_id IN (SELECT fi.id FROM feed_items fi ORDER BY fi.first_highlighted_at DESC, fi.id ASC LIMIT 20)
|
||||
-- 읽기 포인트: Hash Semi Join 이라 자식 행(1509)만 반환 — 부모 M × 자식 K 로 곱하지 않는다(카테시안 없음).
|
||||
-- L5 배치 IN (b)와 같은 shape/행수(1509). 프로젝션은 필요 컬럼만(color/text/created_at) 읽는다.
|
||||
|
||||
Hash Semi Join (cost=50.51..235.83 rows=177 width=686) (actual time=0.218..0.452 rows=1509 loops=1)
|
||||
Hash Cond: (h.feed_item_id = "ANY_subquery".id)
|
||||
Buffers: shared hit=202
|
||||
-> Seq Scan on highlights h (cost=0.00..178.71 rows=1771 width=686) (actual time=0.148..0.226 rows=1961 loops=1)
|
||||
Buffers: shared hit=161
|
||||
-> Hash (cost=50.26..50.26 rows=20 width=16) (actual time=0.067..0.067 rows=20 loops=1)
|
||||
Buckets: 1024 Batches: 1 Memory Usage: 9kB
|
||||
Buffers: shared hit=41
|
||||
-> Subquery Scan on "ANY_subquery" (cost=50.01..50.26 rows=20 width=16) (actual time=0.060..0.062 rows=20 loops=1)
|
||||
Buffers: shared hit=41
|
||||
-> Limit (cost=50.01..50.06 rows=20 width=24) (actual time=0.059..0.060 rows=20 loops=1)
|
||||
Buffers: shared hit=41
|
||||
-> Sort (cost=50.01..50.62 rows=246 width=24) (actual time=0.059..0.059 rows=20 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id
|
||||
Sort Method: top-N heapsort Memory: 26kB
|
||||
Buffers: shared hit=41
|
||||
-> Seq Scan on feed_items fi (cost=0.00..43.46 rows=246 width=24) (actual time=0.035..0.041 rows=100 loops=1)
|
||||
Buffers: shared hit=41
|
||||
Planning Time: 0.064 ms
|
||||
Execution Time: 0.497 ms
|
||||
@@ -0,0 +1,34 @@
|
||||
-- L6 (a) parent scalar projection — SELECT new carrier(f.id, u.name, u.username, p.url, p.title, f.firstHighlightedAt)
|
||||
-- FeedProjectionIT.l6ExplainProjectionHasLimitAndSemiJoinNotNarrowerWidth (seed 100, page size 20)
|
||||
-- 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 로 엔티티 SELECT fi.*(L5 (a) width 1194)보다 넓다
|
||||
-- — users/pages 조인 + PG varchar 추정치 탓. 프로젝션 이득은 EXPLAIN 아니라 ORM 층(entityLoadCount 0).
|
||||
|
||||
Limit (cost=102.56..102.61 rows=20 width=2088) (actual time=0.187..0.190 rows=20 loops=1)
|
||||
Buffers: shared hit=81
|
||||
-> Sort (cost=102.56..103.17 rows=246 width=2088) (actual time=0.187..0.188 rows=20 loops=1)
|
||||
Sort Key: fi.first_highlighted_at DESC, fi.id
|
||||
Sort Method: top-N heapsort Memory: 27kB
|
||||
Buffers: shared hit=81
|
||||
-> Hash Join (cost=51.24..96.01 rows=246 width=2088) (actual time=0.137..0.161 rows=100 loops=1)
|
||||
Hash Cond: (fi.page_id = p.id)
|
||||
Buffers: shared hit=81
|
||||
-> Hash Join (cost=10.68..54.79 rows=246 width=1072) (actual time=0.072..0.087 rows=100 loops=1)
|
||||
Hash Cond: (fi.user_id = u.id)
|
||||
Buffers: shared hit=43
|
||||
-> Seq Scan on feed_items fi (cost=0.00..43.46 rows=246 width=56) (actual time=0.053..0.057 rows=100 loops=1)
|
||||
Buffers: shared hit=41
|
||||
-> Hash (cost=10.30..10.30 rows=30 width=1048) (actual time=0.010..0.010 rows=20 loops=1)
|
||||
Buckets: 1024 Batches: 1 Memory Usage: 10kB
|
||||
Buffers: shared hit=2
|
||||
-> Seq Scan on users u (cost=0.00..10.30 rows=30 width=1048) (actual time=0.003..0.004 rows=20 loops=1)
|
||||
Buffers: shared hit=2
|
||||
-> Hash (cost=39.14..39.14 rows=114 width=1048) (actual time=0.059..0.059 rows=100 loops=1)
|
||||
Buckets: 1024 Batches: 1 Memory Usage: 17kB
|
||||
Buffers: shared hit=38
|
||||
-> Seq Scan on pages p (cost=0.00..39.14 rows=114 width=1048) (actual time=0.040..0.045 rows=100 loops=1)
|
||||
Buffers: shared hit=38
|
||||
Planning Time: 0.134 ms
|
||||
Execution Time: 0.228 ms
|
||||
@@ -0,0 +1,20 @@
|
||||
반복되는 Page ToOne 부모 쿼리의 실행계획 (N2 — 선형 범인)
|
||||
출처: FeedPersistenceIT.l2ExplainRepeatedPageToOneQuery 콘솔 출력
|
||||
조건: seed(100) 직후. warm buffer cache(shared read=0). id는 시드된 실제 pages.id 1건.
|
||||
쿼리: SELECT * FROM pages WHERE id = ? (@ManyToOne EAGER가 행마다 반복하는 2차 SELECT)
|
||||
|
||||
Index Scan using pk_pages on pages
|
||||
(cost=0.14..8.15 rows=1 width=2104) (actual time=0.009..0.009 rows=1 loops=1)
|
||||
Index Cond: (id = 'a069f5ac-fa46-41f8-bcde-174153789467'::uuid)
|
||||
Buffers: shared hit=2
|
||||
Planning Time: 0.031 ms
|
||||
Execution Time: 0.021 ms
|
||||
|
||||
주의(문서 §7.3 / §6.4 caveat와 동일):
|
||||
- PK 조회라 pk_pages Index Scan으로 1건 0.021 ms. "쿼리가 느려서"가 아니다 — page는 아이템당
|
||||
고유(dedup 없음)라 이 빠른 계획이 정확히 N번 반복되는 게 문제다(왕복 N회).
|
||||
- users 계획(toone-users-plan.txt)과 실행계획이 사실상 동일하다. 비용을 가르는 것은 계획이 아니라
|
||||
반복 횟수(page=N vs user=distinct≤20)다 — 카디널리티가 곡선을 가른다.
|
||||
- Buffers: shared hit=2, read=0 → warm buffer cache. cold 디스크 I/O 실행시간으로 읽지 말 것.
|
||||
- Execution Time 0.021 ms는 executor 내부 시간. 애플리케이션 지연(§6.2)과 같은 지표가 아니다.
|
||||
- 인덱스로 안 풀린다(계획이 이미 PK Index Scan). 왕복 횟수 자체를 줄이는 fetch 전략이 필요(§9).
|
||||
@@ -0,0 +1,19 @@
|
||||
반복되는 User ToOne 부모 쿼리의 실행계획 (N2 — 평탄, 1차 캐시 dedup)
|
||||
출처: FeedPersistenceIT.l2ExplainRepeatedPageToOneQuery 콘솔 출력
|
||||
조건: seed(100) 직후. warm buffer cache(shared read=0). id는 시드된 실제 users.id 1건.
|
||||
쿼리: SELECT * FROM users WHERE id = ? (@ManyToOne EAGER가 반복하는 2차 SELECT)
|
||||
|
||||
Index Scan using pk_users on users
|
||||
(cost=0.14..8.15 rows=1 width=2104) (actual time=0.013..0.014 rows=1 loops=1)
|
||||
Index Cond: (id = '0a2a85ed-f8f3-47f0-b957-c477f4b077ab'::uuid)
|
||||
Buffers: shared hit=2
|
||||
Planning Time: 0.027 ms
|
||||
Execution Time: 0.022 ms
|
||||
|
||||
주의(문서 §7.3):
|
||||
- 단건 실행계획은 pages(toone-pages-plan.txt)와 사실상 동일하다: 둘 다 pk Index Scan, ~0.02 ms.
|
||||
- 그러나 반복 횟수가 다르다. user는 소수 풀(≤20)을 재사용하고 한 번 로드된 대상은 영속성
|
||||
컨텍스트(1차 캐시)에 남아 재조회되지 않으므로, 서로 다른 대상(distinct target) 수만큼만
|
||||
나간다 → N과 무관하게 ≤20에서 평탄. page는 아이템당 고유라 N번.
|
||||
- 결론: 같은 @ManyToOne(EAGER)·같은 실행계획인데 곡선이 갈리는 원인은 계획이 아니라
|
||||
데이터 분포(카디널리티)다. EXPLAIN만 보면 둘이 똑같아 보이는 것이 '숨은' N+1의 얼굴이다.
|
||||
@@ -0,0 +1,7 @@
|
||||
metric,precompute,single_or,note
|
||||
page1_unified_parents,20,20,keyset page — both parent paths return the same 20
|
||||
page1_unified_rows,60,60,LATERAL top-3 per parent (<=60)
|
||||
page1_buffers_shared_hit,63,181,env-dependent (relative only — warm cache)
|
||||
deep_keyset_parent_scanned,19,200,precompute index-range(19) vs BitmapOr + hashed mention SubPlan(200)
|
||||
deep_keyset_buffers_shared_hit,60,88,env-dependent (relative only — warm cache)
|
||||
viewer_visible_set,1500,1500,feed_visible count for user008 (= single-OR page-1 candidate set)
|
||||
|
@@ -0,0 +1,4 @@
|
||||
N,collection_init,prepared_total,toone
|
||||
10,10,25,13
|
||||
100,100,222,120
|
||||
1000,1000,2022,1020
|
||||
|
@@ -0,0 +1,8 @@
|
||||
rank,highlights
|
||||
1,500
|
||||
2,225
|
||||
3,141
|
||||
5,79
|
||||
10,35
|
||||
50,6
|
||||
100,3
|
||||
|
@@ -0,0 +1,4 @@
|
||||
K,window_rows,window_buffers,window_ms,lateral_rows,lateral_buffers,lateral_ms
|
||||
3,60,162,1.388,60,114,0.271
|
||||
50,695,216,1.540,695,155,0.908
|
||||
500,1509,269,2.905,1509,171,1.259
|
||||
|
@@ -0,0 +1,3 @@
|
||||
variant,top_node,child_access,buffers_shared_hit,exec_ms
|
||||
with_index,Nested Loop,Index Scan using ix_highlights_feed_items_created (Limit 3),168,0.336
|
||||
without_index,Nested Loop,Seq Scan on highlights (Rows Removed by Filter 2842/loop),4446,5.472
|
||||
|
@@ -0,0 +1,4 @@
|
||||
strategy,top_node,returned_rows,buffers_shared_hit,exec_ms
|
||||
window,WindowAgg (Subquery Scan on t),60,430,1.552
|
||||
lateral,Nested Loop (Index Scan + Limit 3),60,204,0.323
|
||||
twostep,Sort (Hash Semi Join),1509,430,1.686
|
||||
|
@@ -0,0 +1,5 @@
|
||||
strategy,returned_rows,parents_covered,max_per_parent
|
||||
window,60,20,3
|
||||
lateral,60,20,3
|
||||
twostep_full,1509,20,unbounded
|
||||
naive_wrong_limit3,3,1,3
|
||||
|
@@ -0,0 +1,4 @@
|
||||
variant,top_node,returned_rows,scanned_rows,buffers_shared_hit,exec_ms
|
||||
offset,Limit<-Sort<-Seq Scan,20,2000,141,0.996
|
||||
keyset_with_index,Limit<-Index Only Scan,20,20,1,0.076
|
||||
keyset_without_index,Limit<-Sort<-Seq Scan (filter),20,20,141,0.373
|
||||
|
@@ -0,0 +1,4 @@
|
||||
offset,page,offset_scanned,offset_buffers,keyset_scanned,keyset_buffers
|
||||
0,1,20,1,20,3
|
||||
980,50,1000,18,20,3
|
||||
1980,100,2000,106,20,2
|
||||
|
@@ -0,0 +1,4 @@
|
||||
approach,top_node,sort,mentions_handling,candidate_rows,buffers_shared_hit,exec_ms
|
||||
single_or,Bitmap Heap Scan + top-N Sort,re-sort,hashed SubPlan,1500,122,0.808
|
||||
union_decompose,Merge Append (per-branch index),per-branch merge,Hash Join,,200,0.780
|
||||
precompute,Index Only Scan on feed_visible,none,pre-materialized,20,1,0.034
|
||||
|
@@ -0,0 +1,4 @@
|
||||
N,page_fetch,user_fetch,entity_fetch,collection_init,prepared_total
|
||||
10,10,3,13,10,25
|
||||
100,100,20,120,100,222
|
||||
1000,1000,20,1020,1000,2022
|
||||
|
@@ -0,0 +1,4 @@
|
||||
N,transferred_rows_join_card,list_size_hibernate6_dedup,distinct_items,seeded_highlights,blowup_x,prepared_total
|
||||
10,1285,10,10,1285,128.5,14
|
||||
100,1961,100,100,1961,19.6,121
|
||||
1000,2917,1000,1000,2917,2.9,1021
|
||||
|
@@ -0,0 +1,4 @@
|
||||
N,p50_ms,p99_ms,thread_alloc_kb
|
||||
10,6.184,6.566,1582
|
||||
100,13.890,16.062,3061
|
||||
1000,79.452,83.526,10230
|
||||
|
@@ -0,0 +1,4 @@
|
||||
N,returned_page,feed_item_loaded,over_fetch_x,seeded_highlights
|
||||
10,10,10,1.0,1285
|
||||
100,20,100,5.0,1961
|
||||
1000,20,1000,50.0,2917
|
||||
|
@@ -0,0 +1,4 @@
|
||||
N,l1_prepared_before,l5_prepared_after,l1_collfetch_before,l5_collfetch_after,feed_item_loaded_page,collapse_x
|
||||
10,25,5,10,1,10,5.0
|
||||
100,222,5,100,1,20,44.4
|
||||
1000,2022,23,1000,10,20,87.9
|
||||
|
@@ -0,0 +1,2 @@
|
||||
scope,page_size,entities_loaded
|
||||
page20_seed1000,20,1569
|
||||
|
@@ -0,0 +1,3 @@
|
||||
plan,explain_width_estimate
|
||||
l6_parent_projection,2088
|
||||
l5_entity_paging,1194
|
||||
|
@@ -0,0 +1,5 @@
|
||||
metric,before_l5_batch,after_l6_projection
|
||||
entities_loaded_page20_seed1000,1569,0
|
||||
prepared_n1000,23,2
|
||||
collection_fetch_n1000,10,0
|
||||
child_rows_page20_seed1000,1509,1509
|
||||
|
Reference in New Issue
Block a user