ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

EMR数据库表结构设计:字典表、映射表与命名规范避坑

EMR数据库表结构设计:字典表、映射表与命名规范避坑

简介:《卫宁电子病历表结构》面向医院信息科、系统集成与医疗数据分析人员,聚焦卫宁EMR数据库表结构的查阅与理解。文档系统梳理800余张表的说明、字段定义、类型、长度、备注及值域,覆盖职工、科室、病区、药品、收费、医保、诊断等基础代码库与业务数据模型,可用于系统对接、报表开发、数据查询和接口字段核对。压缩包内为1个doc文件,约13.92MB,适合按目录检索并作为日常数据字典参考。已有1554人学习下载,说明其在医疗信息化从业者中有一定参考价值。通过厘清表间关系与编码规则,读者能更快定位字段含义、减少沟通成本,并为数据标准化、互操作及权限管理提供依据。

1. 字典表、业务表与映射表:800 张表里真正装的是什么

第一次翻电子病历表结构文档的人,几乎都会先去找「病历正文存在哪张表」,然后在 800 多张表里翻半天,最后发现真正承载病历内容的表可能连十分之一都不到。剩下的大头,全是字典,以及把字典两两对起来的映射表。

原因不难理解。临床信息系统 EMR 里最难缠的部分从来不是文本存储,而是编码体系:一条诊断要落到 ICD-10 上,一个收费项目要和医保目录对得上,一种剂型只允许配特定的医嘱频次与用法,一个科室要挂到某个药房下面。这些「谁和谁能配上」的规则,每一条都得有张表来承接。所以看这份数据库结构设计说明书的目录,前五十项几乎清一色是PUB_、SYS_开头:职工代码库、科室代码库、药品分类库、收费大项目库、医保分类库、职称编码库、诊断代码库。

这套设计真正解决的问题,是把「医疗业务规则的高频变化」和「数据库表结构的低频变化」隔开。规则变了改数据,结构尽量不动。值不值得搞懂,取决于你信不信一句话——医院的流程天天在变,但变的方式是稳定的。

2. 命名即契约:前缀和后缀在说什么

一套 800 多张表的 EMR 库,如果表名不带约定,三个月后连原厂的人都读不懂。这份文档最值钱的隐性资产,其实是命名规则本身。它用前缀划边界、用后缀标角色,两层叠加之后,光看表名就能猜出这张表在系统中扮演什么角色、大概长什么样。

2.1 四个前缀划出的系统边界

SYS_是系统级基础,职工代码库、科室代码库、病区代码库都在这里。它存储的是「全院唯一」的组织实体,任何模块引用科室,最终都指回SYS_KSDMK。

PUB_是公共字典,覆盖医疗项目、药品分类、凭证类型、收费大项目、医保分类、药房代码、职称编码、诊断代码、地区/区县/国家代码,以及 ICD9、ICD10。这一层的特点是跨模块复用:门诊要用、住院要用、医生站要用、护士站也要用。文档里把 ICD9 与 ICD10 并排放成两张独立表(PUB_ICD9、PUB_ICD10),而不是一张表加版本字段,这个取舍后面还会说到。

CPOE_是医生站侧,医嘱、申请单、手术、模板、打印、排班、临床路径相关的东西基本都在这个前缀下。CPOE 是 Computerized Physician Order Entry 的缩写,直译是「计算机化医嘱录入」,但这份文档里的 CPOE 前缀覆盖面明显更宽,连血库审核流程、小处方、术后随访都挂在这里,说明它在实际实现里代表的是「医生工作站的整套业务域」,而不只是医嘱录入。

OUTP_是门诊侧,里面有门诊医嘱频次、门诊医嘱用法、皮试药品对应、导入院感诊断。这里有个值得注意的信号:CPOE_YZYPPCK是住院医嘱药品频次,OUTP_MZYZPCK是门诊医嘱频次,两条平行车道。同样叫「频次」,门诊和住院各存一份,而不是统一到一张全局字典里。

