ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

卫宁电子病历表结构解析:从Word文档到SQL视图的实战指南

2026/10/2 9:17:20 拓冰建站 浏览量
卫宁电子病历表结构解析:从Word文档到SQL视图的实战指南 简介这份资源是卫宁电子病历系统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.DescendantsTable()) { foreach (TableRow row in table.ElementsTableRow()) { var cells row.ElementsTableCell() .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, encodingutf-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(fCOMMENT ON COLUMN {table_name}.{col_name} IS {comment.replace(chr(39), chr(39)chr(39))};) sql_lines.append(fCREATE 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 大字段是CLOBgroup by和distinct直接崩现象一句话统计SQL把文书表JOIN进来后COUNT(DISTINCT d.DOCUMENT_ID)变成几秒钟不出结果或者直接报临时表空间不足。原因如果查询里意外把CONTENT_TEXT这种CLOB字段带进了GROUP BY或DISTINCTOracle无法对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映射到神通也是VARCHAR2NUMBER映射为NUMERICDATE不变。CLOB要特别注意神通对大字段支持良好但如果你在视图层已经做了DBMS_LOB.SUBSTR截断迁移后建议统一改成VARCHAR2(4000)避免下游工具再踩CLOB的坑。6.3 增量更新文档的习惯doc作为静态文件天生会过期。我现在每接手一个医院项目都会在基线版本上维护一份增量记录哪张表在哪个上线日期加了哪些字段是谁加的用途是什么。这份增量记录可以是Excel也可以是Markdown表格但它必须跟着变更走。这个习惯救过我一次。有家医院在半年内做了电子病历五级评审的改造连续三个迭代版本都改了文书核心表。我因为一直维护着增量记录在评审数据核验时能准确回答“这个字段是哪个版本加的目的是什么”而另一家供应商标出来的doc还是半年前的老版本一眼就被甲方发现对不上现场。所以拿到doc只是起点把它变成观察现场结构变化的基线才是它真正的用途。最后分享一个我自己的习惯每次交付前我会把doc的解析脚本、生成的CSV、差异体检报告放在同一个目录里命名带日期。这样半年后有人问起“这表结构当时怎么定的”还能翻出当时的原始依据不至于靠回忆。希望这些方法对你手头那台老EMR系统也有用。本文还有配套的精品资源点击获取