- 30 Aug, 2026 17 commits
-
-
只读:每条子场景 SQL 外套 count(*),不写库。判据用**墙钟**,不看各查询耗时之和 (并发下单条会被争抢拉长,和不可比 —— 这正是 2026-08-29 那次误判的成因之一)。 背景:代码注释里「
⛔ 别指望靠并发提速」的依据是「并发3=24.1分 vs 串行23.3分」, 3% 的差距落在 ±25% 的环境噪音里,什么也没证明,却成了生产不开并发的理由。 今晚测试机 C 轮(legacy,并发4)各子场景耗时之和 1,736s 而场景段墙钟仅 794s —— 墙钟远小于求和 = 并发确实在重叠,与「瓶颈是共享I/O、并行无用」的旧归因矛盾。 本地(30K,8000 患者子集,两对交替): conc=4 4.8s / 5.2s conc=1 10.5s / 10.0s → 稳定 ×2.0,可复现 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
luoqi committed
-
plan.controller recomputeForPatient 是 HTTP 端点,走的就是这套召回 SQL 的 单患者路径(scope.patientId)。批量慢是运维问题,这条慢是用户当场感受得到的问题, 之前只测了批量和画像,漏了它。 按「一位患者跑完 11 个子场景」= 一次刷新的真实代价来计时,同时逐 signal×tooth 比结果。 本地(30K):零差异;单查询 p95 26→17ms;一次刷新 103→71ms。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
luoqi committed
-
⛔ 我上一版据 B 轮单点写下「集合式内存打穿、溢盘、拖垮全口查询」——全错。 D 轮复现不出来(545s,四轮最快);实测每轮临时文件 C=4 个 / D=4 个、增量 62MB, 根本没溢盘;187GB 是统计累计值不是今晚产生的。B 轮整段压在 02:00 的 stale-scan cron 上,是外部负载。那套「内存打穿」的因果链是照着一个数字编的。 真实结论(C vs D,相邻两轮、两个同SQL全口对照组给出 ±25% 噪音基准): 场景段 794s → 545s(−31%) 整轮 1,181s → 923s(−22%) 超噪音的收益集中在 impacted ×2.25 / caries ×2.09 / hard ×1.59 / endo ×1.43 正确性三道门全过;端到端差异全是新增(时间漂移),零删除。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
luoqi committed
-
luoqi committed
-
luoqi committed
-
luoqi committed
-
luoqi committed
-
luoqi committed
-
luoqi committed
-
画像是**逐患者**调用(全量 54.7 万次),SQL 形态与召回不同(scope 恒 1 行), 单次开销会被放大 54.7 万倍 —— 必须单独验,而且要比耗时分布不只比结果。 只读不写库:直接调 selectForPatient 两次比 gap 列表。 selectForPatient 加可选 variant 入参(只给对拍用;生产路径不传,走环境开关)。 本地实测(2000 位有 active 信号的患者): 零差异(其中 1441 位有 gap) 耗时/患者 legacy p50=7ms p95=18ms 合计=16.1s setbased p50=6ms p95=16ms 合计=13.9s ← 不劣化 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
luoqi committed
-
luoqi committed
-
形态:cand → scope → resolved 预聚合 → 牙位反连接。核心恒等式 ∃x∈G: t(x) ⋛ a ⟺ max{t(x)} ⋛ a,分组 G=(患者,牙位)。 13 个分支对 sig 的相关性只有三类:时间门(10)/ 病历号等值(1)/ 无相关(2), 另有「建议优先」一条多个 sig.type 标量谓词(ndx 标志)。⚠ ️ 集合式是**独立重写一份**,刻意不与 legacy 共用片段 —— 共用则重构写错的地方 两边一起错、对拍互相抵消。等价性靠 verify-gap-equivalence 逐行差分来证。 全口码(K05/K07)不进集合式:它们的 lateral PG 本来就会摘掉(零收益), 实测硬套进 gap_cand 还慢 2~3 倍(1309→4439ms / 1388→3065ms)。 新增: · src/cli/verify-gap-equivalence.cli.ts —— 两版同一 REPEATABLE READ 快照, 双向 EXCEPT ALL 差分到 (患者,信号,牙位);--self 自对拍先证工具可信 · tests/gap-setbased-parity.spec.ts —— 结构对拍,守「分支集合不许走散」 (加分支只改一边 = 静默错召,tsc 和现有 spec 都发现不了) · scenario.buildScenarioSql() 抽成独立方法,让对拍拿到线上跑的那条 SQL 本身 · PAC_GAP_VARIANT=setbased 切换;默认 legacy 本地实测(30K 库):11 个子场景全部零差异,行数逐个相同; 牙位级 ×1.05~1.73(库小全热,不作为收益判据,以测试机 585K 为准)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
① 全口场景(K05/K07)的 lateral PG 已自动删除(useless-left-join removal), 本方案对它们零收益;测试机 K05 9m13s 的钱在患者级 NOT EXISTS,不在 resolvedTeeth。 → 全轮预期改善下修到 35~40%,不足以单独装进 2 小时窗口。 ② 本地 30K 库单分支对拍只快 1.26×,库太小全热不能外推 → 先建对拍与基准, 单子场景试点达标(≥3×)再铺开;不达标就停,只留对拍工具。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed
-
- 29 Aug, 2026 19 commits
-
-
luoqi committed
-
luoqi committed
-
2026-08-29 生产六轮实测(endo_no_rct,72,769 行,全部在生产 RDS 上跑): 配置 耗时 相对基线 无索引 + 4MB (冷) 18:43 无索引 + 4MB (暖) 17:33 ← 基线;冷暖只差 6%,缓存不是主因 无索引 + 256MB (暖) 11:37 −34% 无索引 + 256MB (暖,复现) 12:12 −30% 有索引 + 4MB (暖) 16:42 −5% ← 噪声内,索引已从生产撤除 有索引 + 128MB (暖) 14:47 −16% ← 128MB 只吃到一半收益
⭐ 取 256MB:收益对取值很敏感,128→256 还差一倍。原本按「溢出只有 4 批、 内存用量 8MB」推断 128MB 够用,实测否定了这个推断。⛔ 事务级 SET LOCAL,不能全局调:work_mem 是每个排序/哈希节点的上限,不是每连接。 生产 RDS 约 7GB 内存,全局设大值遇上并发排序会吃穿。SET LOCAL 出事务自动还原, Web API 连接不受影响;场景查询串行,同一时刻只有一条,峰值可控。⛔ 别再加信号码部分索引:测试机与生产都测过,生产 −5% 落在噪声里 (同配置两次测量本身差 6%),不值得引入一个 Prisma 管不到的索引。 tests/recall-future-return-visit-gate.spec.ts 的 SQL 捕获桩补 $transaction/$executeRaw。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
补的是常态可观测性:2026-08-29 生产 plan 段从 12 分钟涨到 159 分钟仍未跑完, 而引擎中间什么都不打,只能临时起 pg_stat_activity 采样器反推。 现在每个子场景一行(SQL 与 Node 后处理分开计时)+ 每轮阶段耗时一行。 同时带上义齿按颌分支的预过滤(逻辑恒等,单患者路径实测 16.7×)—— 它针对的正是本次新增的两条 exam_findings 分支,是生产回归的头号嫌疑。
luoqi committed -
luoqi committed
-
luoqi committed
-
2026-08-29 生产 plan 段从 12 分钟涨到 159 分钟仍未跑完,而引擎从「
▶ Running engine」 到最后的统计块之间**什么都不打**,整轮是个黑盒 —— 只能临时起 pg_stat_activity 采样器 反推,才勉强定位到子场景粒度。这组日志就是为了不再重复那个过程。 ① 每个子场景一行(常态开启,每轮 11 行): [recall] sub=missing_tooth code=K08 sql=1234ms rows=567 post=89ms hits=42⭐ SQL 与 Node 后处理**分开计时** —— 今天最大的困难就是知道慢、却分不清 慢在 SQL 还是慢在 Node。 ② runAllForHost 三段(常态开启,每轮 1 行): [plan] 阶段耗时 场景=Xms 预取=Yms 写入=Zms 命中患者=N 总计=Wms 今天一度误以为瓶颈在预取,有这行就不会跑偏。 ③ PAC_RECALL_DUMP_SQL=1(默认关)保留,需要拿 SQL 原文做 EXPLAIN 时才开。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
luoqi committed
-
luoqi committed
-
2026-08-29 用 dump 开关 + pg_stat_activity 采样 + EXPLAIN ANALYZE 测出来的, 结论与直觉相反,写进代码免得后来人重走弯路: ① 11 个子场景只有 2 个慢(K05 牙周 9m13s / K01 阻生牙 13m+),其余 9 个秒回; 且与结果集大小无关 —— K02 理由数最多(49,382)却秒回。 ② 11 条 SQL 逐字节相同,只有绑定参数不同 → 是数据分布 + 执行计划,不是 SQL 写法。 ③ 真实热点:Append(resolvedTeeth 的 UNION) loops=117,256 × 1.71ms ≈ 201 秒, 占 K01 单条 301 秒的 68% —— 每候选行跑一次的 gap 相关子查询。
⛔ 已实测否定,别再试: · 子场景并发=3:24.1 分钟 vs 串行 23.3。瓶颈是共享磁盘 I/O,并行只抢同一批 page。 · 信号码部分索引:EXPLAIN 估算成本降 13×(122 万行 → 5,593 行), 墙钟 24.5 vs 23.3 —— 无效。教训:别拿估算成本当依据,要看 EXPLAIN ANALYZE 的实际时间。 (该索引的迁移已撤回,不上生产。)✅ 真正的方向(独立项目,未做):resolvedTeethSql 从逐行相关子查询改集合式。 它是召回与画像共用的单一真理源,重写必须配等价性验证,否则是拿静默少召赌运气。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
luoqi committed
-
PAC_RECALL_DUMP_SQL=1 时把每个子场景的完整 SQL + 绑定值打到日志。默认关,零开销。 为什么需要:2026-08-29 排查 plan 段耗时,采样抓到某条子场景查询单次跑 6.7 分钟, 但 pg_stat_activity.query 被 track_activity_query_size(默认 1024 字节)截断 —— 拿不到全文就没法 EXPLAIN,排查卡死在这一步。调 track_activity_query_size 要重启 PG, 生产上不划算;做成开关更可控。 实现上主查询从「$queryRaw 标签模板」改为「先建 Prisma.sql 对象再 $queryRaw(obj)」, 两者等价,但对象有 .sql / .values 可检视。 tests/recall-future-return-visit-gate.spec.ts 的 SQL 捕获桩同步兼容两种调用形态。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
修的是 2026-08-29 生产的两个真实故障: ① plan 段被拖过 2 小时 cron 间隔后套圈,雪崩自持(源头消失也不自愈) ② 18 万患者 reparse 跑满 61/61 批后倒在收尾统计的 bind 变量溢出
luoqi committed -
luoqi committed
-
2026-08-29 测试服实测(585K 患者 / 87,661 命中,空闲机): 串行(默认 1):24.6 / 21.7 / 23.5 分钟(三次) 并发 3: 24.1 分钟 —— 不但没快,还略慢 采样显示全程 DataFileRead:本阶段是共享磁盘 I/O 受限,不是查询延迟受限, 并行只让几条查询抢同一批 page,总读取量不变。要提速得减少读取量(索引/收窄扫描), 不是提高并行度。把这个负结果写进注释,免得后来人再拧一次这个旋钮。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
## 一、cron 防重入(sync-incremental.scheduler.ts)
🔴 2026-08-29 生产事故:plan 段耗时涨过 2 小时后,cron(每 2 小时)照常触发下一轮, 两轮 plan 段并发抢同一批 I/O → 都更慢 → 更易被再下一轮套圈 → 雪崩。 实测 08-28 20:15 起连续多轮 plan 段一次都没跑完;到 08-29 11:19(即触发原因 reparse 结束 1.4 小时后)仍有两轮在并行互拖 —— 雪崩是**自持**的,源头消失也不会自愈。 现有的锁都挡不住,这是本修复存在的理由: ① NestJS CronJob **默认不防重入** —— 上一次回调还在 await,下一次照样进; ② sync_logs 的 partial UNIQUE(host_id) WHERE status='running' 只覆盖**摄入段**, 摄入一结束锁就放了,而最慢的 persona / plan 段还在跑。 所以必须在回调入口用进程内 Set 挡。跳过而非排队:摄入是游标增量,下轮自然 catchup; plan 是时间驱动全量,跳一轮只是晚 2 小时评估,远好过雪崩。 释放放在 finally —— 抛异常时不释放会把该 host 锁死到进程重启。 tests/scheduler-reentrancy-guard.spec.ts 锁四条:并发跳过 / 结束后放行 / 按 host 而非全局 / 抛异常也释放。 ## 二、reparse 两处 bind 变量溢出(cold-import.service.ts) `patientId: { in: [...] }` 直接塞完整患者清单会撞 PG 的 32767 上限。 实跑路径(第 3 步统计受影响患者)—— 2026-08-29 生产实测:18 万患者的 reparse 跑满 61/61 批、写完全部事实(superseded=9,062)之后**倒在最后一步**: Assertion violation: too many bind variables ... received 32769 6.6 小时的活全干完,只因收尾统计炸掉而 exit 1。 dry-run 路径同病:>3.2 万患者直接崩,而 --patients-file 的文档恰恰说 「按受影响患者收窄是最有效的提速手段(可达 250 倍)」—— 最需要先 dry-run 探路的 大清单场景,正好是它唯一不工作的场景。 两处都按 3000 分块(与 PAC_REPARSE_BATCH 同款),每块 3001 个变量,离上限很远。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
archDentureBranch 在展开 exam_findings 的 JSON 数组之前没有任何文本级过滤, 而兄弟分支 RESTORATION_IN_PLACE 那条本来就先在整段文本上过滤了(rsrc 子查询)。 这是漏了,不是有意为之。 ARCH_DENTURE_UPPER_RE / LOWER_RE 两条都要求句中出现「义齿|假牙」,所以整段 exam_findings 里一个都没有时,任何条目都不可能命中 —— 预过滤逻辑上恒等,只剪枝。 2026-08-29 测试库实测: exam_findings 是数组的 emr 事实 1,519,829 份,含「义齿|假牙」的仅 7,396(0.49%) 按患者相关子查询(实际形态,300 患者样本):832.7ms → 49.7ms,快 16.7 倍,结果一致
⛔ 注释里写死了一条反向提醒:别用「全表扫」形态验证本优化 —— 那个形态两边都是 81 秒(顺序扫描 + detoast 压倒一切),看不出差别,照着那个数会误以为没用而删掉。 我自己 2026-08-29 就先测错了这一次。批量路径接近全表扫形态,收益不明显; 吃到 16.7 倍的是单患者路径(详情页刷新 / recomputeForPatient / reparse 后定向重算)。 tests/arch-denture-false-missing.spec.ts 新增一组用例锁「预过滤词必须始终是 两条 RE 的必要条件」的蕴含关系 —— 一旦不成立,预过滤会静默丢真命中(少召不报错)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed -
luoqi committed
-
luoqi committed
-
- 28 Aug, 2026 4 commits
-
-
luoqi committed
-
0366b572 加这条规则时只挡了 restorative(补牙散文含「调合」),放在了 periodontic 之前 —— 漏掉了牙周/预防/外科。「调合/调颌」是咬合微调,几乎任何术式的收尾都可能 带一句,它**单独出现**时才代表修复动作;和别的操作词同句时,那个操作词才是本次治疗。 2026-08-28 测试服 reparse 实测:新写入的 179 条调合/调颌事实里 18 条(10%)判错 「牙周刮治+调合」「根面平整+调合+牙周内上药」「洁治,47调合」→ 应 periodontic(16) 「涂氟,45调合」→ 应 preventive(1);拔牙类 → 应 surgical(1) 两个方向都坏: ① 牙周治疗不再解 perio_no_srp 缺口 = 多召牙周; ② prosthodontic 在结构 resolver 里,反而去解那些牙位的缺牙缺口 = 少召缺牙(不报错)。 修法:挪到本表倒数第二行(只在人群词 pediatric 之前)。原始个案照样接得住 —— 「调合,抛光」「调合抛光」里的「抛光」不在任何含词表,只有「调合」命中,仍判 prosthodontic(陈惠英@16 / 陶美玉@22)。 tests/keyword-case-and-terms.spec.ts 锁住两侧:实测误判串必须归回原类目, 原始个案必须仍是 prosthodontic。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
luoqi committed -
luoqi committed
-
代码分支 fix/treated-evidence-from-emr-text(已含 fix/polish-on-missing-tooth-implies-restoration)。 摄入期(需 reparse --subject-type=treatment): 67fe9d19 补义齿维护族(卡环/基托/重衬/假牙)+ 扩根/松牙固定/用氟/洁牙 0366b572 补含词「调合/调颌」 6ee7b8e9 姑息处置(调磨/试戴/冲洗/换药/上药)route 到 review,不再整条丢弃 查询期(plan 重算即生效,均只往 resolvedTeeth 加牙位,失败方向为少召): e0a12c44 缺失牙位上的裸「抛光」= 修复体在位 50a9c9a5+362f484e+79e63437 主诉/现病史自证已治疗(仅开缺失牙场景) 96765e1d 检查所见按条目举证修复体在位 aac4e249+1a3916a5 活动义齿「假缺失」按颌判定(限同一份病历,不整颌铺开) 1284675e 抑制改为全有或全无 —— 处置是计划级的
luoqi committed
-