这不是设计失误,而是被业务节奏逼出来的。门诊频次是「每日三次、隔日一次」这种以天为单位的粗粒度;住院频次则要精确到「q8h」「q12h」这种以小时为单位的执行节拍,还要和护士执行单绑定。强行合并成一张表,字段会膨胀成一张谁都看不懂的宽表,查询时还得处处带就诊类型过滤条件。分开存虽然冗余,但每张表都干净。

CPOE_RIS_这种二级前缀也值得留意。RIS 是放射信息系统,检查申请单相关的一组表都挂在它下面:主表、明细、附属信息、模板库、数据分类库。二级前缀的作用是把一个子系统圈起来,避免它污染整个 CPOE 命名空间。做数据库分层的时候,这个手法可以直接借鉴。

2.2 后缀才是信息密度最高的部分

前缀回答「这张表属于谁」,后缀回答「这张表怎么用」。六个常见后缀值得单独拎出来:

后缀含义典型例子判断依据
K字典库,存编码与名称PUB_YPFLK药品分类库有*DM主键和*MC名称
MXK明细库,主表的下级行项目CPOE_RIS_BRJCSQDMX检查申请单明细有指向主表的*ID
DYK对应库,两张表之间的多对多映射PUB_KSYFDYK科室药房对应至少两个*DM维度字段
SZ设置,参数、开关、联动规则CPOE_XCF_SZ小处方设置无业务主键,多为一行为一条规则
MB/MBK模板CPOE_SSJLMB手术记录模板带模板名称与启用标志
BFK备份CPOE_ZXZTDSQBFK中心制团队申请备份库结构与主表同构

这套后缀约定至少在两个地方救过命。一是做数据迁移时,*DYK结尾的表可以先跳过,它们都是纯映射关系,重置成本极低;二是排查线上问题时,如果怀疑某条医嘱的频次不对,视线要落到*PCK和*PCYFDYK上,而不是去翻病历正文表。

但约定只是约定,它依赖每个开发者自觉遵守,没有任何机制强制。写个脚本把表名扫一遍、推断角色,是读这套文档最快的切入点:

import re # 后缀必须按长度降序匹配: # 如果 'K' 排在 'DYK' 前面,所有对应表都会被误判成字典表 SUFFIX_RULES = [ ("DYK", "mapping"), # 对应库:两个维度以上的多对多关系 ("MXK", "detail"), # 明细库:主表的行项目 ("MBK", "template"), # 模板库 ("BFK", "backup"), # 备份库 ("_SZ", "setting"), # 设置项:参数、开关、联动规则 ("MB", "template"), # 无 K 的模板,如 CPOE_SSJLMB ("K", "dict"), # 字典库 ] def classify(table_name: str) -> str: upper = table_name.upper() for suffix, role in SUFFIX_RULES: if upper.endswith(suffix): return role return "biz" # 无后缀约定的纯业务流水表 for t in ["PUB_KSYFDYK", "CPOE_RIS_BRJCSQDMX", "CPOE_XCF_SZ", "CPOE_SSJLMB", "PUB_YPFLK"]: print(f"{t:24s} -> {classify(t)}")

这段逻辑只有三十来行,但它把「匹配顺序就是正确性」这件事讲透了。DYK天然以K结尾,MXK也一样,顺序一错,整批表的角色判定全崩,后面所有依赖这个分类做的迁移脚本、脱敏脚本、索引建议都会跟着跑偏。这类问题不靠测试很难发现,因为脚本本身不会报错,它只是安静地给你一份错的答案。

另一个坑是格式不统一。CPOE_XCF_SZ用的是下划线后缀,CPOE_SSTSGR干脆没有后缀,CPOE_YZDYSZ_EX还带了个_EX。这说明命名约定在长期迭代中已经被侵蚀了一部分。真正写解析逻辑时,规则表得留一个兜底分支,把认不出来的表登记出来人工过一遍,而不是硬塞进某个分类。

3. 映射表为什么占了大半:多对多关系的隔离带

