☰
卫宁电子病历表结构解析:从Word文档到SQL视图的实战指南
2026/10/2 9:16:41 网站建设 项目流程

简介:这份资源是卫宁电子病历系统(EMR)的数据库表结构设计说明书,实际文件名为《临床信息系统数据库结构设计说明书》,基于卫宁5.0版本整理。内容覆盖系统框架、财务收费、医疗信息三大板块,完整收录800多张数据表的结构定义,包括职工代码库(SYS_ZGDMK)、医疗项目库(PUB_YLXMK)、药品分类库(PUB_YPFLK)、科室/病区代码库、收费项目库、医保分类库、诊断代码库(PUB_ZDDMK)等,每张表均含字段名、类型、长度、备注和值域。面向医院信息科人员、HIS/EMR开发商、医疗数据工程师与系统集成商,可直接用于二次开发、接口联调、数据迁移及医疗大数据分析。压缩包内共1个doc文档,大小13.92MB,已有1541人学习。对于需要了解卫宁EMR底层数据结构或排查数据库层问题的技术人员,这份文档能提供直观、完整的字段级参考。

1. 卫宁电子病历表结构.doc:医院数据项目里那张最值钱也最没人看的旧地图

做过医院集成平台或者互联互通评审的人,大概率都有过这种经历:进场第一周,甲方丢给你一个“卫宁电子病历表结构.doc”,说是历届项目组传下来的“宝贝”。你打开一看,里面是几百张表的字段清单,空格和缩进混乱,说明文字夹杂着“�”字符,日期字段类型五花八门。你随手关掉,决定自己去库里头摸。三个月后,你在报表取数、临床数据上报、数据迁移这些小问题上反复翻车,才意识到那张doc里写的是卫宁这套EMR系统里最全的库表地图——虽然它老、它脏、它不一定跟你手里的库完全对得上,但所有关键表的主键关系、字段语义、类型陷阱,都藏在里面。

这篇文章不为科普“电子病历是什么”,而是把“卫宁电子病历表结构.doc”当成一份可以二次开发的原始资产来拆解:怎么把它变成能跑的SQL脚本、能建的数据字典、能对得上现场库的体检报告,以及这条路上你会踩的坑。适合集成商的数据工程师、驻场运维、做数据中台和上报的伙伴,以及刚接手医院老系统、准备做国产化迁移的人。

2. 读懂doc里的“卫宁方言”:表前缀、主键和版本差异

2.1 表前缀的潜规则:EMR_、PAT_、MED_、ODS_各管哪一段

卫宁电子病历的库表命名有一个底层习惯:用前缀区分业务域。我拆过好几个版本的现场库,虽然表名的细节各不相同,但前缀规律基本稳定。

以EMR_开头的是电子病历的核心业务表,比如病历文书主表、病程记录、护理记录、知情同意书。以PAT_开头的是患者主索引相关表,承载患者基本信息、就诊记录、过敏史。MED_开头大多是医嘱域,包括长期医嘱、临时医嘱、医嘱执行记录。ODS_开头则往往指向集成平台或数据中心,也就是卫宁做互联互通时往中间库同步的那一批表。

打开doc时,不要一上来逐行读字段,先按前缀把表分组,再按业务域去对照。

前缀常见业务域现场典型用途
EMR_电子病历文书文书内容、模板、签章、质控
PAT_患者与就诊患者主索引、入院记录、就诊卡
MED_医嘱长期/临时医嘱、执行记录
ODS_集成平台中间库上报、互联互通、数据订阅
CPT_收费/计费(部分版本)费用明细、结算

如果doc里出现你现场库里没有的前缀表,先别急,多半是版本差异导致的废弃表,或者另一个产品线的表被并了进来。卫宁在不同医院部署时,会按院方需求裁剪功能模块,表清单不会完全一致。我一般会先在doc里把这些表标成“候选”,再去现场库用ALL_TABLES比对,确认是否真实存在。

