Commit a267668f by luoqi

merge: main → test(反向合)

parents 734f693f ef4d3274
Pipeline #3646 failed in 0 seconds
# 增量摄入的回看窗:为什么是 48 小时,能不能缩
> 结论先行:**不能缩,而且根因不在我们这边**。真正的解法是让 DW 提供「入仓时间」列。
> 测量日期 2026-08-31,生产 jvs-dw(113 万患者)。全程只读。
## 1. 现象
每轮增量(2 小时一轮)的账:
```
fetched = 248,112 行 其中 dup = 208,556(84%)是重复
实际只写 transactions = 16,448 / facts = 12,854
耗时 31~35 分钟,占整轮(约 1h57m)的 27%
```
直觉是「84% 白拉,把回看窗从 48h 缩到 24h 就能砍一半」。**测下来这个直觉是错的。**
## 2. 根因:游标跟的量与数据到达顺序无关
- 游标跟的是 **`updated_date`** —— **源 HIS 系统**记录被改动的时刻,≈ 事件发生时间
- 数据何时能被 PAC 查到,取决于 **DW 自己的 ETL**,与 `updated_date` 无关
于是必然存在「`updated_date` 比游标旧、但此刻才到达」的行 —— 游标无论怎么设都追不上。
**回看窗不是冗余,是对上游不可控延迟的唯一防线。**
最干净的证据(退费记录):
```
created_date 08-23 13:05
updated_date 08-23 13:13 ← 源系统 8 分钟内当场写完
received_at 08-25 12:39 ← PAC 47 小时后才看到
```
中间那 47 小时 `updated_date` 一动没动。
## 3. 落库滞后实测(7 天,281,760 条**增量**记录)
| 分位 | 滞后 |
|---|---|
| p50 | 2.39h |
| p95 | 5.80h |
| p99.9 | 15.39h |
| max | **47.43h** |
| 阈值 | 超过的行数 |
|---|---|
| >12h | 4,389 |
| >24h | **31** |
| >36h | **19** |
| >48h | **0** |
**按表拆开后,两类行为截然不同:**
| 表 | 条数 | max 滞后 | >24h |
|---|---|---|---|
| **refund** | 150 | **47.4h** | **27** |
| treatment | 48,880 | 42.2h | 4 |
| image | 48,346 | 19.8h | 0 |
| emr / diagnosis / recommendation | 60,748 | 15.5~15.6h | 0 |
| appointment / encounter / payment | 123,402 | 14.9~15.0h | 0 |
**召回真正依赖的临床表(diagnosis / emr / recommendation / encounter)7 天内最大滞后 15.5h,超 24h 一条没有。**
顶到 48h 边界的是 **refund**,而召回逻辑不看退费。
## 4. ⛔ 为什么仍然不能全局缩窗
1. 缩到 24h 会丢 **31 条**、缩到 36h 会丢 **19 条**。游标一旦推过去就是**永久漏拉**,
下一轮补不回来(见 [[incremental-ingest-history-gap]])。
2. **max = 47.43h 恰好顶在 48h 边界、>48h 为 0** —— 这是**截断的指纹**,不是"刚好够"。
真实分布的尾巴可能更长,我们只是观察不到。
> 🔴 **这个测量的方向是单边的**:它能证明「48h 绰绰有余」,**不能**证明「48h 不够」。
> 而现在数据显示边界被顶到了,连"绰绰有余"都谈不上。别拿单边结论去支撑双边决策。
## 5. 根治办法(不在我们这边):请 DW 提供入仓时间列
**DW 当前一个入仓时间字段都没有。** 十张表的时间列只有 `created_date` / `updated_date`,
全是源 HIS 的;没有 `etl_date` / `load_time` / `dw_insert_time`,也没有分区列
(`rq` 是业务日期,只到天,已排除)。
若 DW 增加一列由其 ETL 写入、只增不改的时间戳:
| | 现在 | 有入仓时间之后 |
|---|---|---|
| 游标语义 | 事件时间,与到达顺序无关 | **在到达顺序上单调** |
| 漏拉风险 | 靠窗口够大来赌 | **结构上不可能** |
| 每轮 fetched | 248,112 行(84% 重复) | 约 16,000 行 |
| 摄入耗时 | 31~35 分钟 | 预计 5~10 分钟 |
## 6. 我们这边的缓解手段(未采纳)
**按表分设回看窗**:临床表 24h、退费/支付 48h+。拉取量约降一半,摄入 31m → ~20m。
**没做,理由**:
- 引入「每张表一个窗口」的配置复杂度,且必须持续盯着 DW 各表的延迟特性有没有变
- `treatment` 有 4 条 >24h(最大 42.2h),它是判「已治疗」的关键表,漏了会**误召**,
需要单独定窗或先查清成因
- 在根治方案可能拿得到的情况下,性价比不高
## 7. ⛔ 取样陷阱(第一版测错了)
第一版按 `received_at > now() - 7 days` 一刀切,得出 p50 滞后 **43,889 小时(5 年)**
max 14.7 年的荒谬结果。原因:窗口内混进了 08-24~08-26 的 **5 次 `full:` 全量补摄**,
那些行的 `updated_date` 是几年前的历史数据。
**在既有增量又有补摄的系统里,按时间窗取样是不成立的**,必须按事件来源限定
(`sync_logs.triggered_by LIKE 'sync:%'`)。修正后样本从 689 万降到 28 万,结论才成立。
## 8. 复现方式(只读,约 9 秒)
```sql
SELECT count(*), percentile_cont(0.999) WITHIN GROUP (ORDER BY lag_h), max(lag_h),
count(*) FILTER (WHERE lag_h > 24)
FROM (
SELECT EXTRACT(EPOCH FROM (t.received_at - (t.raw_payload->>'updated_date')::timestamptz))/3600 AS lag_h
FROM patient_transactions t JOIN sync_logs s ON s.id = t.sync_log_id
WHERE s.triggered_by LIKE 'sync:%' AND s.started_at > now() - interval '7 days'
AND t.raw_payload ? 'updated_date'
) x WHERE lag_h >= 0;
```
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