翻目录时最容易忽略、也最容易在维护时踩坑的,是那一大批*DYK结尾的表:科室药房对应、门诊收费执行科室对应、住院收费执行科室对应、科室病区对应、剂型用法对应、医嘱频次用法对应、医保项目对应、医保药品对应、诊断药品对应、职务药品对应。数量上看,它们在这份文档里占据了相当可观的篇幅,而每一张都可能直接关系到线上业务能不能正常跑通。

3.1 用对应表代替外键的真实动机

如果按教科书的做法,科室和药房的关系应该在科室表里放一个药房代码外键。这套结构没有这么做,它把关系抽成独立的行,放进PUB_KSYFDYK。

动机是组织结构的不稳定性。医院里科室合并、药房拆分、收费执行科室调整、医保目录年度切换,这些事件的发生频率远高于「数据库表结构变更」的容忍度。关系抽成数据行之后,一次调整就是一次INSERT加一次UPDATE,不动 DDL,不用停业务,也不用担心历史数据被牵连。

更关键的是,对应表一般都带有效标志和生效时间,而不是直接把旧行删掉。这一点对医嘱系统尤其重要:三年前开的一条医嘱,回看时得按「当时」的对应关系还原出它到底执行到哪个科室、走了哪个药房。如果只保留当前最新的对应关系,历史医嘱的责任归属就彻底丢了。

常见做法是在对应表上加三列:YXBZ(有效标志)、QSRQ(生效日期)、JSRQ(结束日期),查询时用QSRQ <= 业务日期 AND (JSRQ IS NULL OR JSRQ >= 业务日期)来定位。代价是每次关联都要多一个时间条件,索引要建在(维度A, 维度B, QSRQ)这种组合上,否则历史区间的过滤会退化成全表扫。

3.2 一条医嘱落下去,到底碰了几张表

拿一条住院西药医嘱举例,药品本身的属性在PUB_YPLXK,剂型在PUB_YPJXK,频次和用法分别走CPOE_YZYPPCK与CPOE_YZYPYFK,但这两个不是自由填写的——它们要经过CPOE_YZPCYFDYK和CPOE_YZYPYFDYK的合法性校验。也就是说,一种剂型允许哪些频次、哪些用法,是配置出来的,不是硬编码在代码里的。

-- 还原一条住院西药医嘱的字典侧完整依赖链 -- 目的:确认频次/用法是否在该剂型的合法组合内,以及医保报销比例 SELECT yz.YZID AS 医嘱号, yz.YZMC AS 医嘱内容, yplx.YPMC AS 药品名称, jx.JXMC AS 剂型名称, pc.PCMC AS 频次, yf.YFMC AS 用法, dyk_pc.YXBZ AS 频次对该剂型可用, dyk_yf.YXBZ AS 用法对该剂型可用, yb.YBBZ AS 医保标志, bl.BL AS 报销比例 FROM CPOE_YZK yz LEFT JOIN PUB_YPLXK yplx ON yz.YPDM = yplx.YPDM LEFT JOIN PUB_YPJXK jx ON yplx.JXDM = jx.JXDM LEFT JOIN CPOE_YZYPPCK pc ON yz.YZID = pc.YZID LEFT JOIN CPOE_YZYPYFK yf ON yz.YZID = yf.YZID LEFT JOIN CPOE_YZPCYFDYK dyk_pc ON pc.PCDM = dyk_pc.PCDM AND yplx.JXDM = dyk_pc.JXDM LEFT JOIN CPOE_YZYPYFDYK dyk_yf ON yf.YFDM = dyk_yf.YFDM AND yplx.JXDM = dyk_yf.JXDM LEFT JOIN CPOE_YBYPDYK yb ON yz.YPDM = yb.YPDM LEFT JOIN CPOE_YBYPBXBLDYK bl ON yb.YBPZDM = bl.YBPZDM;

这几张表里,JXDM是最不显眼、作用却最大的一个字段。它在频次对应和用法对应里充当维度,把「剂型」变成了一条隐形的约束边界。口服液的频次选项和注射剂的频次选项本来就该不一样,这个差异不是写死在界面上的,而是从CPOE_YZPCYFDYK里查出来的。

