---
title: 数据摄入
description: PAC 的数据摄入(首次全量 / 日级增量)、数据完整性、资源调优与配置变更。
icon: Import
---

`pnpm sync` 是统一入口:首跑(游标为空)等价全量,之后增量。`pnpm cold-import` 保留为"忽略游标强制全量"的旧别名,日常一律用 `pnpm sync`。

> 命令里 `<host>` 指一个 host 标识,对应配置目录 `apps/pac-service/data/<host>/`。

---

## 一、首次全量

**用途**:首次部署 / 重建库 / 配置大版本变更。

**机制**:游标为空即等价全量,**按患者 cohort 分批** —— 每批 load → transform → assemble → 批量写 → 释放内存 → 下一批,内存恒定在 1G 以内,不会因一次性载入而溢出。

```bash
# Docker(prod)— 必带 --env-file
docker compose --env-file apps/pac-service/.env -f docker-compose.prod.yml exec -T \
  -e PAC_COHORT_CONCURRENCY=3 \
  pac-service pnpm sync:prod -- --dir=./data/<host> --cohort-batch=500 --no-recompute

# 裸机 / 本地
PAC_COHORT_CONCURRENCY=3 pnpm sync -- --dir=./data/<host> --cohort-batch=5000 --no-recompute
```

| 参数 | 含义 |
|---|---|
| `--cohort-batch=N` | 每批 N 个患者(默认 5000;数据源较慢时调小到 500 看进度更细) |
| `PAC_COHORT_CONCURRENCY=3` | 3 个 cohort 并发(默认 1 串行);连接池自动随并发放大 |
| `--no-recompute` | 只拉数据,persona / plan 留待单独跑(大数据量分阶段更可控) |

> **量级参考**:十万级患者、`concurrency=3` 约 1.5–2 小时,内存峰值 < 1G。

全量完成后手动触发重算(增量模式下 cron 会自动联动,首跑用了 `--no-recompute` 时需手动补):

```bash
pnpm recompute-persona:prod -- --host=<host>
pnpm recompute-plans:prod -- --host=<host>
```

**验证**:

```sql
-- 规模自检:三者都应 > 0(导入成功)
SELECT 'patients' AS t, count(*) FROM patients
UNION ALL SELECT 'tx', count(*) FROM patient_transactions
UNION ALL SELECT 'facts', count(*) FROM patient_facts;

-- 一致性自检:下面两个都应 = 0(无重复 tx、无多 active fact)
SELECT count(*) AS dup_tx FROM (SELECT source_event_id,host_id,tenant_id FROM patient_transactions
  WHERE source_event_id IS NOT NULL GROUP BY 1,2,3 HAVING count(*)>1) x;
SELECT count(*) AS multi_active FROM (SELECT subject_id,host_id,tenant_id FROM patient_facts
  WHERE status='active' GROUP BY 1,2,3 HAVING count(*)>1) y;
```

---

## 二、日级增量

`SyncIncrementalSchedulerService` 启动时**为每个 `auto_sync: true` 的 host 各注册一个 CronJob**,走与全量同一套 `importDirectory`。cron 时间取自该 host `manifest.incremental_cron`(按 manifest `timezone` 解释),缺省回退全局 `PAC_INCREMENTAL_CRON` —— 各 host 在自己上游刷新窗口后跑,不再挤一个全局时刻:

```yaml
# data/<host>/manifest.yaml
incremental_cron: '15 8 * * *'   # 该 host 数据源当日刷新之后
```

```
到点(每 host 各自 cron)
   ↓
importDirectory({ incremental: true })
   - 并发锁:sync_logs partial UNIQUE (host_id) WHERE status='running' AND direction='pull'
     同 host 已在跑 → 跳过,等下次 cron
   - 读上次 cursor_after → SQL WHERE updated_date > (cursor − 回看窗 48h)
     (回看重叠靠 source_event_id 去重;增量数据量小,通常 1 批)
   - 拉新增/变更 fact,并补全主档(见 §三)
   - 写 cursor_after = 本次起跑时间(非 max(updated_date)),
     保证跑期间数据源的任何新写入下次都能捞回
   ↓
Persona 重算(本次涉及的患者)→ Plan 重算(SQL 召回 + 多因子打分)
```

> 端到端约 T+1(数据源本身比临床事件晚一天)。

手动触发(调试 / 紧急补跑):

```bash
pnpm sync -- --dir=./data/<host>                 # 增量 + 联动重算
pnpm sync -- --dir=./data/<host> --no-recompute  # 只拉数据
pnpm sync -- --dir=./data/<host> --dry-run       # 不写库,预览游标注入
pnpm sync -- --dir=./data/<host> --full          # 忽略游标强制全量(灾后)
```

---

## 三、数据完整性

**幂等(数据库双 UNIQUE)** —— 任何并发 / 重跑结果一致:

- `patient_transactions`:partial UNIQUE `(host_id, tenant_id, source_event_id) WHERE source_event_id NOT NULL`
- `patient_facts`:UNIQUE `(host_id, tenant_id, subject_id, version)` + active 行 partial UNIQUE

`source_event_id` 内含 `updatedAt`,因此源数据行级 in-place 更新会自然产生新 tx 与新 fact 版本(旧版 supersede);同行同 `updatedAt` 则幂等跳过。

**主档补全(两条保障,确保 patient_id 不丢)**:

