Commit 7d20ca2b by luoqi

perf(召回): 场景查询事务级 work_mem=256MB —— 生产实测降 30~34%

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>
parent 4ac764b6
......@@ -517,7 +517,32 @@ export class TreatmentInitiationRecallScenario implements PlanScenarioPlugin {
// 2026-08-29 排查时最大的困难,就是知道 plan 段慢、却分不清慢在 SQL 还是慢在 Node,
// 只能靠采样 pg_stat_activity 反推。有了这两个数,看一眼日志就知道该往哪查。
const sqlStart = Date.now();
const rows: HitRow[] = await this.prisma.$queryRaw(scenarioSql);
// ⚡ 本查询单独抬高 work_mem —— 2026-08-29 **生产**六轮实测(endo_no_rct,72,769 行):
// 配置 耗时 相对基线
// 无索引 + 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 而不是 128MB:收益对取值**很敏感**,128→256 还差一倍。
// 原本按"溢出只有 4 批、内存用量 8MB"推断 128MB 够用,实测否定了这个推断。
// 原因:默认 4MB 太小,EXPLAIN ANALYZE 里看到哈希溢出成 4 批
// (Buckets:131072 Batches:4 Memory Usage:8032kB)+ 临时文件读写 11,865 页。
//
// ⛔ 必须用事务级 SET LOCAL,**不能全局调**:work_mem 是「每个排序/哈希节点」的上限,
// 不是每连接。生产 RDS 只有约 7GB 内存(shared_buffers 1.83GB 反推),全局设大值
// 遇上几十个并发连接同时排序会把内存吃穿。SET LOCAL 出了事务自动还原,
// Web API 的连接完全不受影响;且场景查询是串行的,同一时刻只有一条在跑,峰值可控。
//
// ⛔ 别再去加「信号码部分索引」——(content->>'code') 上的部分索引在测试机和生产
// 都实测过,生产上 −5% 落在噪声里(同配置两次测量本身差 6%),不值得引入
// 一个 Prisma 管不到的索引。详见 [[GAP_FLAGS_BY_PRIMARY]] 附近的耗时归因注释。
const [, rows] = await this.prisma.$transaction([
this.prisma.$executeRaw`SET LOCAL work_mem = '256MB'`,
this.prisma.$queryRaw<HitRow[]>(scenarioSql),
]);
const sqlMs = Date.now() - sqlStart;
const postStart = Date.now();
......
......@@ -62,7 +62,12 @@ function makeSqlCapturingPrisma(): { prisma: PrismaService; sql: () => string }
captured.push(parts.map((s, i) => s + (i < values.length ? flatten(values[i]) : '')).join(''));
return Promise.resolve([]);
};
const prisma = { $queryRaw: queryRaw } as unknown as PrismaService;
// 主查询走 `$transaction([SET LOCAL work_mem, $queryRaw(sql)])`(2026-08-29 起,
// 为抬高 work_mem;见 scenario 里那段实测注释)。桩要同时认这三个:
// $queryRaw 负责捕获 SQL,$executeRaw 吞掉 SET LOCAL,$transaction 把数组按序解析。
const noop = () => Promise.resolve(0);
const tx = (arr: unknown[]) => Promise.all(arr as Promise<unknown>[]);
const prisma = { $queryRaw: queryRaw, $executeRaw: noop, $transaction: tx } as unknown as PrismaService;
return { prisma, sql: () => captured.join('\n/* --- next query --- */\n') };
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment