Postgres does not charge for the vectors it reads
Problem: P25 found frames/guided on seq scan. Guess: planner mis-estimates policy filter selectivity. pg_stats says otherwise — row estimate 24,982 vs actual 24,738. Estimate right, plan still wrong.
Root cause: Planner gets price wrong. vector(1152) = 4,616 bytes → every vector in TOAST. frame_vectors heap = 20MB, vectors = 1,322MB. Seq scan costed on heap. Detoasting for distance = costed at nothing. Planner prices guided scan at 11,174 (reads 211,655 buffers) vs index walk at 19,891 (reads 23,468) → takes scan.
Why uniform works: Both prices move with rows passing filter. Detoast uncharged same, but uncharged cost can't move estimate. Scan charged part grows with passing rows (sort + distance eval). Walk charged part falls (fewer index rows/output row). Verified with EXPLAIN disabling one plan at a time. Uniform scan priced at 19,194 vs walk at 11,290. Guided scan at 11,174 vs walk at 19,891.
P25 stopped at guided. scenecut + deictic same plan. 3 of 5 app calls = full scan.
Fix: 4th SET clause on frame_search: enable_seqscan = off (scoped to function).
Warm (10 queries, live DB):
| policy | frames | before | after |
|---|
| uniform | 239,124 | 40–47ms | 47–52ms |
| guided | 24,738 | 497–874ms | 61–67ms |
| scenecut | 22,257 | 273–286ms | 67–73ms |
| deictic | 3,881 | 91–212ms | 143–173ms |
| none | 253,831 | 45–50ms | 44–48ms |
deictic left off index (correct). HNSW walk priced at 72,350; frames_pkey at 20,910. 1.5% rows pass → walk visits most graph for 200 rows. Timing in own spread (143–173ms vs 91–212ms).
Cold guided (verified suspend+resume):
| reading | before | after |
|---|
| cold | 8,974ms | 4,209ms |
| warm 1 | 8,587ms | 59ms |
| warm 2 | 2,209ms | 66ms |
| warm 3–15 | 506–663ms | 60–72ms |
Both runs cleared postmaster gate → cold by evidence. 3-reading tail scan needed to settle = gone. Query after cold = already at warm floor.
Per-session cost (16 queries, 1 session): - Scan: session pays 211,655 buffers once. Walk: ~23,000 blocks scattered in 1,978MB index. Cache < index. Blocks needed depend on vector. Every 1st query pays for own region as random reads from Neon pageserver (~0.2ms/block). - 6 different queries on just-resumed compute:
| query | 1st touch | repeat |
|---|
| q010 | 4,320ms | 65ms |
| q020 | 6,137ms | 79ms |
| q030 | 5,091ms | 94ms |
| q040 | 4,584ms | 270ms |
| q050 | 5,550ms | 72ms |
| q060 | 4,341ms | 97ms |
No decay across 6. Live route same: 6,229, 4,340, 3,339ms. Before: 8,974 → 8,587 → 2,209 → 506–663ms.
Per guided search on resumed compute:
| query in session | before | after |
|---|
| 1 | 8,974ms | 4,209ms |
| 2 | 8,587ms | ~5,000ms |
| 3 | 2,209ms | ~5,000ms |
| 4+ each | 506–663ms | ~5,000ms |
| already asked | 506–663ms | 65–270ms |
1 search after resume = 2x faster. 6 different searches = ~30s DB time vs ~21s before. Crossover at 3–4 queries. Autosuspend = 300s → resumed compute = ordinary state. P30 traded shared-session cost for per-query cost. Nothing in function can lower per-query cost (walk reads minimum).
Hosting decision (Ed's): - Cron keeping compute awake → all readers on 65–270ms row, but bills every idle hour. - Decision 001 (text tower) answered no. - Min CU >0.25 holds more graph between queries, bills floor whenever awake across suspend. - pg_prewarm moves read to resume. Index 1,978MB > cache → helps only first few searches. - Decision doc has costs. Missing measurement: searches per real session (doesn't exist).
Eval revelation: Seq scan = exact NN. Index walk = approximate. If guided=scan + uniform=index, eval compared 2 retrieval methods, called difference a sampling policy difference. Timings rule out. Per-cell knn in results.json: both rows served by scan. Guided: 311–564ms (all 99 cells). Uniform: 1,209–2,413ms (98/100). Index plan = 45ms. Both exact.
Offline verification: 100 ground-truth queries against both plans → every guided cell identical (splits + by-style). Says metrics only read ground-truth film rank + returned t. Retrievals differ elsewhere: mean frame-level recall 0.944, 9/100 different top-10 order. None moved ground-truth film across rank boundary. results.md now notes which retrieval produced it (inferred from timing).
Re-run needed: Published numbers from retrieval site no longer performs. Uniform row also under P29's plan. Command: make eval (~13 min wall). Zone rate limit: 300 req paced 2.6s. Tower: 200 embeds + 1 cold start (~50s if slept). DB: ~20s query time at new warm costs, stays awake 13 min. Overwrites evals/results.json/.md.
Traces (build cce1eb6, 6 first-time queries/policy, idle DB):
| policy | p50 | p95 |
|---|
| guided | 4,840ms | 6,804ms |
| uniform | 1,641ms | 1,870ms |
| deictic | 1,510ms | 1,888ms |
| scenecut | 598ms | 1,392ms |
All 4: p50 1,641ms, p95 6,256ms. Prev build: p50 1,506ms, p95 8,209ms (no policy in spans, mix unknown). All readings = first touch (matches first-time query table). Uniform pays own smaller per-query cost on live path.
First-time guided search: 4–6s on live site after DB idle. Once run: 65–270ms. Before: 9s for 1st search of session, 0.5s after. Recall table stands; now says which retrieval wrote it.