医疗 FHIR 临床数据分析数仓实践

将 HL7 FHIR 格式的临床数据(患者、就诊、检验、用药)入库,构建支持临床质控指标计算和医疗费用管控的多层数仓。本文以 5 名患者、5 条就诊、7 条检验、6 条医嘱的模拟 FHIR JSON 数据集为基础,端到端演示 ODS(原始 FHIR JSON)→ DWD(解析层)→ DWS(聚合层)→ ADS(临床质控指标) 的完整构建过程,覆盖 JSON 嵌套字段提取、Dynamic Table 增量计算、Column Masking PHI 脱敏和 Time Travel 历史快照四项关键能力。


概述

医疗 FHIR 临床数据仓库的核心挑战是:FHIR 消息以嵌套 JSON 形式存储,患者、就诊、检验、医嘱四类资源结构各异,需要先提取字段再建立关联,同时患者姓名、出生日期等 PHI(Protected Health Information)字段必须对非特权用户脱敏。

问题云器解决方案
FHIR JSON 嵌套深(name[0].given[0]、reasonCode[0].coding[0].code 等)
get_json_object
get_json_object
+ JSONPath 语法,按需提取任意层级字段
四类 FHIR resource 需要关联分析Dynamic Table,声明式 JOIN SQL,系统自动增量刷新
ODS → DWD → DWS → ADS 数据管道Dynamic Table 链式依赖,上游刷新后下游自动跟进
患者姓名、出生日期等 PHI 字段需脱敏Column Masking,绑定到列,对非特权用户透明生效
保险对账需调取历史月份数据快照Time Travel,
TIMESTAMP AS OF
TIMESTAMP AS OF
语法点查任意时间点数据
临床路径合规率等质控指标每日更新ADS 层 Dynamic Table + Studio Task 每日定时刷新

涉及的 SQL 命令

命令 / 函数用途说明
CREATE TABLE
CREATE TABLE
建 ODS 层 FHIR 原始表每种 resource 对应一张表,存完整 JSON
get_json_object
get_json_object
从 FHIR JSON 中按 JSONPath 提取字段支持
$
$
路径,数组用
[0]
[0]
下标
CREATE DYNAMIC TABLE
CREATE DYNAMIC TABLE
构建 DWD / DWS / ADS 各层声明式 SQL,系统自动增量计算
REFRESH DYNAMIC TABLE
REFRESH DYNAMIC TABLE
手动触发一次刷新首次构建或调试时使用
ALTER TABLE ... CHANGE COLUMN ... SET MASK
ALTER TABLE ... CHANGE COLUMN ... SET MASK
绑定 PHI 列的脱敏函数对查询透明生效,包括 Dynamic Table
TIMESTAMP AS OF
TIMESTAMP AS OF
Time Travel 历史快照查询保险对账场景按月回溯
DATEDIFF
DATEDIFF
计算住院天数(LOS)、患者年龄时间差计算
FLOOR / CAST
FLOOR / CAST
数值类型转换年龄取整、JSON 字符串转 DOUBLE

前置准备

本文所有示例在

best_practice_fhir_clinical
best_practice_fhir_clinical
Schema 下运行。

CREATE SCHEMA IF NOT EXISTS best_practice_fhir_clinical;


ODS 层:原始 FHIR JSON 表

ODS 层直接存储完整的 FHIR resource JSON 字符串,保留原始报文不做解析,便于后续字段扩展和重放。

建表

-- FHIR Patient resource CREATE TABLE IF NOT EXISTS best_practice_fhir_clinical.doc_fhir_patient ( patient_id STRING, resource_json STRING ); -- FHIR Encounter resource(就诊事件) CREATE TABLE IF NOT EXISTS best_practice_fhir_clinical.doc_fhir_encounter ( encounter_id STRING, patient_id STRING, resource_json STRING ); -- FHIR Observation resource(检验/生命体征) CREATE TABLE IF NOT EXISTS best_practice_fhir_clinical.doc_fhir_observation ( obs_id STRING, patient_id STRING, resource_json STRING ); -- FHIR MedicationRequest resource(用药医嘱) CREATE TABLE IF NOT EXISTS best_practice_fhir_clinical.doc_fhir_medication_request ( req_id STRING, patient_id STRING, resource_json STRING );

写入模拟数据

生产环境通过 COPY INTO + Volume 批量导入 FHIR JSON 文件。

从本地 CSV 导入数据(推荐):

-- 第一步:通过 SQL PUT 将本地 CSV 文件上传到 User Volume PUT '/path/to/your/data.csv' TO USER VOLUME FILE 'data.csv';

-- 第二步:从 User Volume COPY INTO 表 COPY INTO best_practice_fhir_clinical.doc_fhir_patient FROM USER VOLUME USING csv OPTIONS('header'='true', 'sep'=',', 'nullValue'='') FILES ('data.csv');

也可直接内联插入小批量测试数据(不需要 CSV 文件):

写入 5 条患者数据(Patient resource):