- 增量只拉到 fact、主档游标未动时,用三段键建空 stub,真实主档随后 upsert 补 PII;
- 每次跑完按 `(patient_id, brand)` 反向强拉一次主档,补齐当批关联。

**数据源无关**:cohort 的取数维度(患者列表来源、患者键、租户键、游标列)全部由 `manifest.sql_source.cohort` 声明,代码不硬编码表名列名 —— 接新 host 只改 yaml。

---

## 四、资源调优

| 旋钮 | env / 参数 | 作用 | 建议 |
|---|---|---|---|
| cohort 大小 | `PAC_COHORT_BATCH_SIZE` / `--cohort-batch=N` | 每批患者数(控内存) | 5000;数据源较慢看进度可调 500 |
| cohort 并发 | `PAC_COHORT_CONCURRENCY` | 并发 cohort 数(提吞吐) | 默认 1 稳;3-4 并行(连接池自动放大) |
| 写批大小 | `PAC_WRITE_BATCH_SIZE` | 每批写入行数 | 1000;0 = 逐行(回退开关) |
| query 并行 | (自动) | 多个数据源 query 并发 | load 阶段约 4x,无需配置 |

并发安全:cohort 是互不相交的患者集,所有写操作按患者隔离,叠加数据库双 UNIQUE 兜底,任何并发下都一致(并发模式下禁用 checkpoint resume,靠幂等从头重跑)。

---

## 五、配置变更

配置 yaml 位于 `apps/pac-service/data/<host>/`。

### 5.1 变更影响速查

| 改动 | 影响 | 处理 |
|---|---|---|
| `enum_mapping` / `keyword_mapping` 加改术语 | 新数据生效,老数据需重解析 | `pnpm reparse --subject-type=<canonical>`(§5.2,不用全量重摄) |
| `field_mapping` 改字段名 | 同上 | 同上 |
| `transforms` 改 split / derive | 影响装配管道 | 同上(reparse 会重跑 transforms) |
| `scenario` SQL 改召回口径 | 立即生效(下次重算) | 等 cron 或手动 `recompute-plans`(facts 不变,无需 reparse) |
| `canonical-codes` 字典 | 立即生效 | 重启 pac-service |

### 5.2 Reparse —— 字典改了补存量(无需全量重摄)

改了 assembler yaml(`enum_mapping` / `keyword_mapping` / `field_mapping` / `transforms`)后,要把**已入库的老 fact 按新口径重新衍生**。无需 truncate + 全量重摄(那会重拉数据源且删掉执行历史与分配)。`reparse` 用已存的 `transaction.rawPayload`(原始源行)**离线**重跑装配 + 解析:

- **非破坏**:fact 走版本流(内容变则升版本 supersede,不变则跳过),不 truncate、不碰 transaction 账本,客服执行历史与分配全部保留;
- **不连数据源**:rawPayload 在本地,比全量重摄快一个量级;
- **可圈定**:按 host / subject-type / 患者集 / dry-run / 是否重算;
- **覆盖范围**:field/enum/keyword_mapping、transforms 算子、parser 改动;**不覆盖**改了 `sql_source` 的 SELECT 本身(需从数据源多拉列/表,那才需真重摄)。非 transform 产出的资源(如 SQL 视图类)会自动跳过。

```bash
# dry-run:报 scope(患者数 / txn 数),不写库
pnpm reparse:prod -- --host=<host> --subject-type=diagnosis --dry-run

# 实跑某资源(按患者分批,内存恒定;自动定向重算受影响患者的 persona/plan)
pnpm reparse:prod -- --host=<host> --subject-type=diagnosis

# 限定患者集(秒级,改 keyword 只影响一小批时用)
pnpm reparse:prod -- --host=<host> --subject-type=diagnosis --patient=<uuid>,<uuid>

# 只重衍 fact、不重算(超大批量时,稍后单独 recompute-plans)
pnpm reparse:prod -- --host=<host> --subject-type=diagnosis --no-recompute

# Docker(prod)容器内 detached 跑 + 看日志
docker compose --env-file apps/pac-service/.env -f docker-compose.prod.yml \
  exec -d pac-service sh -c "pnpm reparse:prod -- --host=<host> --subject-type=diagnosis > /tmp/reparse.log 2>&1"
docker compose -f docker-compose.prod.yml exec -T pac-service tail -f /tmp/reparse.log
```

| 参数 | 含义 |
|---|---|
| `--subject-type=<canonical>` | 限定资源(如 diagnosis / treatment_actual);空 = 所有 transform 产出的资源 |
| `--patient=<uuid,...>` | 限定患者集(秒级);空 = 全 host(分批) |
| `--dry-run` / `--no-recompute` | 只报 scope / 只重衍 fact 不重算 |
| env `PAC_REPARSE_BATCH` | 全量时每批患者数(默认 3000,控内存) |

> **典型场景**:源数据中文诊断名变体繁多、多数无标准 ICD 码,全靠 enum / keyword 白名单翻译;漏映射会导致 `code=null`、进不了对应病种召回。修法:在 `diagnosis.yaml` 补 keyword → `pnpm reparse:prod -- --subject-type=diagnosis`,即可把该病种此前漏召的存量患者补回召回池。十万级患者全量 reparse 内存恒定在 1G 以内,单患者 / 小批为秒级。
