简介:面向医院信息科技术人员、医疗信息化开发者与实施顾问的卫宁电子病历系统数据结构参考文档,聚焦EMR底层表结构梳理与数据交换规范化问题。文档系统整理了800余张数据表,逐表给出说明、字段定义、字段类型、长度及备注与值域,覆盖系统框架、财务收费、医疗信息等模块,如职工代码库、医疗项目库、药品分类库、科室与病区代码库、收费大项目库、医保分类库、诊断代码库等,并体现ICD编码、权限管理与数据加密等标准化与安全设计思路。压缩包内为1个doc文件,约13.92MB,便于按目录检索查阅。已有1551人学习下载,适合用于系统优化、接口对接与医疗大数据分析时对照字段口径、核对编码规则,快速建立对卫宁EMR数据模型的整体认识。
1. 拿到一份 800 张表的 EMR 字典,先别急着建库
上周有个做医院集成的朋友甩过来一个压缩包,里面是一份《临床信息系统数据库结构设计说明书》,版本 5.0,落款日期 2014 年。他问我一句话:这玩意儿现在还能用吗?我打开翻了十分钟,心里大概有数了——这不是一份“教程”,而是一份表结构字典,覆盖系统框架、医生工作站、护理、收费、医保、手术、输血、新生儿首页等模块,光目录里列出来的表就超过 800 张,每张表带字段名、类型、长度、备注和值域说明。对做 EMR 对接、老系统迁移、数据治理的人来说,这种字典的价值不在“新”,而在“全”和“准”:它把一家医院临床信息系统里那些命名古怪、含义靠猜的字段,一次性摊开给你看。适合谁?做 HIS/EMR 接口的、接手老库要写数据字典的、做临床数据仓库抽取的。不适合谁?想找现成建表脚本一键跑起来的——这份文档给的是设计说明,不是 DDL。
2. 先读懂命名法:表名前缀就是一张模块地图
拿到这种老牌临床信息系统的字典,最忌讳的操作是直接按拼音首字母去猜表。它的表名是有严格前缀体系的,前缀本身就是模块边界。先把前缀吃透,后面查表效率能差出好几倍。
2.1 四类前缀:SYS / PUB / CPOE / OUTP 各管一摊
从目录结构能清楚看出四层划分。SYS_开头的是系统级基础代码库,比如SYS_ZGDMK职工代码库、SYS_KSDMK科室代码库、SYS_BQDMK病区代码库,这类表数据量小、变更少,是全系统共享的字典。PUB_开头的是公共业务字典,覆盖面最广:药品分类PUB_YPFLK、收费大项目PUB_SFDXMK、医保分类PUB_YBFLK、诊断代码PUB_ZDDMK、ICD9/ICD10 代码PUB_ICD9/PUB_ICD10、手术相关的切口等级、麻醉方式、手术部位等一整串。CPOE_开头的是医生工作站(Computerized Physician Order Entry)业务表,医嘱、申请单、模板、评估、随访都在这里。OUTP_开头的是门诊侧,比如门诊医嘱频次OUTP_MZYZPCK、门诊医嘱用法OUTP_MZYZYFK、皮试药品对应OUTP_PSYPDYK。
这里有个容易翻车的点:同一类字典在PUB_和CPOE_下会重复出现。比如医嘱频次,既有CPOE_YZYPPCK医嘱频次库,又有OUTP_MZYZPCK门诊医嘱频次;医嘱用法既有CPOE_YZYPYFK,又有OUTP_MZYZYFK。这不是设计冗余,而是门诊和住院两套业务流程各自维护自己的字典,字段可能不完全一致。做数据抽取时如果只认一张表,门诊数据就会漏。
2.2 后缀 K / MXK / DYK 的含义与查询策略
再看后缀,这套命名法里后缀承载了“这张表是什么性质”的信息。K结尾基本是“库”,即主字典表,一条记录一个实体,比如PUB_YFDMK药房代码库。MXK结尾是“明细库”,挂在某张主表下面,一对多关系,比如CPOE_RIS_BRJCSQDMX检查申请单明细、CPOE_XCFMXK小处方明细、CPOE_ZDFLMXK诊断分类明细库。DYK结尾是“对应库”,专门存两张表之间的映射关系,这是老系统里最容易被忽视、也最容易在迁移时丢数据的一类表:PUB_KSYFDYK科室药房对应、CPOE_YBXMDYK医保项目对应、CPOE_YZPCYFDYK医嘱频次用法对应、CPOE_YPYFDYK药品用法对应。
对应库的存在意味着业务规则被“数据化”了——某个科室能开哪个药房的药、某个医嘱频次配哪个用法,不是写死在代码里,而是存在DYK表里。迁移或对接时如果只搬主字典不搬对应表,系统能启动,但开医嘱时会报“无可用药房”或者频次用法对不上,这类问题排查起来非常费劲,因为报错信息往往只指向业务层。
查表时我一般按这个顺序走:先确定业务模块(前缀),再确定是主字典还是明细/对应(后缀),最后才去目录里定位具体表名。举个实际场景,要查“住院医嘱的频次和用法怎么关联”,路径是CPOE_YZYPPCK(频次主表)→CPOE_YZYPYFK(用法主表)→CPOE_YZPCYFDYK(频次用法对应表),三张表串起来才是完整规则。只查前两张,你会以为频次和用法是自由组合的,实际上对应表里已经限定了合法组合。
提示:目录里
CPOE_TEXTORDER文字医嘱和CPOE_WZYZK文字医嘱库同时出现,命名相近但归属不同章节,实际使用时以字段定义为准,不要凭表名判断功能重复。
3. 把字典变成可查的资产:字段解析与建库落地
文档本身是 Word 格式的设计说明书,几百页翻起来痛苦,真正干活时需要把它变成能 SQL 查询的东西。这一步的核心是把“表说明 + 字段定义 + 类型长度 + 值域”四类信息结构化。
3.1 字段四要素:类型、长度、备注、值域怎么读
每张表的字段描述通常包含字段名、中文含义、数据类型、长度、是否可空、备注/值域。读的时候重点抓三样。第一是类型和长度,老系统里varchar长度往往卡得很死,比如诊断名称给 100 字符、备注给 255,迁移到新库时如果盲目放大长度,索引和存储都会受影响;反过来如果新库字符集从 GBK 换成 UTF-8,原来按字节算的长度可能不够,中文会截断。第二是值域,很多字段备注里写着“0 男 1 女”或者“见 PUB_YBFLK”,这类字段不能当自由文本处理,抽取时必须做码表关联。第三是可空性,临床数据里大量字段允许为空,做非空约束前要先统计实际空值率,否则建表就失败。
下面这段是我把单张表的字段定义整理成结构化数据的思路,用 Python 演示解析逻辑,实际输入可以是手工整理的 CSV,也可以是从文档里抽出来的表格。
import csv # 字段定义记录:表名、字段名、中文名、类型、长度、可空、值域说明 rows = [ ["CPOE_YZYPPCK", "YZPCID", "医嘱频次ID", "varchar", "20", "N", "主键"], ["CPOE_YZYPPCK", "YZPCMC", "医嘱频次名称", "varchar", "50", "N", ""], ["CPOE_YZYPPCK", "MRCS", "每日次数", "int", "4", "Y", "正整数"], ["CPOE_YZYPPCK", "YXBZ", "有效标志", "char", "1", "N", "0无效 1有效"], ] def build_ddl(rows): table = rows[0][0] cols = [] for r in rows: _, col, cn, dtype, length, nullable, domain = r # 长度只对字符类型生效,数值类型忽略 if dtype in ("varchar", "char", "nvarchar"): col_def = f" {col} {dtype}({length})" else: col_def = f" {col} {dtype}" if nullable == "N": col_def += " NOT NULL" col_def += f" COMMENT '{cn} {domain}'.strip()" cols.append(col_def) return f"CREATE TABLE {table} (\n" + ",\n".join(cols) + "\n);" print(build_ddl(rows))这段代码的逻辑很直白:把每行字段定义映射成 DDL 片段,字符类型带长度、数值类型不带,N转成NOT NULL,中文名和值域拼进COMMENT。参数上要注意两点:一是length对int这类类型没有意义,直接忽略,否则会生成int(4)这种在新版本 MySQL 里已废弃的写法;二是注释里的值域说明要保留,它是后续做码表校验的唯一线索。实际处理 800 张表时,我会先跑一遍生成 DDL,人工抽查十几张核心表(职工、科室、药品、诊断、医嘱频次),确认类型映射没错,再批量执行。
3.2 用一张元数据表管住 800 张表
表一多,光靠目录翻是不现实的。我的做法是先建一张元数据表,把“表名、中文名、所属模块、主键、关联表”录进去,之后所有查询都从这张表出发。
CREATE TABLE meta_table_dict ( table_name VARCHAR(64) NOT NULL COMMENT '物理表名', table_cn VARCHAR(128) NOT NULL COMMENT '中文表名', module VARCHAR(16) NOT NULL COMMENT 'SYS/PUB/CPOE/OUTP', pk_col VARCHAR(64) COMMENT '主键字段', ref_tables VARCHAR(255) COMMENT '关联表,逗号分隔', PRIMARY KEY (table_name) ); INSERT INTO meta_table_dict VALUES ('CPOE_YZYPPCK', '医嘱频次库', 'CPOE', 'YZPCID', 'CPOE_YZPCYFDYK'), ('CPOE_YZYPYFK', '医嘱药品用法库', 'CPOE', 'YFID', 'CPOE_YZPCYFDYK'), ('CPOE_YZPCYFDYK', '医嘱频次用法对应库', 'CPOE', 'ID', 'CPOE_YZYPPCK,CPOE_YZYPYFK');这张元数据表的价值在于把“人脑记忆”变成“可查询数据”。比如要排查某张对应表的两端是谁,一条SELECT ref_tables FROM meta_table_dict WHERE table_name='CPOE_YZPCYFDYK'就出来了。参数上,module字段用前缀枚举值,方便按模块统计表数量;ref_tables存逗号分隔的表名,虽然不符合范式,但在这种轻量字典场景下够用,真要严谨可以再拆一张关联表。录入时优先录核心业务表,边缘表可以后补,不要一开始就追求 800 张全录完,那样容易半途而废。
3.3 从字典反推业务规则:以医嘱频次用法为例
字典不只是查字段,还能反推业务约束。以医嘱频次用法为例,CPOE_YZPCYFDYK这张对应表的存在说明:频次和用法不是任意组合,而是有白名单的。做接口对接时,如果对方系统传过来一个频次+用法的组合,你得先查这张对应表确认合法性,否则落库后医生工作站可能显示异常。
常见做法是写一个校验函数,在数据入库前跑一遍:
def check_pc_yf(pc_id, yf_id, valid_pairs): """valid_pairs 为从 CPOE_YZPCYFDYK 加载的 (频次ID, 用法ID) 集合""" if (pc_id, yf_id) not in valid_pairs: raise ValueError(f"频次 {pc_id} 与用法 {yf_id} 无对应关系,拒绝入库") return Truevalid_pairs在服务启动时一次性加载进内存,数据量通常几百到几千条,完全放得下。这个校验看着简单,但能挡掉大量“数据能存进去、业务跑不通”的隐性错误。类似的对应表还有CPOE_YPYFDYK药品用法对应、CPOE_YPDYLDSZ剂型对应药品联动设置,思路完全一样:凡是DYK结尾的表,都当成业务规则表来对待,而不是普通字典。
4. 避坑与排查:老字典落地时最容易翻车的五件事
这份文档是 2014 年的设计说明,直接照搬会踩坑。下面五条是我和同行实际遇到过的,按“现象 → 原因 → 解决”记下来。
4.1 现象:按文档建表后中文乱码或截断
原因:文档里的长度是按当时数据库字符集(常见 GBK)给的字节长度,迁到 UTF-8 后一个汉字占 3 字节,原来varchar(50)存 16 个汉字,现在只能存 16 个字符但字节数翻倍,如果新库按字节限制就会截断。解决:建表前确认目标库字符集,字符类型长度按“字符数”而非“字节数”定义,并对存量数据做长度抽样,找出接近上限的字段单独评估。
4.2 现象:接口能通,但医生开医嘱时报“无可用频次”
原因:只迁了CPOE_YZYPPCK和CPOE_YZYPYFK两张主表,漏了CPOE_YZPCYFDYK对应表,业务层查不到合法组合。解决:迁移清单里把所有DYK结尾的表单独列一组,逐张核对数据量,对应表为空是最隐蔽的故障源。
4.3 现象:同一字段在不同表里类型不一致
原因:老系统多模块并行开发,PUB_和CPOE_下同类字典各自演进,字段类型可能一个char(1)一个varchar(2)。解决:以业务主表为准做类型归一,写一个跨表字段比对脚本,把同名字段的不同定义列出来人工裁决,不要默认“文档写的都一样”。
4.4 现象:值域字段直接当文本存,统计时口径混乱
原因:备注里写着“0 男 1 女”的字段,实际数据里混进了空串、NULL、甚至中文“男”。解决:抽取时强制走码表映射,对无法映射的值单独落异常表,不要静默转成默认值,否则性别统计永远对不上。
4.5 现象:按表名猜功能,把明细表当主表用
原因:MXK明细表数据量可能远大于主表,误当主表做全量同步会拖垮链路。解决:同步前先查元数据表确认表性质,明细表必须带主表外键条件增量抽取,禁止无条件下全量。
5. 进阶用法:把表结构字典变成数据质量检查基线
字典用熟了之后,它的价值会从“查字段”升级成“定基线”。我现在的习惯是:拿到任何一份临床数据,先用字典里的类型、长度、值域生成一套校验规则,跑一遍再谈分析。比如PUB_ICD10诊断代码表,字段长度和编码规则是固定的,抽取病案首页诊断时如果出现长度不符或编码不在表内,基本可以判定是上游映射错了,而不是数据本身有问题。
具体做法是写一个规则生成器,从元数据表里读出每张表的字段定义,自动产出校验 SQL。下面是一个简化示例,针对CPOE_YZYPPCK生成三条检查:主键非空、频次名称非空、有效标志值域合法。
-- 规则1:主键非空 SELECT COUNT(*) FROM CPOE_YZYPPCK WHERE YZPCID IS NULL; -- 规则2:名称非空 SELECT COUNT(*) FROM CPOE_YZYPPCK WHERE YZPCMC IS NULL OR TRIM(YZPCMC) = ''; -- 规则3:有效标志值域 SELECT COUNT(*) FROM CPOE_YZYPPCK WHERE YXBZ NOT IN ('0','1');三条语句返回都应为 0,非 0 就说明数据有问题。参数上,值域检查的合法集合直接从字典备注里解析,不要手写,否则表一多就维护不动。这套规则跑在数据入库前的 staging 层,比事后在报表里发现异常再回溯要省事得多。
还有一个更实用的场景:做新旧系统字段映射时,把字典里的中文名和值域当成“语义锚点”。新系统字段叫order_freq_code,老系统叫YZPCID,光看名字对不上,但两边中文名都是“医嘱频次”,值域都是正整数,就能建立映射。映射表建好后,用抽样数据双向比对,命中率低于 95% 的映射关系全部打回人工确认。
有个血泪经验:不要相信“文档里没写的字段就不存在”。老系统运行多年,现场加字段是常态,字典只能作为基线,实际库结构一定要以information_schema查出来的为准,两者做 diff,差异部分才是真正需要花时间的地方。从那以后我每次接手这类字典,都强制先跑一遍文档与实库的字段比对,再谈迁移方案。希望帮到你。
本文还有配套的精品资源,点击获取