INSERT INTO best_practice_fhir_clinical.doc_fhir_patient VALUES ('P001', '{"resourceType":"Patient","id":"P001","name":[{"family":"Zhang","given":["Wei"]}],"gender":"male","birthDate":"1980-05-15","address":[{"city":"Shanghai","postalCode":"200000"}]}'), ('P002', '{"resourceType":"Patient","id":"P002","name":[{"family":"Li","given":["Fang"]}],"gender":"female","birthDate":"1972-11-23","address":[{"city":"Beijing","postalCode":"100000"}]}'), ('P003', '{"resourceType":"Patient","id":"P003","name":[{"family":"Wang","given":["Jun"]}],"gender":"male","birthDate":"1955-03-08","address":[{"city":"Guangzhou","postalCode":"510000"}]}'), ('P004', '{"resourceType":"Patient","id":"P004","name":[{"family":"Chen","given":["Mei"]}],"gender":"female","birthDate":"1990-07-30","address":[{"city":"Shenzhen","postalCode":"518000"}]}'), ('P005', '{"resourceType":"Patient","id":"P005","name":[{"family":"Liu","given":["Yang"]}],"gender":"male","birthDate":"1968-01-12","address":[{"city":"Chengdu","postalCode":"610000"}]}');

写入 5 条就诊数据(Encounter resource,含 ICD-10 诊断码):

INSERT INTO best_practice_fhir_clinical.doc_fhir_encounter VALUES ('E001', 'P001', '{"resourceType":"Encounter","id":"E001","status":"finished","class":{"code":"IMP","display":"inpatient"},"subject":{"reference":"Patient/P001"},"period":{"start":"2026-01-10T08:00:00Z","end":"2026-01-15T14:00:00Z"},"reasonCode":[{"coding":[{"system":"http://hl7.org/fhir/sid/icd-10","code":"I50.0","display":"Congestive heart failure"}]}],"serviceProvider":{"reference":"Organization/Hospital-A","display":"Cardiology"}}'), ('E002', 'P002', '{"resourceType":"Encounter","id":"E002","status":"finished","class":{"code":"AMB","display":"ambulatory"},"subject":{"reference":"Patient/P002"},"period":{"start":"2026-02-03T09:30:00Z","end":"2026-02-03T11:00:00Z"},"reasonCode":[{"coding":[{"system":"http://hl7.org/fhir/sid/icd-10","code":"E11.9","display":"Type 2 diabetes mellitus"}]}],"serviceProvider":{"reference":"Organization/Hospital-A","display":"Endocrinology"}}'), ('E003', 'P003', '{"resourceType":"Encounter","id":"E003","status":"finished","class":{"code":"IMP","display":"inpatient"},"subject":{"reference":"Patient/P003"},"period":{"start":"2026-01-20T07:00:00Z","end":"2026-01-28T16:00:00Z"},"reasonCode":[{"coding":[{"system":"http://hl7.org/fhir/sid/icd-10","code":"J44.1","display":"COPD with acute exacerbation"}]}],"serviceProvider":{"reference":"Organization/Hospital-B","display":"Pulmonology"}}'), ('E004', 'P004', '{"resourceType":"Encounter","id":"E004","status":"in-progress","class":{"code":"AMB","display":"ambulatory"},"subject":{"reference":"Patient/P004"},"period":{"start":"2026-03-15T14:00:00Z"},"reasonCode":[{"coding":[{"system":"http://hl7.org/fhir/sid/icd-10","code":"N18.3","display":"Chronic kidney disease stage 3"}]}],"serviceProvider":{"reference":"Organization/Hospital-A","display":"Nephrology"}}'), ('E005', 'P005', '{"resourceType":"Encounter","id":"E005","status":"finished","class":{"code":"EMER","display":"emergency"},"subject":{"reference":"Patient/P005"},"period":{"start":"2026-02-18T22:15:00Z","end":"2026-02-19T06:00:00Z"},"reasonCode":[{"coding":[{"system":"http://hl7.org/fhir/sid/icd-10","code":"I63.9","display":"Cerebral infarction"}]}],"serviceProvider":{"reference":"Organization/Hospital-B","display":"Neurology"}}');

写入 7 条检验结果(Observation resource,含 LOINC 码)和 6 条用药医嘱(MedicationRequest,含 RxNorm 码)后,验证 ODS 层行数:

SELECT COUNT(*) AS patient_count FROM best_practice_fhir_clinical.doc_fhir_patient; SELECT COUNT(*) AS encounter_count FROM best_practice_fhir_clinical.doc_fhir_encounter; SELECT COUNT(*) AS obs_count FROM best_practice_fhir_clinical.doc_fhir_observation; SELECT COUNT(*) AS med_count FROM best_practice_fhir_clinical.doc_fhir_medication_request;


DWD 层:FHIR JSON 解析与结构化

DWD 层通过 Dynamic Table 将 ODS 层的嵌套 JSON 字段提取为关系列,是后续聚合和质控分析的基础。

JSON 字段提取模式

FHIR JSON 结构嵌套较深,提取时遵循以下 JSONPath 模式:

