Commit 89dac080 by luoqi

perf(sync): patient_transactions 补 sync_log_id 索引 —— push 全表扫 74s 的根因

FRIDAY 对接增量 push 报 `SocketTimeoutException: Read timed out`,今天在测试服
按他们的路径(公网域名 + HMAC)模拟推 2 行患者主档定位到:

  单次请求 74s,74s **全部**耗在这一条 SQL 上
  (逐 6s 采 pg_stat_activity,同一条查询年龄从 6s 一路涨到 74s):

    SELECT id, patient_id, tenant_id FROM patient_transactions
    WHERE sync_log_id = $1 AND patient_id IS NOT NULL

  来源 cold-import.service.ts `touchedRows`(push / cold-import / 增量共用这条路径)。
  EXPLAIN:Parallel Seq Scan (cost=0.00..2150645.78)

外键约束不会自动建索引,这句从上线起就在全表扫:
  测试服 patient_transactions 12,519,060 行 / 33 GB
  生产   同表           24,074,031 行 / 64 GB  ← 生产更慢,FRIDAY 真上生产会更糟

本地建索引后 plan 变 Index Scan(cost=0.29..4.31)。

── 顺带更正一处仓库里的错误注释 ──
20260701060607 那份迁移写着「CONCURRENTLY 不能跑在事务里,Prisma migrate 对整份
migration.sql 有事务包裹,故不能靠 migrate deploy 直接跑」。**当前 Prisma 6.19 已不成立** ——
本地实测 migrate deploy 直接把 CONCURRENTLY 跑通、索引建出、plan 生效。
新迁移里写清了真实情况,并保留「推荐手动先建」的建议,但理由改成正确的那个:
deploy 时 pac-service 要等 pac-migrate 退出才启动,33/64GB 建索引几分钟 = 白等的停机。

push 侧本身是好的:同一次模拟 accepted=2 / failed=0 / mappingMisses=0,
canonical 映射(externalId/name/gender/birthDate/medicalRecordNumber/phone)全对。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent 1171fb42
-- CreateIndex
--
-- 【为什么要这条索引】
-- 每次摄入收尾都要按 sync_log_id 反查本轮 touched 患者
-- (cold-import.service.ts `touchedRows`,push / cold-import / 增量共用这一条路径):
-- SELECT id, patient_id, tenant_id FROM patient_transactions
-- WHERE sync_log_id = $1 AND patient_id IS NOT NULL
-- 外键约束**不会**自动建索引,所以这句一直在全表扫。
--
-- 2026-07-27 测试服实测(FRIDAY 对接增量 push 报 SocketTimeoutException 时定位到):
-- 推 2 行患者主档 → 单次请求 74s,74s 全耗在上面这条 SQL(逐 6s 采 pg_stat_activity,
-- 同一条查询年龄从 6s 一路涨到 74s);EXPLAIN:
-- Parallel Seq Scan on patient_transactions (cost=0.00..2150645.78 rows=1579)
-- patient_transactions:测试服 12,519,060 行 / 33 GB;生产 24,074,031 行 / 64 GB。
-- 建索引后本地实测 plan 变 Index Scan(cost=0.29..4.31)。
-- 这条随摄入量增长只会更糟 —— 属结构性缺失,不是调优。
--
-- 【CONCURRENTLY 与上线方式】
-- 普通 CREATE INDEX 会 ACCESS EXCLUSIVE 锁全表,33/64GB 上要锁几分钟,期间摄入与 push 全挂,
-- 故用 CONCURRENTLY(不锁写)。
--
-- ⚠️ 关于「Prisma migrate 有事务包裹所以跑不了 CONCURRENTLY」——
-- 那是 20260701060607 里的说法,**在当前 Prisma 6.19 已不成立**:2026-07-27 本地实测
-- `prisma migrate deploy` 直接把本文件跑通、索引建出、plan 生效。所以本条**可以**随
-- migrate deploy 走,不需要 `migrate resolve` 那套。
--
-- 但**推荐仍手动先建**,理由与事务无关,是停机时间:
-- deploy 时 pac-service 通过 depends_on 等 pac-migrate 退出才启动,而 33/64GB 建索引要几分钟
-- → 白等一段服务不可用。先手动建好(业务照常),deploy 时本条 IF NOT EXISTS 直接空跑:
-- docker exec pac-postgres-1 psql -U pac -d pac -c \
-- 'CREATE INDEX CONCURRENTLY IF NOT EXISTS "patient_transactions_sync_log_id_idx" \
-- ON "patient_transactions"("sync_log_id");'
CREATE INDEX CONCURRENTLY IF NOT EXISTS "patient_transactions_sync_log_id_idx" ON "patient_transactions"("sync_log_id");
...@@ -516,6 +516,16 @@ model PatientTransaction { ...@@ -516,6 +516,16 @@ model PatientTransaction {
@@index([receivedAt]) @@index([receivedAt])
/// 水位游标查询(persona 重算"自上次以来新增的 transaction") /// 水位游标查询(persona 重算"自上次以来新增的 transaction")
@@index([eventSeq]) @@index([eventSeq])
/// 一次摄入产生了哪些 transaction —— 每次 push / cold-import / 增量收尾都要按 sync_log_id
/// 反查 touched 患者(cold-import.service.ts `touchedRows`)
///
/// ⚠️ 外键**不会**自动建索引。缺这条 每次 push 全表 Seq Scan:
/// 2026-07-27 测试服实测,FRIDAY 对接推 2 行患者主档耗时 74s,EXPLAIN
/// `Parallel Seq Scan on patient_transactions (cost=0.00..2150645.78)`,
/// 1251 万行 / 33GB;生产同表 2407 万行 / 64GB(更慢)
/// 对方 HTTP 客户端直接 `SocketTimeoutException: Read timed out`
/// 这条随表增长只会更糟 —— 摄入量越大、push 越慢,属结构性问题不是调优。
@@index([syncLogId])
/// 幂等键 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