代价是查询深度。单是一条医嘱的完整展示,就要七到八次关联。真实系统不会每次都这么查,它会在医嘱主表里冗余一份频次名称和用法名称的文本快照。字典改了,历史医嘱显示不变;这是有意为之,并非偷懒。快照字段的存在,等于给每条医嘱留了一颗后悔药。

这也解释了一个现象:为什么*DYK表数量这么多,但真正参与高频查询的只有那么几张。大部分对应表只在开单那一刻被读一次,用来生成候选列表;之后就被快照替代了。理解这一点,才不会误以为所有 DYK 都需要建重量级索引。

4. 从医嘱到执行:主表、明细、设置这套组合怎么配合

如果说字典层解决的是「数据从哪来」,那 CPOE 业务层解决的就是「一次临床动作怎么被完整记录下来」。这套文档里反复出现一种三段式结构:主表管生命周期、明细表管行项目、设置表管规则。看懂了这个模式,剩下几百张表基本都是它的变体。

4.1 申请单的三段式:主表、明细与附属信息

以检查申请单为例,CPOE_RIS_BRJCSQD存一次开单行为本身——开单医生、开单时间、申请科室、执行科室、单据状态;CPOE_RIS_BRJCSQDMX存具体检查项目,一次可以开多个,所以是一对多;CPOE_RIS_BRJCSQDFSXXK存附属信息,比如临床诊断、症状描述、结构化字段这些。

把附属信息单独拆出来,是这套结构里比较讲究的一手。主表的字段集合相对稳定,几年下来就那么几十个;但附属信息的结构随检查类型变化——CT 要问造影剂过敏史,超声要问空腹状态,内镜要问既往病史。如果全塞进主表,主表会被撑成一张几百列、处处可空的宽表,任何一次SELECT *都是灾难。

拆成附属信息表之后,它变成了一个纵向扩展的结构,检查类型增加只需要增加行,不需要增加列。代价是读一次申请单要做两次查询,但在申请单这种低频操作上,这个代价完全可以接受。

模板库CPOE_RIS_SQDMBK和文字模板库CPOE_RIS_SQDWZMBK的存在,进一步说明这套设计对「开单效率」的重视。模板不是给医生看的参考文档,它是可复用的行集:选中一个模板,等于一次性往明细表里插好几行。这是把医生的重复劳动压缩成一次点击。

4.2 小处方与血库审核流程:同构模式在别处的体现

小处方用的是完全相同的三段式:CPOE_XCFK是处方头,CPOE_XCFMXK是处方明细,CPOE_XCF_SZ是设置。设置表在这里承担的是「某类处方默认包含哪些药品、默认用法用量是什么」的规则存储。

血库审核那组表则更值得单独看,因为它其实是套微缩的工作流引擎:

表名角色存什么
CPOE_XKSHBZK步骤库定义审核有哪些步骤、步骤顺序、是否终态
CPOE_XKSHJSK角色库定义每个步骤允许哪些角色操作
CPOE_XKSHLCK流程库定义某类业务(如全血、成分血)走哪条步骤链
CPOE_XKSHMXK明细库记录实际发生的每一步审核动作

运行时序大致是这样:开单时按输血类型去CPOE_XKSHLCK拿到流程编号,再顺着流程编号去CPOE_XKSHBZK取出这条流程的步骤序列;用户打开待办列表时,用当前用户的角色去CPOE_XKSHJSK过滤出他可执行的步骤;审核完成时往CPOE_XKSHMXK写一条记录,带上步骤号、操作人、时间;最后判断当前步骤是否为终态,决定单据能不能发血。

这套结构的价值在于「换流程不改代码」。某个医院想把「主治审核后再由血库复核」改成「血库直接复核」,改的是CPOE_XKSHLCK里的几行数据,不是重新发版。输血本身是高风险操作,能不动物流逻辑只调配置,对实施团队来说省了很多事。

顺带一提,输血相关的表不止这四个,还有CPOE_SXBLFYK输血不良反应库和CPOE_SXZLTYS输血治疗同意书。前者说明系统把「事后」也纳入了数据模型——不良反应是独立实体,不是主记录上的一个标志位,这样才能支持一条输血记录对应多种不良反应。