FHIR 字段类型JSONPath 示例说明
顶层标量
$.gender
$.gender
直接取
数组第一个对象的属性
$.name[0].family
$.name[0].family
数组下标
[0]
[0]
多层嵌套数组
$.reasonCode[0].coding[0].code
$.reasonCode[0].coding[0].code
多级嵌套
嵌套对象的标量
$.period.start
$.period.start
点路径
数值(需类型转换)
CAST(get_json_object(...) AS DOUBLE)
CAST(get_json_object(...) AS DOUBLE)
默认返回字符串

验证 Patient resource 提取效果:

SELECT patient_id, get_json_object(resource_json, '$.name[0].family') AS family_name, get_json_object(resource_json, '$.name[0].given[0]') AS given_name, get_json_object(resource_json, '$.gender') AS gender, get_json_object(resource_json, '$.birthDate') AS birth_date, get_json_object(resource_json, '$.address[0].city') AS city FROM best_practice_fhir_clinical.doc_fhir_patient;

patient_id | family_name | given_name | gender | birth_date | city -----------+-------------+------------+--------+------------+----------- P001 | Zhang | Wei | male | 1980-05-15 | Shanghai P002 | Li | Fang | female | 1972-11-23 | Beijing P003 | Wang | Jun | male | 1955-03-08 | Guangzhou P004 | Chen | Mei | female | 1990-07-30 | Shenzhen P005 | Liu | Yang | male | 1968-01-12 | Chengdu

验证 Encounter resource 中 ICD-10 码和就诊时长提取:

SELECT encounter_id, patient_id, get_json_object(resource_json, '$.status') AS enc_status, get_json_object(resource_json, '$.class.code') AS enc_class, get_json_object(resource_json, '$.period.start') AS period_start, get_json_object(resource_json, '$.period.end') AS period_end, get_json_object(resource_json, '$.reasonCode[0].coding[0].code') AS icd_code, get_json_object(resource_json, '$.reasonCode[0].coding[0].display') AS diagnosis, get_json_object(resource_json, '$.serviceProvider.display') AS department FROM best_practice_fhir_clinical.doc_fhir_encounter;

encounter_id | patient_id | enc_status | enc_class | period_start | period_end | icd_code | diagnosis | department -------------+------------+-------------+-----------+-----------------------+-----------------------+----------+----------------------------------+----------- E001 | P001 | finished | IMP | 2026-01-10T08:00:00Z | 2026-01-15T14:00:00Z | I50.0 | Congestive heart failure | Cardiology E002 | P002 | finished | AMB | 2026-02-03T09:30:00Z | 2026-02-03T11:00:00Z | E11.9 | Type 2 diabetes mellitus | Endocrinology E003 | P003 | finished | IMP | 2026-01-20T07:00:00Z | 2026-01-28T16:00:00Z | J44.1 | COPD with acute exacerbation | Pulmonology E004 | P004 | in-progress | AMB | 2026-03-15T14:00:00Z | NULL | N18.3 | Chronic kidney disease stage 3 | Nephrology E005 | P005 | finished | EMER | 2026-02-18T22:15:00Z | 2026-02-19T06:00:00Z | I63.9 | Cerebral infarction | Neurology

建 DWD Dynamic Table

dwd_patient_dim
dwd_patient_dim
:患者维度表,提取基础人口学字段并计算年龄:

CREATE DYNAMIC TABLE IF NOT EXISTS best_practice_fhir_clinical.dwd_patient_dim AS SELECT p.patient_id, get_json_object(p.resource_json, '$.name[0].family') AS family_name, get_json_object(p.resource_json, '$.name[0].given[0]') AS given_name, get_json_object(p.resource_json, '$.gender') AS gender, get_json_object(p.resource_json, '$.birthDate') AS birth_date, get_json_object(p.resource_json, '$.address[0].city') AS city, DATEDIFF(CURRENT_DATE(), CAST(get_json_object(p.resource_json, '$.birthDate') AS DATE)) / 365 AS age_years FROM best_practice_fhir_clinical.doc_fhir_patient p;

dwd_encounter_fact
dwd_encounter_fact
:就诊事实表,提取 ICD-10 码、住院天数、科室,并对 ICD 章节分类:

