From 0ca3af4d1a8e55045dff506997370bbd0eaeecd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claude=20=28=E7=B8=BD=E7=AE=A1=E9=9B=B2=E7=AB=AF=29?= Date: Sat, 18 Jul 2026 08:30:38 +0000 Subject: [PATCH] =?UTF-8?q?kbdb:=20searchByTemplate=20=E4=BF=AE=20N+1?= =?UTF-8?q?=EF=BC=88100=20=E7=AD=86=20record=20=E9=80=90=E7=AD=86=20getRec?= =?UTF-8?q?ord=20=E2=89=88=2019=20=E7=A7=92=20=E2=86=92=20=E6=89=B9?= =?UTF-8?q?=E6=AC=A1=20IN=20=E6=9F=A5=E8=A9=A2=201.3=20=E7=A7=92=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit leo 客戶測試「圖譜/AI 查詢 20-30 秒」的病根:by-template 列表對每筆 record 各跑一次 getRecord(每筆一次 D1 往返)。改為 record id 以 90 一組(D1 綁定 參數上限 100)批次 IN 撈 slot 值,輸出排序與形狀不變。 實測 uncle6:/records/by-template/triplet 19s→1.3s、portal 圖譜 20s→3s、 總圖 1.8s。kbdb 測試 20/20 綠、tsc 0。 --- kbdb/src/actions/record-crud.ts | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/kbdb/src/actions/record-crud.ts b/kbdb/src/actions/record-crud.ts index 63997d0..68f9cd3 100644 --- a/kbdb/src/actions/record-crud.ts +++ b/kbdb/src/actions/record-crud.ts @@ -181,10 +181,31 @@ export async function searchByTemplate(db: D1Database, template: string, owner_i .prepare(`SELECT DISTINCT record_id FROM entry_values WHERE template_id = ? ORDER BY created_at DESC LIMIT ?`) .bind(tpl.id, cap) .all<{ record_id: string }>(); - const out: RecordResult[] = []; - for (const { record_id } of res.results ?? []) { - const rec = await getRecord(db, record_id); - if (rec) out.push(rec); + // 批次撈齊所有 record 的 slot 值(2026-07-18 修 N+1:原本逐筆 getRecord=每筆 1 次 D1 + // 往返,100 筆 triplet ≈ 19 秒——graph/總圖/rag_chat 的「查詢 20-30 秒」病根就是這裡)。 + // D1 綁定參數上限 100 → id 以 90 一組分批 IN 查詢;輸出保持原排序(created_at DESC)。 + const ids = (res.results ?? []).map((r) => r.record_id); + if (ids.length === 0) return []; + const byId = new Map(); + for (let i = 0; i < ids.length; i += 90) { + const chunk = ids.slice(i, i + 90); + const placeholders = chunk.map(() => '?').join(','); + const evRes = await db + .prepare( + `SELECT ev.record_id as record_id, ev.slot_name as slot, e.content as content, ev.template_id as template_id + FROM entry_values ev JOIN entries e ON ev.entry_id = e.id + WHERE ev.record_id IN (${placeholders})`, + ) + .bind(...chunk) + .all<{ record_id: string; slot: string; content: string; template_id: string }>(); + for (const r of evRes.results ?? []) { + let rec = byId.get(r.record_id); + if (!rec) { + rec = { record_id: r.record_id, template_id: r.template_id, values: {} }; + byId.set(r.record_id, rec); + } + rec.values[r.slot] = r.content; + } } - return out; + return ids.map((id) => byId.get(id)).filter((r): r is RecordResult => !!r); }