4.3 手术域:字典先行,业务后置

手术相关的表几乎把「字典先行」这个思路演示了一遍。手术等级、切口等级、手术诊断、手术部位、手术麻醉、手术类别、手术科室,七张字典表打底,然后才是CPOE_SSYZK手术医嘱库和CPOE_SSYZK_SZ手术医嘱设置。

VVZYK_SZ这个后缀在这里的语义很清楚:它存的是「某类手术默认要下哪些医嘱」的规则。开一台胆囊切除术,系统自动带出术前抗生素、术后镇痛、留置引流管观察这几条医嘱,医生逐条确认或删掉。规则从代码里搬到表里,术式新增时实施人员自己就能配,不用排开发资源。

CPOE_ZXZTDK中心制团队库和它的备份库CPOE_ZXZTDSQBFK则是另一种模式:主表和备份表同构,备份表承担的是「申请变更时先存一份原样」的职责。这是在没有完整审计日志体系时常用的补偿手段,简单、直接、不依赖任何中间件。它的短板也很明显——只能存一个版本,第二次变更就覆盖了第一次的备份。要真正追溯,还是得上独立的变更流水表。

5. 避坑清单:拿到这套表结构后最容易翻车的五处

文档读得再顺,落地时照样会踩坑。下面这几条是从实际对接中攒下来的,按现象、原因、解决三段写,每条都能对上号。

5.1 文档层面的坑:同名表、重复条目与命名不统一

现象:按文档建表时,CPOE_TEXTORDER出现了两次——一次在系统框架章节(1.36 文字医嘱),一次在医生工作站章节(2.24 文字医嘱库)。照单全收会直接撞名,建表脚本在第二处报错退出。

原因:这份文档是按模块分区组织的,每个模块把自己用到的表都列一遍,模块之间没有做全局去重。CPOE_PSYPDYK皮试药品对应库也是同样情况,在OUTP_和CPOE_两个章节里都出现过。这反映的是文档组织方式,不是数据库里真有两张同名表。

解决:建表前先做一次全量去重,按表名分组,同名表只保留一份 DDL,其余位置只写引用注释。去重时以业务归属为准——CPOE_TEXTORDER归医生站,OUTP_PSYPDYK归门诊,CPOE_PSYPDYK归住院,两边字段如果有差异要人工比对,不能想当然认为完全一致。

5.2 建表层面的坑:主键、类型与值域三件事都没给全

现象:文档给的是表说明、字段名、字段类型、字段长度、备注和值域,看起来齐了。但照着建出来的表,一张主键都没有,也没有索引定义。上线跑两周,一条关联查询要几秒。

原因:结构设计说明书描述的是逻辑模型,不是物理模型。主键、索引、约束这些属于物理实现,文档里天然缺失。另外值域那一列很多时候写的是中文描述(比如「0 表示未审核,1 表示已审核」),不是可执行的枚举,直接拿去生成CHECK约束会失败。

解决:主键靠命名推断加人工确认两步走——*DM、*ID、*BH结尾的字段优先怀疑是主键,*MXK结尾的表还要再看有没有联合主键(通常是主表 ID 加行号)。索引先按外键列和日期列建,别一上来就建一堆复合索引。值域那一列要反查对应的字典表,PUB_*K里基本都能找到,找不到的才考虑用注释里的描述生成枚举,并且要在代码里注明来源。

5.3 运行期的坑:字典变更把历史数据带偏

现象:某科室调整后,三个月前开的历史医嘱在报表里全部归到了新科室名下,财务对账对不上。

原因:*DYK对应表如果是直接UPDATE覆盖,而不是插入新行并结束旧行,历史业务日期就失去了当时的映射关系。再加上医嘱主表里的科室字段如果存的是代码而不是快照文本,回看时必然跟着字典一起漂移。

解决:对应表一律用「新增行 + 结束日期」的方式维护,禁止原地改主键维度。查询侧统一带业务日期条件。医嘱、申请单这类核心业务表,科室、药品、频次这些关键属性除存代码外,再冗余一份名称快照,历史展示只读快照不读字典。