CREATE DYNAMIC TABLE IF NOT EXISTS best_practice_fhir_clinical.dwd_encounter_fact AS SELECT e.encounter_id, e.patient_id, get_json_object(e.resource_json, '$.status') AS enc_status, get_json_object(e.resource_json, '$.class.code') AS enc_class, CAST(get_json_object(e.resource_json, '$.period.start') AS TIMESTAMP) AS admit_time, CAST(get_json_object(e.resource_json, '$.period.end') AS TIMESTAMP) AS discharge_time, DATEDIFF( CAST(get_json_object(e.resource_json, '$.period.end') AS TIMESTAMP), CAST(get_json_object(e.resource_json, '$.period.start') AS TIMESTAMP) ) AS los_days, get_json_object(e.resource_json, '$.reasonCode[0].coding[0].code') AS icd_code, get_json_object(e.resource_json, '$.reasonCode[0].coding[0].display') AS primary_diagnosis, get_json_object(e.resource_json, '$.serviceProvider.display') AS department, CASE WHEN UPPER(get_json_object(e.resource_json, '$.reasonCode[0].coding[0].code')) LIKE 'I%' THEN 'Cardiology' WHEN UPPER(get_json_object(e.resource_json, '$.reasonCode[0].coding[0].code')) LIKE 'E%' THEN 'Endocrinology' WHEN UPPER(get_json_object(e.resource_json, '$.reasonCode[0].coding[0].code')) LIKE 'J%' THEN 'Pulmonology' WHEN UPPER(get_json_object(e.resource_json, '$.reasonCode[0].coding[0].code')) LIKE 'N%' THEN 'Nephrology' ELSE 'Other' END AS icd_chapter FROM best_practice_fhir_clinical.doc_fhir_encounter e;

dwd_observation_fact
dwd_observation_fact
:检验/生命体征事实表,提取 LOINC 码、检验值和单位:

CREATE DYNAMIC TABLE IF NOT EXISTS best_practice_fhir_clinical.dwd_observation_fact AS SELECT o.obs_id, o.patient_id, get_json_object(o.resource_json, '$.category[0].coding[0].code') AS obs_category, get_json_object(o.resource_json, '$.code.coding[0].code') AS loinc_code, get_json_object(o.resource_json, '$.code.coding[0].display') AS obs_name, CAST(get_json_object(o.resource_json, '$.valueQuantity.value') AS DOUBLE) AS obs_value, get_json_object(o.resource_json, '$.valueQuantity.unit') AS obs_unit, CAST(get_json_object(o.resource_json, '$.effectiveDateTime') AS TIMESTAMP) AS obs_time FROM best_practice_fhir_clinical.doc_fhir_observation o;

dwd_medication_fact
dwd_medication_fact
:用药医嘱事实表,提取 RxNorm 码、药名、剂量和给药途径:

CREATE DYNAMIC TABLE IF NOT EXISTS best_practice_fhir_clinical.dwd_medication_fact AS SELECT m.req_id, m.patient_id, get_json_object(m.resource_json, '$.status') AS req_status, get_json_object(m.resource_json, '$.medicationCodeableConcept.coding[0].code') AS rxnorm_code, get_json_object(m.resource_json, '$.medicationCodeableConcept.coding[0].display') AS medication_name, CAST(get_json_object(m.resource_json, '$.dosageInstruction[0].doseAndRate[0].doseQuantity.value') AS DOUBLE) AS dose_value, get_json_object(m.resource_json, '$.dosageInstruction[0].doseAndRate[0].doseQuantity.unit') AS dose_unit, get_json_object(m.resource_json, '$.dosageInstruction[0].route.coding[0].code') AS route, CAST(get_json_object(m.resource_json, '$.authoredOn') AS TIMESTAMP) AS authored_on, REPLACE(get_json_object(m.resource_json, '$.encounter.reference'), 'Encounter/', '') AS encounter_id FROM best_practice_fhir_clinical.doc_fhir_medication_request m;

手动触发首次刷新,验证数据:

REFRESH DYNAMIC TABLE best_practice_fhir_clinical.dwd_patient_dim; REFRESH DYNAMIC TABLE best_practice_fhir_clinical.dwd_encounter_fact; REFRESH DYNAMIC TABLE best_practice_fhir_clinical.dwd_observation_fact; REFRESH DYNAMIC TABLE best_practice_fhir_clinical.dwd_medication_fact; SELECT patient_id, family_name, gender, birth_date, city, ROUND(age_years, 1) AS age FROM best_practice_fhir_clinical.dwd_patient_dim ORDER BY patient_id;

patient_id | family_name | gender | birth_date | city | age -----------+-------------+--------+------------+-----------+----- P001 | Zhang | male | 1980-05-15 | Shanghai | 46.1 P002 | Li | female | 1972-11-23 | Beijing | 53.6 P003 | Wang | male | 1955-03-08 | Guangzhou | 71.3 P004 | Chen | female | 1990-07-30 | Shenzhen | 35.9 P005 | Liu | male | 1968-01-12 | Chengdu | 58.4

就诊事实表验证(关注 los_days 计算和 icd_chapter 分类):

SELECT encounter_id, patient_id, enc_class, icd_code, primary_diagnosis, department, los_days, icd_chapter FROM best_practice_fhir_clinical.dwd_encounter_fact ORDER BY encounter_id;

encounter_id | patient_id | enc_class | icd_code | primary_diagnosis | department | los_days | icd_chapter -------------+------------+-----------+----------+---------------------------------+---------------+----------+------------ E001 | P001 | IMP | I50.0 | Congestive heart failure | Cardiology | 5 | Cardiology E002 | P002 | AMB | E11.9 | Type 2 diabetes mellitus | Endocrinology | 0 | Endocrinology E003 | P003 | IMP | J44.1 | COPD with acute exacerbation | Pulmonology | 9 | Pulmonology E004 | P004 | AMB | N18.3 | Chronic kidney disease stage 3 | Nephrology | NULL | Nephrology E005 | P005 | EMER | I63.9 | Cerebral infarction | Neurology | 0 | Cardiology

