Commit 1c537ccb by luoqi

perf(reparse): patient_transactions 补 (host_id, subject_type, patient_id) 复合索引

reparse 分批取源行那句:
  SELECT id, raw_payload, payload_hash FROM patient_transactions
  WHERE host_id = $1 AND subject_type IN ('emr','diagnosis') AND patient_id IN (3000 个)

没这条索引时 planner 只能走 (patient_id, occurred_at),把这 3000 个患者的**全部**
transaction 连 raw_payload 一起捞进堆,再在堆上过滤 host_id / subject_type。

2026-07-27 测试服 EXPLAIN ANALYZE 实测(12,519,060 行 / 33GB):
  Bitmap Heap Scan  10,385 ms
  rows=14,578,Rows Removed by Filter: 39,305   ← 读进来 73% 是白读
  Buffers: read=15,616(约 122MB 磁盘读)
全量 87 批,单这一句约占 15 分钟。本地建索引后 planner 已改走
Index Scan using patient_transactions_host_id_subject_type_patient_id_idx。

️ 收益如实说:每批总耗时 76–180s,这句只占 6–14%,对全程约 4%,**不是提速主手段**。
   reparse 真正的杠杆是上一个提交的 --patients-file(按受影响患者收窄,实测最高 250 倍)。
   本索引胜在一次性代价、之后每次 reparse / 每个 host 都受益。

体积:(uuid,text,uuid) × 千万行,测试服估 ~600MB、生产(2407 万行 / 64GB)估 ~1.2GB。
     测试服建前 24G 空闲够用;生产上线前需先确认磁盘余量。

上线方式同 20260727070000:CONCURRENTLY(不锁写),推荐部署前手动先建 ——
deploy 时 pac-service 要等 pac-migrate 退出才启动,几分钟索引构建 = 白等的停机;
手动建好后本迁移 IF NOT EXISTS 空跑。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent 6e7d1f60
-- CreateIndex
--
-- 【为什么要这条索引】
-- reparse 分批重建源表时,每批取这 3000 个患者的源行(cold-import.service.ts):
-- SELECT id, raw_payload, payload_hash FROM patient_transactions
-- WHERE host_id = $1 AND subject_type IN ('emr','diagnosis') AND patient_id IN (3000 个)
-- 没有 (host_id, subject_type, patient_id) 时 planner 只能走 (patient_id, occurred_at),
-- 把这些患者的**全部** transaction 连 raw_payload 一起捞进堆,再在堆上过滤 host/subject_type。
--
-- 2026-07-27 测试服 EXPLAIN ANALYZE 实测(patient_transactions 12,519,060 行 / 33 GB):
-- Bitmap Heap Scan actual time=997..10382ms → 10,385 ms
-- rows=14,578,Rows Removed by Filter: 39,305 ← 读进来 73% 是白读
-- Buffers: shared hit=8,723 read=15,616(约 122MB 磁盘读)
-- 全量 reparse 87 批,单这一句约占 15 分钟。
--
-- ⚠️ 收益如实说:每批总耗时 76–180s,这句占 6–14%,补索引对全程约 4%。
-- reparse 真正的提速手段是 CLI 的 --patients-file(按受影响患者收窄,实测最高 250 倍)。
-- 本索引胜在一次性代价、之后每次 reparse / 每个 host 都受益。
--
-- 【体积】(uuid, text, uuid) × 千万行,测试服估 ~600MB、生产(24,074,031 行 / 64GB)估 ~1.2GB。
-- 测试服建前 24G 空闲,够;生产上线前请先确认磁盘余量。
--
-- 【CONCURRENTLY 与上线方式】同 20260727070000(sync_log_id 那条):
-- 普通 CREATE INDEX 会 ACCESS EXCLUSIVE 锁全表数分钟,摄入与 push 全挂,故用 CONCURRENTLY。
-- 当前 Prisma 6.19 可以直接 `migrate deploy` 跑通(20260701060607 里"有事务包裹跑不了"的
-- 说法已不成立,2026-07-27 实测)。但**推荐部署前手动先建**:deploy 时 pac-service 要等
-- pac-migrate 退出才启动,几分钟索引构建 = 白等的停机。手动建好后本条 IF NOT EXISTS 空跑:
-- docker exec pac-postgres-1 psql -U pac -d pac -c \
-- 'CREATE INDEX CONCURRENTLY IF NOT EXISTS "patient_transactions_host_id_subject_type_patient_id_idx" \
-- ON "patient_transactions"("host_id","subject_type","patient_id");'
CREATE INDEX CONCURRENTLY IF NOT EXISTS "patient_transactions_host_id_subject_type_patient_id_idx" ON "patient_transactions"("host_id", "subject_type", "patient_id");
...@@ -516,6 +516,19 @@ model PatientTransaction { ...@@ -516,6 +516,19 @@ model PatientTransaction {
@@index([receivedAt]) @@index([receivedAt])
/// 水位游标查询(persona 重算"自上次以来新增的 transaction") /// 水位游标查询(persona 重算"自上次以来新增的 transaction")
@@index([eventSeq]) @@index([eventSeq])
/// reparse 按批取源行:`WHERE host_id = ? AND subject_type IN (...) AND patient_id IN (3000)`
/// (cold-import.service.ts 分批重建源表那段)
///
/// 没这条时 planner (patient_id, occurred_at):把这 3000 个患者的**全部** transaction
/// raw_payload 一起捞进堆,再在堆上按 host_id / subject_type 过滤。
/// 2026-07-27 测试服 EXPLAIN ANALYZE 实测(12,519,060 / 33GB):
/// Bitmap Heap Scan 10,385ms; 53,883 行只要 14,578 —— Rows Removed by Filter: 39,305(73% 白读)
/// Buffers: read=15,616( 122MB 磁盘读);全量 87 这一句就占约 15 分钟
///
/// ⚠️ 收益要说实话:每批 76180s 里它只占 614%,对全程约 4%,**不是提速主手段** ——
/// 真正的杠杆是 CLI `--patients-file` 按受影响患者收窄(实测最高 250 )
/// 这条胜在每次 reparse / 每个 host 都受益,代价只有一次建索引。
@@index([hostId, subjectType, patientId])
/// 幂等键 partial UNIQUE (host_id, tenant_id, source_event_id) WHERE source_event_id IS NOT NULL /// 幂等键 partial UNIQUE (host_id, tenant_id, source_event_id) WHERE source_event_id IS NOT NULL
/// Prisma 不支持声明式 partial unique;migration 20260528000002 是单一真理源。 /// Prisma 不支持声明式 partial unique;migration 20260528000002 是单一真理源。
/// (init migration 漏建,20260528000002 dedup + 建索引) /// (init migration 漏建,20260528000002 dedup + 建索引)
......
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