2. 医疗数据模型与表结构
医疗数据查询与分析的第一道门槛,不是 SQL 语法,而是“数据到底长什么样”。同一家医院里,患者信息、就诊记录、诊断结论、医嘱明细、检验结果、影像报告和费用流水往往分散在不同业务系统中,各自有独立的表结构和主键体系。如果直接凭字段名猜关系,很容易把“一次就诊”和“一个患者”混为一谈,导致统计口径错误。本节围绕核心实体及其关系建模展开,目标是建立一张可落地的“医疗数据地图”,为后续查询、聚合和指标计算提供稳定基础。
一、核心实体与主键设计
医疗数据模型通常以患者(Patient)为最上层实体,以就诊(Encounter/Visit)为业务主线,向下挂接诊断、医嘱、检验、影像和费用。每个实体必须有唯一标识,且标识的粒度决定了后续关联的准确性。
- 患者表(patient):主键为
patient_id,存储姓名、性别、出生日期、身份证号、联系方式等相对稳定的属性。一个患者可以多次来院,因此患者与就诊是一对多关系。 - 就诊表(encounter):主键为
encounter_id,外键patient_id,记录就诊类型(门诊/急诊/住院)、科室、接诊医生、入院时间、出院时间、就诊状态。一次就诊是诊断、医嘱、检验、影像和费用的共同归属单元。 - 诊断表(diagnosis):主键
diagnosis_id,外键encounter_id,字段包括诊断编码(ICD-10)、诊断名称、诊断类型(主诊断/次诊断)、诊断时间。一次就诊可有多条诊断,主诊断通常唯一。 - 医嘱表(order):主键
order_id,外键encounter_id,记录医嘱类型(药品/检查/检验/护理)、项目编码、项目名称、开立时间、执行状态、剂量与频次。 - 检验表(lab_result):主键
lab_id,外键encounter_id,字段包括检验项目、结果值、单位、参考范围、异常标志、报告时间。 - 影像表(imaging):主键
imaging_id,外键encounter_id,记录检查部位、检查方法、影像号、报告结论、报告医生。 - 费用表(charge):主键
charge_id,外键encounter_id,记录收费项目、数量、单价、金额、费用类别、结算时间。
这里的关键判断是:诊断、医嘱、检验、影像、费用都直接挂在就诊上,而不是直接挂在患者上。因为一次就诊才对应一次完整的诊疗过程,患者可能在不同时间、不同科室产生多组数据。若把费用直接关联到患者,跨就诊汇总时就会丢失“哪一次就诊花了多少钱”的语义。
二、实体关系与关联路径
从查询视角看,最常用的关联路径是:patient → encounter → 各业务明细表。患者表与就诊表通过 patient_id 关联;就诊表与诊断、医嘱、检验、影像、费用表通过 encounter_id 关联。这种“星型”结构让一次就诊成为分析的基本单元,也便于按患者、科室、时间等维度聚合。
需要特别注意的是一对多关系带来的行数膨胀。例如一次就诊有 3 条诊断、5 条医嘱、10 条检验结果,若直接多表连接后统计费用,费用金额会被重复计算。正确做法是先按业务粒度聚合,再与费用表关联,或使用子查询分别汇总。下面这张关系图展示了核心实体、主键外键以及连接方向,可作为建表和写查询时的参照。
三、建模中的常见陷阱与处理
第一,患者与就诊混淆。统计“糖尿病患者人数”时,应按 patient_id 去重;统计“糖尿病就诊人次”时,应按 encounter_id 计数。两者口径不同,结果可能相差数倍。
第二,主诊断与次诊断混用。诊断表中一次就诊可能有多条记录,若直接按诊断编码分组统计,会把次诊断也计入。通常需要加 diagnosis_type = '主诊断' 条件,或明确说明统计口径。
第三,时间字段语义不清。就诊表有入院时间、出院时间,医嘱表有开立时间、执行时间,检验表有采样时间、报告时间。做时间趋势分析时,必须明确使用哪个时间字段,并在查询中统一口径。
第四,空值与状态过滤。就诊状态可能包含“已取消”“作废”等,费用表可能包含退费记录。建模时应明确哪些状态纳入统计,避免把无效数据计入指标。
四、一个可执行的关联查询示例
假设要统计某科室 2024 年 1 月每位患者的主诊断分布,并附带该次就诊的总费用。可以先按就诊聚合费用,再与诊断、患者关联,避免多表连接导致金额重复。示例 SQL 如下(表名与字段为教学假设):
SELECT
p.patient_id,
p.name,
d.diagnosis_name,
e.encounter_id,
COALESCE(c.total_amount, 0) AS total_amount
FROM encounter e
JOIN patient p ON p.patient_id = e.patient_id
JOIN diagnosis d ON d.encounter_id = e.encounter_id
AND d.diagnosis_type = '主诊断'
LEFT JOIN (
SELECT encounter_id, SUM(amount) AS total_amount
FROM charge
WHERE status = '已结算'
GROUP BY encounter_id
) c ON c.encounter_id = e.encounter_id
WHERE e.dept_name = '内分泌科'
AND e.visit_time >= '2024-01-01'
AND e.visit_time < '2024-02-01'
AND e.status != '已取消'
ORDER BY p.patient_id, e.encounter_id;
这段查询体现了三个建模要点:主诊断过滤、费用子查询预聚合、就诊状态过滤。执行后每一行代表“一位患者的一次就诊及其主诊断和总费用”,粒度清晰,可直接用于后续分组统计。若把费用表直接 JOIN 进来而不做子查询,当一次就诊有多条费用记录时,诊断行会被放大,总费用也会重复累加。