检验事实表(含 LOINC 码和量化值):

SELECT obs_id, patient_id, obs_category, loinc_code, obs_name, obs_value, obs_unit FROM best_practice_fhir_clinical.dwd_observation_fact ORDER BY obs_id;

obs_id | patient_id | obs_category | loinc_code | obs_name | obs_value | obs_unit -------+------------+--------------+------------+-------------------------+-----------+--------- OBS001 | P001 | laboratory | 2160-0 | Creatinine | 1.2 | mg/dL OBS002 | P002 | laboratory | 4548-4 | HbA1c | 8.5 | % OBS003 | P003 | vital-signs | 59408-5 | SpO2 | 88 | % OBS004 | P004 | laboratory | 2160-0 | Creatinine | 2.8 | mg/dL OBS005 | P005 | vital-signs | 8310-5 | Body temperature | 38.9 | Cel OBS006 | P001 | vital-signs | 8480-6 | Systolic blood pressure | 155 | mmHg OBS007 | P002 | laboratory | 2339-0 | Glucose | 12.4 | mmol/L


PHI 字段脱敏:Column Masking

FHIR Patient 数据包含患者姓名(family_name)等 PHI 字段,需对非特权用户脱敏。以下示例对

dwd_patient_dim
dwd_patient_dim
family_name
family_name
列绑定脱敏策略,管理员账号看原始值,其他用户看掩码值。

-- 创建脱敏函数:管理员看原始姓名,其他用户看掩码 CREATE OR REPLACE FUNCTION best_practice_fhir_clinical.mask_phi_name(name STRING) RETURNS STRING AS CASE WHEN current_user() IN ('privileged_user') THEN name -- 替换为实际获授权的用户名 ELSE CONCAT(LEFT(name, 1), REPEAT('*', LENGTH(name) - 1)) END;


DWS 层:科室费用聚合与患者检验汇总

科室费用聚合

dws_department_cost
dws_department_cost
以科室和 ICD 章节为维度,汇总就诊量、平均住院天数和用药数量,作为 DRG 费用管控的基础数据:

CREATE DYNAMIC TABLE IF NOT EXISTS best_practice_fhir_clinical.dws_department_cost AS SELECT e.department, e.icd_chapter, COUNT(DISTINCT e.encounter_id) AS total_encounters, COUNT(DISTINCT e.patient_id) AS total_patients, SUM(CASE WHEN e.enc_class = 'IMP' THEN 1 ELSE 0 END) AS inpatient_count, SUM(CASE WHEN e.enc_class = 'AMB' THEN 1 ELSE 0 END) AS outpatient_count, SUM(CASE WHEN e.enc_class = 'EMER' THEN 1 ELSE 0 END) AS emergency_count, ROUND(AVG(CAST(e.los_days AS DOUBLE)), 2) AS avg_los_days, MAX(e.los_days) AS max_los_days, COUNT(DISTINCT m.req_id) AS total_prescriptions, COUNT(DISTINCT m.rxnorm_code) AS distinct_medications FROM best_practice_fhir_clinical.dwd_encounter_fact e LEFT JOIN best_practice_fhir_clinical.dwd_medication_fact m ON e.encounter_id = m.encounter_id GROUP BY e.department, e.icd_chapter;

REFRESH DYNAMIC TABLE best_practice_fhir_clinical.dws_department_cost; SELECT department, icd_chapter, total_encounters, avg_los_days, total_prescriptions, distinct_medications FROM best_practice_fhir_clinical.dws_department_cost ORDER BY total_encounters DESC;

department | icd_chapter | total_encounters | avg_los_days | total_prescriptions | distinct_medications --------------+---------------+------------------+--------------+---------------------+--------------------- Cardiology | Cardiology | 1 | 5.0 | 2 | 2 Nephrology | Nephrology | 1 | NULL | 1 | 1 Pulmonology | Pulmonology | 1 | 9.0 | 1 | 1 Neurology | Cardiology | 1 | 0.0 | 1 | 1 Endocrinology | Endocrinology | 1 | 0.0 | 1 | 1

结果解读:Pulmonology 科室(J44.1 COPD 急性加重)平均住院天数 9 天是最高的,符合慢阻肺急性期住院时间长的临床规律。Cardiology 科室的心衰患者(I50.0)住院 5 天、开具了 2 种药物(利尿剂 Furosemide + 心衰用药 Carvedilol),与临床路径相符。

患者检验汇总

dws_patient_obs_summary
dws_patient_obs_summary
按患者和 LOINC 码维度聚合检验结果,方便追踪指标趋势:

CREATE DYNAMIC TABLE IF NOT EXISTS best_practice_fhir_clinical.dws_patient_obs_summary AS SELECT o.patient_id, o.loinc_code, o.obs_name, o.obs_unit, COUNT(*) AS obs_count, ROUND(MIN(o.obs_value), 2) AS min_value, ROUND(MAX(o.obs_value), 2) AS max_value, ROUND(AVG(o.obs_value), 2) AS avg_value, MIN(o.obs_time) AS first_obs_time, MAX(o.obs_time) AS last_obs_time FROM best_practice_fhir_clinical.dwd_observation_fact o GROUP BY o.patient_id, o.loinc_code, o.obs_name, o.obs_unit;

REFRESH DYNAMIC TABLE best_practice_fhir_clinical.dws_patient_obs_summary; SELECT patient_id, loinc_code, obs_name, obs_unit, obs_count, min_value, max_value, avg_value FROM best_practice_fhir_clinical.dws_patient_obs_summary ORDER BY patient_id, loinc_code;

patient_id | loinc_code | obs_name | obs_unit | obs_count | min_value | max_value | avg_value -----------+------------+-------------------------+----------+-----------+-----------+-----------+---------- P001 | 2160-0 | Creatinine | mg/dL | 1 | 1.2 | 1.2 | 1.2 P001 | 8480-6 | Systolic blood pressure | mmHg | 1 | 155 | 155 | 155 P002 | 2339-0 | Glucose | mmol/L | 1 | 12.4 | 12.4 | 12.4 P002 | 4548-4 | HbA1c | % | 1 | 8.5 | 8.5 | 8.5 P003 | 59408-5 | SpO2 | % | 1 | 88 | 88 | 88 P004 | 2160-0 | Creatinine | mg/dL | 1 | 2.8 | 2.8 | 2.8 P005 | 8310-5 | Body temperature | Cel | 1 | 38.9 | 38.9 | 38.9

结果解读:P002(糖尿病患者)HbA1c 8.5% 超过临床控制目标(< 7%),血糖 12.4 mmol/L 也明显升高,说明血糖控制不佳,需强化治疗。P004(CKD 3 期)肌酐 2.8 mg/dL 处于中重度升高区间(正常上限约 1.2),提示肾功能受损程度较重。P003(COPD)SpO2 88% 低于正常下限(95%),与急性加重期低氧血症一致。


ADS 层:临床质控指标

ads_clinical_quality_metrics
ads_clinical_quality_metrics
整合患者维度、就诊事实和检验事实,计算三类临床路径合规标志:

  • hba1c_tested
    hba1c_tested
    :糖尿病患者(ICD E 系列)是否完成了 HbA1c 检测
  • creatinine_tested
    creatinine_tested
    :肾病患者(ICD N 系列)是否完成了肌酐检测
  • spo2_monitored
    spo2_monitored
    :COPD 患者(ICD J 系列)是否监测了 SpO2

CREATE DYNAMIC TABLE IF NOT EXISTS best_practice_fhir_clinical.ads_clinical_quality_metrics AS SELECT p.patient_id, p.family_name, p.gender, FLOOR(p.age_years) AS age, e.encounter_id, e.department, e.icd_chapter, e.icd_code, e.primary_diagnosis, e.enc_class, e.los_days, e.enc_status, COUNT(DISTINCT m.req_id) AS prescription_count, COUNT(DISTINCT o.obs_id) AS lab_count, MAX(CASE WHEN o.loinc_code = '4548-4' THEN 1 ELSE 0 END) AS hba1c_tested, MAX(CASE WHEN o.loinc_code = '2160-0' THEN 1 ELSE 0 END) AS creatinine_tested, MAX(CASE WHEN o.loinc_code = '59408-5' THEN 1 ELSE 0 END) AS spo2_monitored, MAX(CASE WHEN o.loinc_code = '4548-4' THEN o.obs_value END) AS hba1c_value, MAX(CASE WHEN o.loinc_code = '2160-0' THEN o.obs_value END) AS creatinine_value, MAX(CASE WHEN o.loinc_code = '59408-5' THEN o.obs_value END) AS spo2_value FROM best_practice_fhir_clinical.dwd_patient_dim p JOIN best_practice_fhir_clinical.dwd_encounter_fact e ON p.patient_id = e.patient_id LEFT JOIN best_practice_fhir_clinical.dwd_medication_fact m ON e.encounter_id = m.encounter_id LEFT JOIN best_practice_fhir_clinical.dwd_observation_fact o ON p.patient_id = o.patient_id GROUP BY p.patient_id, p.family_name, p.gender, FLOOR(p.age_years), e.encounter_id, e.department, e.icd_chapter, e.icd_code, e.primary_diagnosis, e.enc_class, e.los_days, e.enc_status;

REFRESH DYNAMIC TABLE best_practice_fhir_clinical.ads_clinical_quality_metrics; SELECT patient_id, family_name, age, department, primary_diagnosis, enc_class, los_days, prescription_count, lab_count, hba1c_tested, creatinine_tested, spo2_monitored, hba1c_value, creatinine_value, spo2_value FROM best_practice_fhir_clinical.ads_clinical_quality_metrics ORDER BY patient_id;