2.2 核心字段的通用含义:不靠猜,靠关联关系

看字段时,重点看几个几乎每类表都有的公共字段。

PATIENT_ID(或PAT_ID)代表患者唯一标识。在卫宁的体系里,患者主索引一般全局唯一,但要注意有些医院会存在双主键设计,即同一个患者在不同院区有多个ID。VISIT_ID(或CLINIC_ID)代表某一次就诊,比如一次住院或一次门急诊。病历记录和医嘱、检查报告关联时,通常是PATIENT_ID + VISIT_ID联合作为逻辑外键,而不是单靠一个字段。

DOCUMENT_ID在EMR表中通常是文书实例ID,对应一份具体病历。注意这个ID不一定是数字,可能是按“年份+流水号”拼出来的字符串。我在现场见过有人拿NUMBER()去接这个字段,结果隐式转换导致全表扫描,慢得离谱。doc里如果是VARCHAR2,那就是有原因的,不要惯性转类型。

时间字段方面,卫宁偏爱在各种表尾放CREATE_DATE、UPDATE_DATE,但真正业务意义上的时间往往是RECORD_TIME、ADMISSION_DATE、DISCHARGE_DATE这种语义更明确的字段。这条会在后面的“日期字段是VARCHAR2”坑里展开讲,这里先标记一下。

2.3 版本差异:doc很可能对不上你手里的库

这是最容易被忽略、但杀伤力最大的一点。这份doc可能是EMR 3.0时代的产物,而你现场装的是EMR 5.0或WiNEX的某个演进版本,中间经历过医嘱域拆分、文书表重构、CDA结构化升级。表名可能还在,但字段新增了一批,类型也变过。

我习惯的做法是拿到doc后,先挑三张高频表去现场库核对结构和数量:EMR_DOCUMENT(或类似命名的文书主表)的字段数、PATIENT表的索引情况、MED_ORDER表的状态字段类型。如果这三张对不上,那doc里的全部内容都要默认打折扣,只当“语义参考”用,不能直接拿来生成SQL。

注意:不要因为doc对不上就丢掉它。字段语义、取值字典、表间关系的描述,往往比现场库里的注释准确得多——现场库的注释可能压根就没写,或者被人改成过“临时勿动”这类的话。

3. 把.doc变成SQL和Excel:提取方案与最小脚本

3.1 不动原稿,先用WPS另存为docx再解析

.doc是老的OLE复合文档格式,直接解析二进制太过痛苦。唯一值得做的正道是先用WPS或Word打开,另存为.docx,然后再用代码解析XML。这一步十分钟能搞定,能省掉后面所有解码乱码的烦恼。我个人更推荐WPS,因为它在处理老式.doc时兼容性更好,另存docx时不会因为宏或书签问题卡住。

另存为docx后,表结构在Word里基本有两种形态:一种是真正的Word表格,每行一个字段;另一种是用“字段名+空格+类型”这种文本堆出来的伪表格。前者好办,后者需要一点清洗。无论哪种,都不要手动复制几百行字段去建Excel,这不是工程师该干的事。

3.2 C#解析Word表格:提取字段名、类型、说明到CSV

下面这段C#代码是我常用的最小方案,用DocumentFormat.OpenXml读取docx里的表格,把单元格文本按行拼接导出CSV。它的作用是把Word里所有表格一次性抽出来,不依赖Word组件,服务器上也能跑。