5.4 口径层面的坑:门诊与住院的平行表被当成一张用

现象:做统一报表时把CPOE_YZYPPCK和OUTP_MZYZPCK用UNION ALL合并,结果频次去重后数量对不上,同一个频次名称在两边的主键不一样。

原因:这两张表本来就是两条独立车道,主键生成规则不同,编码体系也没有强制对齐。名字撞了不代表语义一致。

解决:跨门诊住院的统计,一律先建立一张内部对照表,把两边的频次、用法、科室代码手工归一,再做聚合。别指望UNION能自动对齐。

5.5 扩展层面的坑:把设置表当业务表改

现象:CPOE_SSYZK_SZ手术医嘱设置被业务人员直接在数据库里改了几行,第二天开单时模板全乱了,而且没人记得改之前是什么。

原因:_SZ后缀意味着规则存储。它没有版本、没有审计、没有回滚,改错了只能靠记忆复原。而它影响的却是所有新开的单据。

解决:所有_SZ结尾的表在权限上单独收口,只允许通过配置界面修改,禁止直连写。上线前做一次全量导出做基线,每次变更自动留一份带时间戳的备份。这类表数量不多,但每张都能直接决定业务能不能跑。

6. 设计取舍:为什么它宁愿宽表加编码,也不做强约束

把这套结构从头看到尾,会发现一个很一致的选择:数据库层面几乎见不到外键约束,几乎所有关系都靠应用层加对应表来维持。这不是技术能力问题,是刻意的取舍。

强约束换来的是一致性,代价是变更成本。医院的字典不是静态的,医保目录一年一调,科室随组织架构变动,收费项目按物价文件更新。如果医嘱表上挂了外键指向药品字典,字典里一条记录想停用都得先处理完所有引用它的历史医嘱,要么级联、要么置空,两种做法都会污染历史数据。把约束上移到应用层,数据库层面就获得了随时增加新维度的自由——代价是每个写入入口都得自己记得校验,漏一处就产生一条脏数据。

第二层取舍在查询性能上。七到八张表的关联在开单那一刻可以接受,在报表和列表页就不能接受。所以它选择在业务表里冗余快照字段,用空间换响应时间,同时顺带解决了历史数据漂移的问题。这算是一份投入拿了两份收益。

第三层取舍在扩展性上。_SZ设置表和*DYK对应表把「规则」从代码里搬到了数据里,实施人员能自己配,不用等发版。代价是规则的执行逻辑散落在各处,没有统一入口,排查问题时得一层层往下找。

判断这套结构合不合理,有个很实在的验证方法:把所有*DYK结尾的表列出来,统计每张表的维度字段数量。如果某张表出现了三个以上的*DM字段,它其实已经在扮演规则引擎的角色,而不是简单的两两映射。

-- 统计对应表的维度字段数,超过 3 个的要重点看 SELECT TABLE_NAME, COUNT(*) AS 维度字段数, GROUP_CONCAT(COLUMN_NAME) AS 维度字段 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME LIKE '%DYK' AND COLUMN_NAME LIKE '%DM' GROUP BY TABLE_NAME HAVING COUNT(*) > 3 ORDER BY 维度字段数 DESC;

跑出来的结果基本就是这套系统里最复杂的几处业务规则所在。拿到这几张表,再去对照文档里的备注,比通读 800 张表效率高得多。

这套结构的适用边界也很清楚:它适合业务规则变化快、历史数据必须可回溯、实施团队有能力做配置的场景;不适合对强一致性要求极高、写入路径单一且稳定的系统。看清楚这个边界,才不会把「宽表加编码」这套手法无差别搬到自己项目里。

我第一次接手这类系统时,吃过一个亏:以为文档写的就是数据库真实结构,直接照建,结果主键、索引全靠猜,第一次压测就翻车。从那以后,我拿到任何一份表结构文档,都先跑三件事——全量去重、后缀分类、维度字段统计,三份结果摆在面前,再动手写第一行 DDL。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进