patient_id | family_name | age | department | primary_diagnosis | enc_class | los_days | prescription_count | lab_count | hba1c_tested | creatinine_tested | spo2_monitored | hba1c_value | creatinine_value | spo2_value -----------+-------------+-----+---------------+----------------------------------+-----------+----------+--------------------+-----------+--------------+-------------------+----------------+-------------+------------------+----------- P001 | Zhang | 46 | Cardiology | Congestive heart failure | IMP | 5 | 2 | 2 | 0 | 1 | 0 | NULL | 1.2 | NULL P002 | Li | 53 | Endocrinology | Type 2 diabetes mellitus | AMB | 0 | 1 | 2 | 1 | 0 | 0 | 8.5 | NULL | NULL P003 | Wang | 71 | Pulmonology | COPD with acute exacerbation | IMP | 9 | 1 | 1 | 0 | 0 | 1 | NULL | NULL | 88 P004 | Chen | 35 | Nephrology | Chronic kidney disease stage 3 | AMB | NULL | 1 | 1 | 0 | 1 | 0 | NULL | 2.8 | NULL P005 | Liu | 58 | Neurology | Cerebral infarction | EMER | 0 | 1 | 1 | 0 | 0 | 0 | NULL | NULL | NULL

临床路径合规率统计

SELECT e.icd_chapter AS disease_group, COUNT(DISTINCT a.encounter_id) AS encounter_count, ROUND(AVG(CAST(a.los_days AS DOUBLE)), 1) AS avg_los, SUM(a.prescription_count) AS total_prescriptions, ROUND(SUM(a.hba1c_tested) * 100.0 / NULLIF(SUM(CASE WHEN a.icd_code LIKE 'E%' THEN 1 ELSE 0 END), 0), 1) AS diabetes_hba1c_rate_pct, ROUND(SUM(a.creatinine_tested) * 100.0 / NULLIF(SUM(CASE WHEN a.icd_code LIKE 'N%' THEN 1 ELSE 0 END), 0), 1) AS ckd_creatinine_rate_pct, ROUND(SUM(a.spo2_monitored) * 100.0 / NULLIF(SUM(CASE WHEN a.icd_code LIKE 'J%' THEN 1 ELSE 0 END), 0), 1) AS copd_spo2_rate_pct FROM best_practice_fhir_clinical.ads_clinical_quality_metrics a JOIN best_practice_fhir_clinical.dwd_encounter_fact e ON a.encounter_id = e.encounter_id GROUP BY e.icd_chapter ORDER BY encounter_count DESC;

disease_group | encounter_count | avg_los | total_prescriptions | diabetes_hba1c_rate_pct | ckd_creatinine_rate_pct | copd_spo2_rate_pct --------------+-----------------+---------+---------------------+-------------------------+-------------------------+------------------- Cardiology | 2 | 2.5 | 3 | NULL | NULL | NULL Nephrology | 1 | NULL | 1 | NULL | 100.0 | NULL Endocrinology | 1 | 0.0 | 1 | 100.0 | NULL | NULL Pulmonology | 1 | 9.0 | 1 | NULL | NULL | 100.0

结果解读:糖尿病(Endocrinology)、CKD(Nephrology)、COPD(Pulmonology)三个病种的关键检验合规率均为 100%,说明每位患者都完成了对应的必查项目。Cardiology 章节的 2 例就诊(心衰 + 脑梗)没有对应的特定质控标志检测,但心衰患者(P001)完成了肌酐检测(用于评估利尿剂用药后的肾功能)。


Time Travel:保险对账历史快照

在保险月度对账场景中,需要调取某一历史时间点的就诊数据快照。Time Travel 通过

TIMESTAMP AS OF
TIMESTAMP AS OF
语法实现:

-- 查询 2026-06-06 23:38 时刻的就诊数据快照(保险对账用) SELECT COUNT(*) AS encounter_count FROM best_practice_fhir_clinical.doc_fhir_encounter TIMESTAMP AS OF '2026-06-06 23:38:00';

encounter_count --------------- 5

-- 对比当前数据(验证差量) SELECT COUNT(*) AS current_count FROM best_practice_fhir_clinical.doc_fhir_encounter;

Time Travel 数据保留期默认 7 天,超出保留期的历史版本不可查询。对于月度保险对账场景,建议定期将月末快照

INSERT INTO
INSERT INTO
到专用归档表中,避免依赖 Time Travel 的时效性限制。


调度配置:Studio Task

Dynamic Table 的定期刷新通过 Studio Task 管理,不在 DDL 中写

REFRESH INTERVAL
REFRESH INTERVAL
。在 Studio 路径
best_practices/fhir_clinical/
best_practices/fhir_clinical/
下创建刷新任务,可在同一任务上附加监控告警和数据质量检查规则。