using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; string docxPath = @"C:\temp\emr_structure.docx"; string csvPath = @"C:\temp\emr_tables.csv"; var sb = new System.Text.StringBuilder(); using (WordprocessingDocument wordDoc = WordprocessingDocument.Open(docxPath, false)) { var body = wordDoc.MainDocumentPart.Document.Body; foreach (Table table in body.Descendants<Table>()) { foreach (TableRow row in table.Elements<TableRow>()) { var cells = row.Elements<TableCell>() .Select(c => c.InnerText.Trim().Replace(",", " ")) .ToList(); sb.AppendLine(string.Join(",", cells)); } sb.AppendLine("---TABLE_END---"); // 表间分隔线,方便后续按表切分 } } File.WriteAllText(csvPath, sb.ToString(), Encoding.UTF8); Console.WriteLine($"导出完成:{csvPath}");

这段代码的逻辑是:打开docx,遍历正文里所有<w:tbl>表格行,把每行的单元格文本取出来,按逗号拼成一行。---TABLE_END---作为表与表之间的切分标记,防止多张表的数据粘连在一起。核心参数就一个docxPath——要解析的docx文件路径。

这里有个容易翻车的点:cell.InnerText会把单元格里所有段落文本拼在一起,如果一个单元格里有换行或项目符号,会有意外拼接。处理办法是在取文本时只取第一段,或者把换行替换成空格。上面的代码已做了逗号替换,但换行符还没处理,建议实际用时加上Replace("\r", " ").Replace("\n", " ")。另外,如果单元格里有新版的“控件”(如文本域、书签内容控件),InnerText会取不到,这时需要额外处理SdtElement类型的单元格内容。

3.3 从CSV生成Oracle/神通可用的建表与注释脚本

CSV只是中间产物,最终目标是把字段清单独立成可执行的SQL。生成SQL的脚本我用Python写,因为正则清洗比C#顺手。它读取上一步导出的CSV,按“表名,字段名,类型,可为空,说明”这种顺序切分,生成两段东西:建表语句和注释语句。

import csv import re from collections import defaultdict def parse_structure_csv(csv_path): tables = defaultdict(list) current_table = None with open(csv_path, "r", encoding="utf-8") as f: for row in csv.reader(f): if not row or len(row) < 3: continue if "TABLE_END" in row[0]: current_table = None continue # 文件名、页眉页脚干扰行清洗 if row[0].startswith("卫宁") or row[0].startswith("文档"): continue # 表名通常在行首,字段名在第二列;如果第二列不是字段名则跳过 if row[1] in ("字段名", "列名", "名称"): continue tables[current_table].append(row) return tables def gen_create_sql(tables): sql_lines = [] for table_name, cols in tables.items(): col_defs = [] comments = [] for c in cols: col_name = c[1].strip() col_type = c[2].strip().upper() nullable = "NULL" if len(c) > 3 and c[3].strip() == "是" else "NOT NULL" comment = c[4].strip() if len(c) > 4 else "" # Oracle类型归一 if col_type == "VARCHAR2" or col_type == "VARCHAR": col_type = "VARCHAR2(255)" elif col_type.startswith("NUMBER") and "(" not in col_type: col_type = "NUMBER(18)" elif col_type == "DATE" or col_type == "DATETIME": col_type = "DATE" col_defs.append(f" {col_name} {col_type} {nullable}") if comment: comments.append(f"COMMENT ON COLUMN {table_name}.{col_name} IS '{comment.replace(chr(39), chr(39)+chr(39))}';") sql_lines.append(f"CREATE TABLE {table_name} (") sql_lines.append(",\n".join(col_defs)) sql_lines.append(");") sql_lines.extend(comments) return "\n".join(sql_lines)

这段的逻辑核心有两个:第一是清洗,过滤掉页眉页脚和标题行;第二是类型归一。VARCHAR2类型必须带长度,否则Oracle会报ORA-00906: missing left parenthesis;不带精度的NUMBER归一成NUMBER(18)是习惯做法,能避免后续精度问题。注释里的单引号转义一定要做,不然很多中文说明中含单引号会让SQL直接崩掉。

提示:这个脚本只解决“建影子表”的需求。如果你要做的是校验现场库和doc是否一致,则要把生成方向反过来——从ALL_TAB_COLUMNS导出现场结构,再和doc CSV做字段级diff,这个差异体检方案放在最后章节。

4. 按表结构搭“只读影子库”:对接数据中台的落地路径

4.1 先拆关联关系,再建视图

doc里面最能直接复用的资产是“表间关联线索”。卫宁的EMR表和医嘱表之间,往往通过VISIT_ID和ORDER_ID这两根线串起来;检查报告则靠REPORT_ID反向关联回就诊。建影子库(只读数据集市)时,我一般不会直接物化所有表,而是先用视图把“患者—就诊—文书—医嘱”这条主链路搭出来,后面所有上报需求都在这条链路上打补丁。

拆关联的顺序是:先找到患者主索引表,再找就诊表,然后分别向左向右扩展。比如,以PATIENT为主表,VISIT为就诊表,两者通过PATIENT_ID关联;EMR_DOCUMENT和VISIT通过VISIT_ID关联;医嘱表又往往挂在VISIT_ID上。这四层就是大多数统计需求的主干。

4.2 Oracle下建视图的最小SQL模板

下面这个SQL是建只读视图的主干,目标是给数据中台一个干净的“就医记录流水”宽表。字段都是从doc里最常见的命名捞过来的,实际使用时按现场doc替换为真实字段名。

CREATE OR REPLACE VIEW V_PAT_VISIT_DOC AS SELECT p.PATIENT_ID, p.NAME AS PAT_NAME, p.BIRTH_DATE, v.VISIT_ID, v.ADMISSION_DATE, v.DISCHARGE_DATE, v.DEPT_CODE, d.DOCUMENT_ID, d.RECORD_TIME, d.DOC_TYPE_CODE, d.CONTENT_TEXT FROM PATIENT p JOIN PAT_VISIT v ON p.PATIENT_ID = v.PATIENT_ID LEFT JOIN EMR_DOCUMENT d ON v.VISIT_ID = d.VISIT_ID WHERE v.ADMISSION_DATE >= TRUNC(SYSDATE) - 365

这段SQL的逻辑说明:用JOIN串起患者、就诊、文书三层,LEFT JOIN文书是因为不是每次就诊都一定有电子病历,比如部分门诊就诊只有处方没有病程。WHERE限定近一年就诊,把视图的数据量控制在合理范围。

需要注意的点:CONTENT_TEXT字段如果doc里标的是CLOB,在下游工具里做WHERE过滤或GROUP BY时会直接报ORA-00932: inconsistent datatypes。所以我建议视图层不要把CONTENT_TEXT整段暴露给中台,而是用一个函数截取前1000个字符。实践上,我会在视图里把大字段换成DBMS_LOB.SUBSTR(d.CONTENT_TEXT, 1000, 1),这样下游做抽样、预览、测试联调都没问题,等需要完整文本时再单独查原表。

4.3 用注释反向生成数据字典,前端直接引用

影子库建完后,还有一个很容易漏掉的活:为每个视图字段补注释。医院侧的数据分析人员通常不会直接看doc,他们习惯用数据字典工具查字段含义。如果视图字段没有注释,他们只能靠猜,猜错后反馈给你的就是一连串“数据对不对”的质疑。

COMMENT ON TABLE V_PAT_VISIT_DOC IS '患者就诊文书宽表,供数据中台调用'; COMMENT ON COLUMN V_PAT_VISIT_DOC.PAT_ID IS '患者唯一标识,来源于PATIENT表'; COMMENT ON COLUMN V_PAT_VISIT_DOC.DOC_TYPE_CODE IS '文书类型编码,见附录字典';

注释文本不要偷懒复制doc原文,因为doc原文往往带着一些“界面描述”,比如“文书上的创建人姓名”,而物化后的视图字段是代码或ID,两者语义并不一致。我会在注释里写清楚“来源字段”和“业务含义”,让使用者一眼知道这列是从哪张表捞出来的。这个细节在中大型医院做互联互通评审时尤其加分。

5. 避坑:卫宁表结构落地最常见的5个坑

5.1 日期字段是VARCHAR2,时间区间过滤直接失效

现象:写查询时WHERE ADMISSION_DATE BETWEEN '2024-01-01' AND '2024-06-30',结果要么报无效月份,要么查到一堆完全不在区间里的数据。

原因:卫宁部分老版本表里,日期字段不是DATE类型,而是VARCHAR2(19),存的值像2024-01-15 10:23:45。当BETWEEN的右边界是'2024-06-30'时,字符串比较会认为'2024-06-30 09:00:00'大于'2024-06-30',所以这天早上之后的记录全被丢掉。

解决:所有在doc里标注为DATE但实际代码里走字符串比较的字段,统一先TO_DATE再过滤。更省事的做法是在视图层就改成真正的日期类型,让下游不用操心。我在现场的做法是建视图时直接写TO_DATE(ADMISSION_DATE, 'YYYY-MM-DD HH24:MI:SS') AS ADMISSION_DATE,把脏活提前干完。

5.2 大字段是CLOB,group by和distinct直接崩

现象:一句话统计SQL,把文书表JOIN进来后COUNT(DISTINCT d.DOCUMENT_ID)变成几秒钟不出结果,或者直接报临时表空间不足。

原因:如果查询里意外把CONTENT_TEXT这种CLOB字段带进了GROUP BY或DISTINCT,Oracle无法对CLOB做分组操作,只能临时转类型或走全表扫描,性能雪崩。

解决:先从SQL里去掉CLOB字段;确实需要看内容摘要的,用DBMS_LOB.SUBSTR截断,同时记得不要在子查询里对这个截断结果再做DISTINCT。这就是一个长期被忽略的教训:字段清单里所有带LOB、LONG标记的列,都要在视图层做个“手术”,不该漏给报表侧就不漏。

5.3 文档版本与库版本错位,表名找得到、字段对不上

现象:按照doc里写的EMR_PRINT_LOG表名去查询,提示表不存在。或者表存在,但ALTER TABLE出来的字段比doc少了七八个。

原因:doc是随项目交付的,而卫宁实施时经常在客户现场升级过补丁或产品版本。补丁会新增字段(比如签章、质控相关的列),而doc不会随着补丁更新。这会导致doc里没有的字段,现场库新增了;doc里有的字段,现场库删了或改名了。

解决:不要依赖doc做精确DDL,而是把它当成“字段语义参考手册”。建视图或影子表之前,必须用SELECT * FROM ALL_TAB_COLUMNS WHERE TABLE_NAME = 'XXX'拉出现场真实结构做一次字段级别比对。比对脚本放在下一章,这里先给出原则:对不上时以现场库为准,但字段含义描述以doc为准——这两者结合才是完整的结构。

5.4 医院客开加字段,doc上没有,按文档建视图会少列

现象:数据中台按doc建好宽表,跑了一段时间后,临床科室突然反馈“报告时间怎么是空的”。排查发现,报告表的实际数据里有个REPORT_AUTH_TIME字段,是医院客开加的,doc完全没有记录。

原因:医院信息科或卫宁的客开团队会在标准版上增加扩展字段,这些字段往往加在表尾,命名风格和标准字段不一致。doc是标准版的产物,自然不会包含客开内容。

解决:每张核心表建视图时,不要手写字段清单,而是用SELECT *加EXCLUDE列的方式先落一个临时表,观察有哪些额外字段,再决定是否纳入视图。或者写一个自动化脚本,把doc和现场库字段自动做差集,跑出“现场新增字段清单”。我一般会把这个清单发给医院信息科,让他们标注这些扩展字段的业务含义,然后我再决定视图是否加列——这样才能保证宽表里不丢业务字段。

5.5 主键不是id,联合主键反而常见

现象:想用DOCUMENT_ID当主键去重,结果发现同一份文书有不同的DOCUMENT_ID,或者同一ID对应多行记录。

原因:卫宁有些业务表的主键是(DOCUMENT_ID, VERSION_NO)或者(PATIENT_ID, VISIT_ID, ITEM_NO)这种组合。一份病历文书可能因为修订、撤回、重新提交产生多个版本,每个版本一行,但DOCUMENT_ID不变。

解决:在视图或数据抽取逻辑里,先确定“你要的是最新版本还是全版本”。如果是取最新版本,可以用ROW_NUMBER() OVER (PARTITION BY DOCUMENT_ID ORDER BY UPDATE_DATE DESC, VERSION_NO DESC) = 1来去重。这个坑在数据上报时尤其致命——上报平台只接受一例一条记录,你不去重,会被平台反复打回。

6. 让doc“活”起来:一致性校验与国产化迁移

6.1 用SQL做字段级差异体检

把doc变成结构化清单后,下一步是拿它去和现场库做体检。下面的SQL从Oracle数据字典抽出某张表的字段、类型、可空性,把结果和doc生成的CSV做EXCEL的VLOOKUP也好,用Python做集合差也好,目标都是找出三个清单:doc有现场没有、现场有doc没有、两边都有但类型不同。

SELECT column_name, data_type, data_length, nullable FROM all_tab_columns WHERE owner = 'EMR' AND table_name = 'EMR_DOCUMENT' ORDER BY column_id;

这个查询本身很朴素,但它能解决第5章里的版本错位问题。我实践时会把结果导出CSV,和doc清单在Python里用集合运算做差集,生成一张“差异Excel”,分三个Sheet展示三类差异。这份文件交给医院信息科,就是一份很有说服力的现状评估报告,比口头说一百遍“你的doc过期了”有用得多。

6.2 把Oracle DDL手工收敛给神通/达梦:dbstudio只备份表结构就够了

现在不少医院在做信创改造,卫宁的Oracle库要迁到神通或达梦。这时doc的价值会被重新放大——因为doc里记载的字段语义,是迁移后做数据校验的基准。

迁移时最省力的做法是用神通自带的dbstudio工具勾选“仅备份表结构”,把现场Oracle库里的结构扒出来。注意,神通数据库的dbstudio的“仅结构”备份选项是必须勾的,否则会把数据一起倒出来,迁移时间会翻好几倍。只备份结构后,再结合doc和差异清单,手工调整字段类型映射即可。常见映射是:Oracle的VARCHAR2映射到神通也是VARCHAR2,NUMBER映射为NUMERIC,DATE不变。CLOB要特别注意,神通对大字段支持良好,但如果你在视图层已经做了DBMS_LOB.SUBSTR截断,迁移后建议统一改成VARCHAR2(4000),避免下游工具再踩CLOB的坑。

6.3 增量更新文档的习惯

doc作为静态文件,天生会过期。我现在每接手一个医院项目,都会在基线版本上维护一份增量记录:哪张表在哪个上线日期加了哪些字段,是谁加的,用途是什么。这份增量记录可以是Excel,也可以是Markdown表格,但它必须跟着变更走。

这个习惯救过我一次。有家医院在半年内做了电子病历五级评审的改造,连续三个迭代版本都改了文书核心表。我因为一直维护着增量记录,在评审数据核验时能准确回答“这个字段是哪个版本加的,目的是什么”,而另一家供应商标出来的doc还是半年前的老版本,一眼就被甲方发现对不上现场。所以,拿到doc只是起点,把它变成观察现场结构变化的基线,才是它真正的用途。

最后分享一个我自己的习惯:每次交付前,我会把doc的解析脚本、生成的CSV、差异体检报告放在同一个目录里,命名带日期。这样半年后有人问起“这表结构当时怎么定的”,还能翻出当时的原始依据,不至于靠回忆。希望这些方法对你手头那台老EMR系统也有用。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询