简介面向医疗信息化领域的Caché数据库开发文档合集围绕HIS系统建设场景系统梳理InterSystems Caché的部署、建模、编程与运维关键知识适用于医院信息系统研发人员、数据库管理员及医疗IT运维团队。压缩包共有145个文件以143份PDF技术文档为主体另含1个HTML导航页面和1张产品示意图整体大小77.18MB。PDF文档内容覆盖环境安装与基础配置、多维数据模型设计、MUMPS语言编程指南、API参考、高可用集群与灾难恢复策略、用户权限与数据安全配置、医疗业务报表与分析工具使用方法并包含示例代码与项目样例便于对照实践。适合需要快速上手Caché数据库、或正在优化现有HIS系统的后端开发工程师、数据库管理员及医疗IT运维人员已有1031人学习使用。通过研读这套资料能够形成从数据模型设计、系统开发调试到容灾加固的完整认知直接辅助医疗业务系统的构建与维护提升开发效率和系统稳定性。1. Caché HIS数据库为什么难上手先把三个底层差异看清楚第一次在Caché里找数据我花了一刻钟因为导航工具里根本没有“表”这个概念。后来才彻底明白Caché HIS数据库存数据靠的是全局变量Global这个多维结构SQL、对象和维度只是不同视角。这套《医疗HIS系统Caché数据库开发文档资料》就是把“数据到底存在哪、怎么改、怎么查”这条主线串起来核心覆盖Global操作、类定义、SQL混用、锁与事务、备份恢复。它适合三类人替老HIS做二次开发但只会Oracle的开发者维护Caché实例却只能重启服务的运维工程师以及要接数据做报表的接口工程师。它不是语法手册更像一套把开发闭环走通的项目笔记跟着在测试库上敲一遍基本功就立住了。2. Caché在HIS里的立足点Global存储、类映射与事务模型2.1 一张挂号单在Global里是怎么存的在Caché里一切持久化数据最终都存在Global里。Global是一个稀疏多维数组以^开头在命名空间里可以被任意代码访问。以挂号数据为例把患者主档和当日挂号信息按下标铺开是这类系统最常见的数据形态。; 写入患者主档字段用分隔符拼接 Set ^HIS.Reg.Patient(Z0001)张明|男|1978-02-11 Set ^HIS.Reg.Patient(Z0002)李红|女|1990-07-23 ; 按下标取出姓名 Set name$Piece(^HIS.Reg.Patient(Z0001),|,1) ; 用 $Order 遍历所有人直到下标为空退出 Set id$Order(^HIS.Reg.Patient()) While (id ) { Write 病历号: , id, 内容: , ^HIS.Reg.Patient(id), ! Set id$Order(^HIS.Reg.Patient(id)) }逻辑说明第一段写两行患者数据下标是病历号内容是竖线拼接的字段串。Caché不校验内容格式“|”的分隔只是业务约定。取出姓名时用$Piece按位取第一段这比先拆成数组再取数要快。遍历用$Order从空下标开始每次取下一个存在的下标循环结束条件就是返回空字符串。因为没有物理行号的概念$Order在顺序扫描里比直接枚举数值下标可靠得多。参数说明下标写成字符串在Global里完全合法而且能跳过空节点若写成数字Caché会按数值排序和字符串顺序不一致混合下标时最容易踩坑。内容串里不要出现和分隔符相同的字符否则$Piece取数就会错位。我在生产环境里会把字段分隔符换成$C(1)这类不可见字符避免病人姓名、地址里出现竖线干扰解析。Caché在HIS里经久不衰一个重要原因是就诊数据天然就是树形的一个患者多张就诊卡一次就诊多组医嘱一组医嘱多条明细。用关系型库表达这个结构要建四五张表加外键而Global允许直接用嵌套下标表达层次关系。这是从Oracle转过来时最容易忽视的差异。2.2 类定义、SQL与Global三者谁是真正的存储类定义%Persistent父类在编译时生成一套Storage映射Storage把属性映射到命名空间内的GlobalSQL表只是这套映射的只读视图。所以一个持久类怎么存最后还要看Storage映射怎么配。先写一个简化的患者主档类。Class HIS.Reg.Patient Extends %Persistent { Property PatNo As %String(MAXLEN20); Property FullName As %String(MAXLEN40); Property Gender As %String(MAXLEN6); Property BirthDate As %Date; Index PatNoIndex On PatNo [ Unique ]; Index NameIndex On FullName; }这段代码编译后会生成两个系列的Global^HIS.Reg.PatientD用来存记录D是Data的缩写^HIS.Reg.PatientI下面按下标存各索引。PatNoIndex定义成Unique时插入重复病历号的请求会直接回滚。索引名和属性名可以不同名它就是一个“下标组合”的命名空间。参数说明MAXLEN影响字符串的存储上限和交互窗口里的长度校验写入超长内容时类保存会报错所以这个值不是随便填的。%Date在磁盘上是整数存储显示时再转成日期格式SQL里处理它时要注意内部存的是从1841年起的“天序号”老系统做数据迁移时很容易在这个点上算错年份。什么时候不该直接碰Global如果项目里已经有类定义绕过类写Global等于绕过了数据校验和默认值逻辑。我的习惯是开发阶段可以用Global快速验证数据形态生产代码统一走类方法只有对性能极度敏感且有监控兜底的模块才特许直接索引。这么做的好处是将来升级要换存储布局时只有类定义和映射要改应用层代码不受牵连。2.3 事务与锁HIS并发写入为什么容易锁冲突HIS里并发最高的写操作集中在挂号、收费、发药确认。Caché的锁机制以Global节点为对象常见锁模式分为共享锁和排他锁锁名称由代码自行约定。; 尝试获取指定节点的共享锁允许3次重试 Set ok$System.Lock.Lock(^HIS.Reg.PatientZ0001,S,3) If ok1 { Write 锁获取失败有人正在改这条患者记录, ! Quit } TSTART Set ^HIS.Reg.Visit(V2025120001)已分诊|分诊台A TCOMMIT Do $System.Lock.Unlock(^HIS.Reg.PatientZ0001,S)逻辑说明先锁住患者节点再更新就诊状态是为了防止分诊和收费并发时读到中间态。锁获取的第三个参数是重试次数用0表示立即返回不等待用正数表示最多重试指定次数。TSTART开启显式事务TCOMMIT提交事务提交时会同步释放事务内获取到的临时锁。如果中间出错用TROLLBACK回滚整段操作。参数说明锁名称不一定要和存储节点严格对应只要进程间约定一致它更像一把业务锁但按存储节点加锁可以顺带阻止数据被改写。从Oracle过来的人最反直觉的一点是无锁模式下两个进程同时改同一个节点后写覆盖先写Caché不会像关系型库那样自动报错。事务要自己包锁要自己申请写代码时脑子里得有这条线。锁冲突通常有两个来源一是事务跨度太长二是事务里夹杂了不必要的读取。收费模块往往会先读患者余额、再写账务、再更新挂号状态事务一拉长高峰期多个事务就在存储节点上互相等。对策是先缩事务边界把只读操作挪到事务外面以及在上游应用层控制同一患者记录上的操作串行化不要把并发压力全丢给数据库。3. 从文档包复现一个患者主索引类定义、Studio与导入验证的完整路径3.1 文档资料先看哪几块按什么顺序翻这类资料包里通常有六类东西环境安装与许可说明、开发工具Studio或Atelier的操作说明、Caché对象脚本和SQL样例、管理门户与DBA命令、备份恢复手册、以及一组演示用的项目源码。很多人拿到手爱直接翻语法部分翻完合上什么都没留下。建议的顺序是先做环境安装和命名空间创建然后挑一个演示源码导入Studio编译成功后立刻去看它生成的Global。命令语法只有在能看到物理存储结构变化的时候才记得住。模块怎么用建议投入时间环境与许可装开发版实例建测试命名空间半天Studio/Atelier熟悉类编辑器、Terminal、SQL执行窗口1天对象与Global样例跟随样例创建类、写数据、观察Global1-2天SQL与接口练习映射表和ODBC连接1天备份与恢复在测试库上做一次全备份和恢复演练半天演示源码反向追踪一个模块的前后端数据流2天表格是参考顺序不是章节顺序。如果你的目标就是快速上手开发第六项演示源码最值得花时间如果目标是维护老系统第二项和第五项优先。3.2 在命名空间里建第一个持久类并完成编译第一步建测试命名空间。常见做法是在管理门户里用“系统管理—配置—命名空间”创建终端下的等价路径如下zn %SYS Do ##class(Config.Namespaces).Create(HISDEMO, c:\cachedemo\hisdata) zn HISDEMO Write $Namespace逻辑说明第一条切换到%SYS命名空间才有权限创建第二条调用配置类创建命名空间第三个参数是数据库路径路径不存在时会自动建目录zn命令负责切换命名空间。参数说明数据库路径最好单独放一个目录不要放到Caché安装目录下否则备份恢复时容易牵连系统库。命名空间名建议大写因为它会渗透到SQL表名和类包名里。第二步打开Studio新建一个类把上一章的患者类定义粘进去保存后编译。也能用命令行直接编译Do $System.OBJ.Compile(HIS.Reg.Patient,c)参数“c”表示编译并保留编译器日志失败时能翻出错在哪一行。编译成功后在Terminal里验证Global是否生成Write $Data(^HIS.Reg.PatientD), ! Write $Data(^HIS.Reg.PatientI(PatNoIndex))第一行返回非0代表数据Global存在第二行返回非0代表Unique索引Global生成了下标内容。如果两者都是0说明类没有真正编译或者映射到了别的命名空间这时候先回Studio看Storage页签。3.3 建完类后做三件事插入、索引查询、看执行计划对象方式插入一条数据Set p##class(HIS.Reg.Patient).%New() Set p.PatNoZ0001 Set p.FullName张明 Set p.Gender男 Set p.BirthDate$ZDH(1978-02-11,3) Set scp.%Save() Write sc逻辑说明%New创建对象实例设置属性%Save持久化保存成功返回1。$ZDH把文本日期转成Caché内部天数第二个参数是日期格式编号。这里推荐统一使用标准格式的日期格式避免英美日期顺序混淆。老系统导入数据时经常因为日期格式编号没对上导致所有生日都偏移这种错误还特别难发现。然后验证索引查询SELECT PatNo, FullName FROM HIS_REG.Patient WHERE FullName %STARTSWITH 张%STARTSWITH匹配前缀配合NameIndex能走索引前缀查找不会逐行扫。如果写成“FullName张”则永远查不到因为表里存的是完整姓名。按模糊记忆查患者姓名时%STARTSWITH也比“LIKE %张%”快很多。想看查询有没有走索引在SQL语句前加SHOWPLANCaché会输出执行计划文档包里对这类操作有专门说明。4. Caché开发问题排查从HIS项目现场带回来的五类踩坑记录4.1 中文姓名排序乱序ORDER BY结果不是拼音顺序现象用ORDER BY FullName查患者列表出来的顺序有时按Unicode码点有时按录入顺序和预期完全对不上。原因Caché的字符串默认Collation按二进制或Unicode码点排序不识别拼音。HIS的名单列表又特别讲究翻页找人这个坑几乎总会碰到。解决对姓名索引显式指定排序规则。Property FullName As %String(MAXLEN40, COLLATIONPinyin);重新编译类后SQL的ORDER BY FullName就会按拼音排序。老实例不给改字段时可以给SQL排序字段套一层函数处理但代价是索引失效表数据量大时这个曲线方案会明显变慢。所以最好还是从索引定义上解决底子干净后面报表模块不遭罪。4.2 收费窗口锁冲突高峰期事务频繁超时现象每天就诊高峰时段收费窗口偶发卡顿错误日志里反复出现“Lock conflict”。原因收费流程里事务跨得太长。窗口人员录入慢事务里带着需要写入的医嘱节点后一个窗口的更新必须等前一个窗口释放锁高峰期自然排队。解决砍事务边界只把最后写库的几步放进TSTART/TCOMMIT中间读取全部放在事务外把“先检查后修改”改成“条件写”。UPDATE HIS_REG.Account SET BalanceBalance-80 WHERE PatNoZ0001 AND Balance80“条件写”把两步合成一步底层对上锁时间窗口就短了一半。锁升级阈值也要留意单节点写操作一旦锁升级可能变成全表锁这时候症状就从一个窗口卡变成一层楼卡。把锁超时时间调到合理范围同时监控锁冲突日志出问题时有据可查。4.3 SQL查得到的记录$Order遍历Global却找不到现象SQL里SELECT能查到10条记录用ObjectScript的$Order遍历对应数据Global却少了几条。原因第一种可能是数据Global实际是索引节点而不是数据节点SQL走了索引第二种可能更隐蔽类的Storage映射把部分属性单独分配到了另一块Global数据和预想的名字对不上。解决在管理门户里打开类的Storage编辑器看它具体生成了哪些Global排查时以Storage定义为准不要靠猜。判断节点是否存在用$Data不要依赖$Order第一次返回的值$Order的返回值在某些边界条件下会被误解为空实际只是从当前下标跳到了下一个空节点。4.4 导出导入后中文乱码与字符串被截断现象把某张表导出成文本再导入另一套环境中文变成问号或残缺字符重启实例也无效。原因老版本Caché导出文件默认带系统编码导入环境的字符集设置和导出端不一致字符串字段的MAXLEN设得太短保存时被静默截断这个更隐蔽因为界面上不报错。解决导出前核对两套环境的默认字符集导入后跑一个对账脚本按主键比对关键字段长度和内容业务关键字符串字段把MAXLEN调大尤其病历备注这类文本宁长勿短老库里截断数据追不回来只能人工补录。文档包里的数据迁移章节一般会附带这类对账脚本模板直接改字段名就能用。4.5 备份恢复演练发现的日志疯狂增长现象做了两次在线备份数据目录空闲空间从60%掉到20%Journal文件数量暴涨。原因备份默认会开启日志归档恢复演练又在同一实例上执行旧日志没及时清理新日志照常累积。解决到管理门户里核对日志自动清除策略。常见做法是备份后立即切换日志并设置归档目录归档目录单独挂一块盘给日志目录单独配置监控告警不要等整个数据盘满了再处理。恢复演练前先确认演练环境和生产环境的日志路径没有共享否则一次误操作就把生产日志冲掉。5. HIS业务模块怎么落地ObjectScript、SQL与Global三者的实际边界5.1 写操作走类方法读操作交给SQLHIS里每个写操作背后都带着业务规则退费必须校验原收费流水存在且未被冲正发药必须校验处方状态。直接用SQL UPDATE能绕开业务层短期痛快长期维护痛苦。我的做法是围绕类封装写方法读和统计放开用SQL。ClassMethod Charge(patNo As %String, amount As %Numeric) As %Status { Set account##class(HIS.Reg.Account).%OpenId(patNo) If $IsObject(account) Quit $System.Status.Error(5001,患者账户不存在) If account.Balance amount Quit $System.Status.Error(5002,余额不足) Set account.Balanceaccount.Balance-amount Quit account.%Save() }逻辑说明%OpenId按主键打开账户对象$IsObject判断是否打开成功。余额不足直接返回错误最后才落库。这个方法在应用层是唯一的扣费入口所有调用方都必须经过它。返回值用%Status对象而不是简单整数调用方要用$System.Status.GetErrorText()去显示具体错误文本。参数说明错误码5001和5002是自定义范围不同模块要预留错码区间别全挤在一起。这类方法越短越好因为它会被窗口客户端频繁调用一旦里面混入报表逻辑响应时间立刻恶化。5.2 病历长文本用流字段存而不是堆在普通属性里把病历全文塞进字符串属性小数据量时看不出问题大数据量时保存会卡读取也慢。正确做法是把长文档定义成流类型的字段。Property MedicalNote As %Stream.GlobalCharacter; Method SaveNote(patientId As %String, noteText As %String) As %Status { Set note##class(HIS.Reg.Patient).%OpenId(patientId) Set streamnote.MedicalNote Set scstream.Write(noteText) If $System.Status.IsError(sc) Quit sc Quit note.%Save() }逻辑说明流字段不是一次性把全文塞进内存Write方法把内容追加到后台分配的Global子节点里读取时按流方式逐段取前台翻页就不容易卡。参数说明%Stream.GlobalCharacter的子节点由系统管理外部不要手工改它有独立的大小统计和移动指针操作数据完整性交由类层维护。存病理报告、检查所见这类长文本这一条能省很多事后补数据的力气。5.3 统计报表位图索引与SQL视图怎么组合HIS里统计报表大多是“按时间范围统计就诊人数、费用汇总”这种查询最怕WHERE条件全表扫。位图索引就是为这种场景准备的。Property VisitDate As %Date; Index VisitDateIdx On VisitDate [ Type bitmap ];位图索引用一个节点描述整个数据范围的位图存储占用小聚合计算特别快。按月的费用合计优化效果很明显。建一个视图收口查询路径CREATE VIEW HIS_V_VISIT_SUMMARY AS SELECT VisitDate, COUNT(*) AS Cnt, SUM(TotalAmount) AS Amt FROM HIS_REG.VisitLog GROUP BY VisitDate报表工程师只查HIS_V_VISIT_SUMMARY不给他们底层表的直连权限查询路径就稳定在数据库控制范围内。开发期间要定期看视图的执行计划确认它走位图索引而不是Huge Scan一旦数据量涨上来位图索引的维护成本也会上升频繁写入的表不建议无脑加位图。6. 进阶用“下标即索引”设计时间序数据顺带把统计报表提速在一套就诊流水表的改造里我把主键从“自增序号”调整为“日期流水号”的多级下标改动量不大效果非常直接。; 按天存就诊流水下标是日期流水号 Set ^HIS.Reg.VisitFlow(2025-11-01,A0001)张明|挂号|120 Set ^HIS.Reg.VisitFlow(2025-11-01,A0002)李红|收费|85 ; 统计某天的总费用 Set total0 Set seq$Order(^HIS.Reg.VisitFlow(2025-11-01,)) While (seq) { Set totaltotal$Piece(^HIS.Reg.VisitFlow(2025-11-01,seq),|,3) Set seq$Order(^HIS.Reg.VisitFlow(2025-11-01,seq)) } Write 当日费用合计: , total逻辑说明日期作为第一层下标查询单天记录时只扫该日期下的子节点不用翻全年数据。统计某月时逐天取数整个过程没有“索引查一遍再回表查一遍”的二次查找。参数说明日期字符串格式必须保持对齐“2025-11-01”和“2025-11-1”的排序顺序不同下标建议统一零填充格式要么都用带横线的要么都用YYYYMMDD不要混用。这套设计配合位图索引后报表查询基本不再碰高代价的排序和全遍历。但要提醒一句直接写Global是性能方案而不是架构方案一旦这么写了索引、事务、权限都不再由类层自动管理所以我只建议在导出类数据或专用报表表上用它。真要在生产账号上这么干必须把监控和恢复方案一并配套。回想那次改造最深的教训不是某一条语法而是那句总被忽略的提醒Caché里“下标即索引”存结构要比写查询花更多心思。从那以后我每次拿到新的存储需求都会先在本子上画树形下标画到没有多余层级才动手建类这个习惯帮我少趟了很多坑。希望帮到你。本文还有配套的精品资源点击获取