# 创建刷新任务 cz-cli task create refresh_fhir_dwd_all -p skill_test --type SQL \ --description "Refresh all FHIR DWD/DWS/ADS Dynamic Tables" # 设置任务内容(按依赖顺序刷新) cz-cli task save-content refresh_fhir_dwd_all -p skill_test --content \ "REFRESH DYNAMIC TABLE best_practice_fhir_clinical.dwd_patient_dim; REFRESH DYNAMIC TABLE best_practice_fhir_clinical.dwd_encounter_fact; REFRESH DYNAMIC TABLE best_practice_fhir_clinical.dwd_observation_fact; REFRESH DYNAMIC TABLE best_practice_fhir_clinical.dwd_medication_fact; REFRESH DYNAMIC TABLE best_practice_fhir_clinical.dws_department_cost; REFRESH DYNAMIC TABLE best_practice_fhir_clinical.dws_patient_obs_summary; REFRESH DYNAMIC TABLE best_practice_fhir_clinical.ads_clinical_quality_metrics;" # 设置每日凌晨 1 点调度 cz-cli task save-cron refresh_fhir_dwd_all -p skill_test --cron "0 1 * * *" # 发布任务(task_id=10354669) cz-cli task online refresh_fhir_dwd_all -p skill_test -y

任务发布后,Studio 界面(

best_practices/fhir_clinical/
best_practices/fhir_clinical/
)可追加:

  • 监控告警:ADS 层行数归零告警、刷新超时告警
  • 数据质控规则:糖尿病患者 HbA1c 合规率跌破 90% 触发告警
  • 任务依赖:将 DWS/ADS 任务链式依赖 DWD 任务,确保按顺序刷新

数仓对象总览

SHOW TABLES IN best_practice_fhir_clinical;

best_practice_fhir_clinical
best_practice_fhir_clinical
Schema 下共 9 张表:

表名层级类型说明
doc_fhir_patient
doc_fhir_patient
ODS普通表FHIR Patient resource JSON
doc_fhir_encounter
doc_fhir_encounter
ODS普通表FHIR Encounter resource JSON
doc_fhir_observation
doc_fhir_observation
ODS普通表FHIR Observation resource JSON
doc_fhir_medication_request
doc_fhir_medication_request
ODS普通表FHIR MedicationRequest JSON
dwd_patient_dim
dwd_patient_dim
DWDDynamic Table患者维度,JSON 解析 + 年龄计算
dwd_encounter_fact
dwd_encounter_fact
DWDDynamic Table就诊事实,ICD 码 + LOS + 科室
dwd_observation_fact
dwd_observation_fact
DWDDynamic Table检验事实,LOINC + 量化值
dwd_medication_fact
dwd_medication_fact
DWDDynamic Table用药事实,RxNorm + 剂量
dws_department_cost
dws_department_cost
DWSDynamic Table科室聚合,DRG 费用管控
dws_patient_obs_summary
dws_patient_obs_summary
DWSDynamic Table患者检验汇总,指标趋势
ads_clinical_quality_metrics
ads_clinical_quality_metrics
ADSDynamic Table临床质控指标,路径合规率

注意事项

  • get_json_object
    get_json_object
    返回字符串:所有提取结果默认为 STRING 类型。数值字段(检验值、剂量)必须显式
    CAST(... AS DOUBLE)
    CAST(... AS DOUBLE)
    ,否则聚合(AVG/MIN/MAX)结果可能异常。

  • FHIR 数组字段:FHIR 标准中大量字段是数组(如

    name[]
    name[]
    reasonCode[]
    reasonCode[]
    ),本文用
    [0]
    [0]
    取第一个元素。如果业务需要展开所有数组元素,需用
    LATERAL VIEW EXPLODE(SPLIT(json_array_str, ...))
    LATERAL VIEW EXPLODE(SPLIT(json_array_str, ...))
    处理。

  • Dynamic Table 增量刷新依赖:DWS/ADS 层 Dynamic Table 依赖 DWD 层,DWD 层依赖 ODS 层。刷新顺序须按 ODS → DWD → DWS → ADS,Studio Task 中按顺序排列 REFRESH 语句,或配置任务依赖链。

  • DATEDIFF
    DATEDIFF
    对 NULL 的处理:在院患者(
    enc_status = 'in-progress'
    enc_status = 'in-progress'
    )的
    discharge_time
    discharge_time
    为 NULL,
    DATEDIFF(NULL, admit_time)
    DATEDIFF(NULL, admit_time)
    返回 NULL。
    avg_los_days
    avg_los_days
    在 DWS 层用
    AVG(CAST(los_days AS DOUBLE))
    AVG(CAST(los_days AS DOUBLE))
    ,AVG 自动忽略 NULL,结果为已出院患者的平均值,符合业务语义。

  • Time Travel 时效性:默认保留期 7 天。保险对账等需要长期保留历史版本的场景,应定期将月末数据快照

    INSERT INTO
    INSERT INTO
    到专用归档表,不能完全依赖 Time Travel。

  • Column Masking:PHI 脱敏对 Dynamic Table 透明生效——DWD 层读取 ODS 层时如有绑定脱敏函数的列,DWD 中存储的也是脱敏后的值。


相关文档

联系我们
预约咨询
微信咨询
电话咨询
邮件咨询