系统卡顿引发的患者冲突:一场发生在贵州县医院的性能战争

上午10点43分,贵州贵阳XX县医院门诊大厅的空气凝固了。收费窗口前排起的长龙突然爆发出一阵争吵。

“我排了20分钟队,你们系统又卡了?!”一位50多岁的中年男子把病历本拍在窗口台上,脖子上的青筋都暴了出来。

收费员小王额头冒汗,手指在键盘上狂敲,屏幕上那个旋转的”加载中”圆圈转得让人心慌。”张师傅,不是我不收钱,是系统转圈圈转不出来。”

“我过敏史、医保卡都递进去了!现在让我重填?”患者的妻子也急了,声音尖利,”你们这效率,我们来来回回跑几趟?”

周围排队的人面面相觑,随即抱怨声四起。导诊台三个护士跑过来调解,但情绪像病毒一样传染。诊室3区,一位患者因为病历打不开,在里面和医生大声争执;药房取药窗口,药师扯着嗓子喊:”6号处方系统里看不到!刚开的!”

信息科值班员小马,27岁,软佳科技驻场在这家医院的实施工程师,此刻正躲在后台机房盯着服务器监控屏幕。他的后背已经被冷汗浸透——过去一个月,类似投诉已经13起。三天前院长下了最后通牒:”小马,再出一次大面积卡顿,系统停用,重新选型。”

小马深吸一口气,调取实时监控。硬件配置完全够:Dell PowerEdge R740,32G内存,SSD硬盘,千兆内网。但为什么每到高峰期(9-11点、14-16点)就卡顿?他打开慢查询日志,三个核心功能的响应时间触目惊心:

– 医生打开患者历史病历:平均4.2秒

– 药品下拉列表加载:平均3秒

– 收费结算:平均2.8秒

“4秒才能看历史病历,一个医生每天要看50+患者,单这一项就多花3分钟+,乘以门诊量300+……”小马在笔记本上快速计算,”这还不算用户重复操作的次数。一个高峰期,系统消耗的无效等待时间,至少是200人·小时。”

他想起主任的话:”县医院接诊量不大,日门诊量也就300多人,系统怎么就这么慢?”但300人不假,每个操作慢3秒,叠加起来就是灾难。高峰期100人同时在线,每秒并发请求30+,系统就像塞车的城市道路,每辆车都在等红绿灯。

小马拿起手机,给软佳总部技术团队发了紧急消息:”XX县医院,情况危急,需要深度性能剖析支援, ASAP。”

小马是软佳科技驻场到这家医院的实施工程师,27岁,贵州本地人。去年大学毕业进了软佳,这是他负责的第4个项目。前3个项目都比较顺利,但这家县医院的问题,让他连续两周没睡好。

“小马,到底能不能解决?”院长在技术协调会上直接问,”我们县医院接诊量不大,日门诊量也就300多人,系统怎么就这么慢?”

小马不敢打包票,但他知道,软佳的技术团队在昆明随时可以支援。

过去两周,小马已经做了初步排查。他用监控工具抓取了高峰期的系统数据,发现瓶颈集中在数据库和软件代码层面,而非硬件。

“院长,我现在要做一个深度诊断,可能需要1-2周时间。期间系统可能会有些调整,但我们会错峰进行,不影响门诊。”小马说。

院长点头:”给你时间,但要见效。”

小马的通知单发出去后,软佳总部技术团队派来了两位顾问:数据库专家老林和架构师老周。

三人碰头的第一天,老林就说:”我们先做一次完整的性能剖析,找出慢的原因。”

他们从三个维度入手:

第一,病历查询慢。

医生打开患者历史病历时,系统要查询 patientmedicalrecord 表。这条SQL很简单:

“`sql
SELECT * FROM patient_medical_record
WHERE patient_id = ?
ORDER BY visit_date DESC
“`

但执行一次要4.2秒。为什么?老林分析执行计划,发现字段 patient_id 没有索引,每次查询都是全表扫描。更糟糕的是,有些患者就诊次数多(>50次),一查就是几万条记录,越查越慢。

第二,药品加载慢。

医生写病历时,要选择药品。药品下拉列表有3000+条记录,每次打开都从数据库全量查询。而且没有缓存,哪怕上午刚查过,下午再开又要重新查一次。耗时3秒。

“医生等3秒没什么,但一天开100张处方,就是300秒,5分钟浪费在等药品列表上。”老周说。

第三,收费结算慢。

缴费时,系统要遍历所有处方项目,计算总额。并发高时(早高峰),多个收费窗口同时操作,数据库连接池很快耗尽,后续请求开始排队。平均响应2.8秒,收费窗口前就开始堵人。

“还有,”小马补充,”系统老问题很多。比如患者基本信息,没有做缓存;比如报表查询,是直接从生产库读;比如权限验证,每次请求都查数据库…”

老林总结:”典型的’能用就行’架构,没考虑性能。”

诊断完成,下一步是优化方案。

老林负责数据库层面:

1. 给 patientmedicalrecord 表加索引,按 patientid 和 visitdate 建立复合索引

2. 病历查询改为分页,每次只取最近20条

3. 高频率查询的表(药品、患者)建立查询缓存

老周负责架构层面:

1. 医生开处方时,实时计算费用明细并缓存,缴费时直接读取

2. 患者基本信息、药品字典加载到内存缓存(Redis),每次优先从缓存读

3. 报表类查询从只读备库走,不影响主库

小马负责实施:

1. 先在测试环境验证效果

2. 选择低峰期(下午1-3点)逐次上线

3. 监控每项优化的影响

4. 准备回滚方案

“我们分三步走,”老周说,”第一周做SQL索引和第二级缓存;第二周做架构调整;第三周观察效果,再做微调。”

实施过程并非一帆风顺。

第一天加索引,系统短暂卡顿了几分钟。有医生反映”病历打不开”,小马紧急回滚,发现是索引重建时锁表导致的。他调整方案:改用在线加索引工具,避免锁表。

第二天上缓存,出现了一个 bug:药品列表缓存更新不及时,新添加的药品在医生端看不到。药房主任投诉:”今天新到的阿莫西林,我怎么在系统里找不到?”

小马赶紧排查:缓存过期时间设成1小时,新药品需要等1小时才能在所有终端同步。他改为”主动刷新+短过期”:添加药品时,系统主动清除相关缓存,下次查询时重建。

第三天调整收费计算逻辑,又出幺蛾子:有个患者的费用明细算错了,多收了20元。原因是缓存的数据格式和计算逻辑不一致。老林加班到凌晨2点,修复了数据转换的 bug。

小马总结了:”性能优化就像做手术,不能急,要一步步来。每动一刀,都要看病人反应。”

两周后,所有优化上线完成。小马在门诊大厅贴出告示:欢迎大家对系统速度进行”找茬”,发现问题及时反馈。

第三天,他拿到了第一组正式数据:

指标 优化前 优化后 提升幅度
病历查询P95响应时间 4.2秒 0.3秒 -93%
药品列表加载 3.0秒 0.1秒 -97%
收费结算响应 2.8秒 0.6秒 -79%
高峰期并发支持 50用户 200用户 +300%
系统慢投诉(月均) 12起 1起 -92%

院长在科室大会上展示这组数据时,全场的目光从怀疑转为惊讶。

“上周我说了,如果系统再卡就停用。”院长说,”今天我要说的是,不仅不停用,还要推广经验。咱们县医院的优化效果,可以作为系统在基层应用的典型案例。”

“最关键的是,”院长顿了顿,”患者投诉’系统慢’这几天几乎没了。收费窗口、药房、诊室,各个部门都反映流程顺畅了。”

一位老医生站起来说:”以前打开病历要等好几秒,现在点下去结果就出来了。这个感受最直接。”

小马坐在角落,松了一口气。

价格问题,院长在总结会上主动提了。

“这次优化是软佳的工程师免费做的,包含在服务里。”院长说,”但我想算一笔账:如果我们县医院一年需要这样的深度优化2次,每次单独请外部团队,费用大概在5-8万元。而我们软佳系统的年费是多少?

“1898元。

“你说便宜不便宜?这1898元,不仅是买一套系统,还包括持续的技术支持、性能优化、安全保障。换做是你们,这笔账怎么算?”

台下有人开始点头。

一位来自邻县的参会代表问:”你们这个系统,会不会用久了又变慢?”

小马回答:”软佳每周都会发布优化补丁,发现问题48小时内响应。而且我们有性能监控平台,可以提前发现潜在问题,主动优化。这不是一次性工程,是持续服务。”

那位邻县代表记了下来。

三个月后,小马回访这家县医院,发现系统依然流畅。他询问IT管理员小陈:”最近还有投诉说慢吗?”

小陈笑了:”上个月只有1起,是因为那位患者用的旧手机,浏览器卡顿。系统本身一点问题没有。”

更让小马欣慰的是,医院信息科的态度变了。过去他们只管”系统能用就行”,现在开始主动关注性能指标,每周看监控报表,发现异常立即上报。

“你们的教育起作用了。”小陈说,”现在我们知道,性能不是玄学,是可以量化和优化的。”

小马想起那个凌晨3点被投诉电话惊醒的自己。那时他以为,系统卡顿是个无解难题——硬件条件有限,用户量增长,慢是必然。

但这次经历让他明白:性能问题往往不是资源不足,而是设计粗糙。很多所谓的”硬件不够”,其实是”软件不巧”。

软佳的定位不是卖一套软件,而是提供持续进化的服务。每一次投诉都是改进的机会,每一次慢查询都是优化的信号。

回昆明总部汇报时,老林对小马说:”你在县医院的这个案例,可以写成一篇技术博客,发到内部知识库。”

小马想了想,写下了三句话,后来成为软佳技术文化的核心:

“系统卡顿不是患者太多,是代码太懒。”

“每一个慢查询背后,都有一个等待的患者。”

“性能优化不是奢侈品,是门诊系统的生命线。”

声明:本文基于真实医院场景改编,人物均为化名,数据为试点统计,实际效果因机构规模、硬件配置、使用习惯而异。

核心金句:

“系统卡顿不是患者太多,是代码太懒。”

“每一个慢查询背后,都有一个等待的患者。”

“性能优化不是奢侈品,是门诊系统的生命线。”

互动话题:

您的门诊系统是否遇到过性能瓶颈?是如何定位和解决的?

如果系统响应速度提升一倍,对您的医护人员和患者意味着什么?

在系统选型时,您是否把性能指标作为核心评估项?


立即免费试用门诊系统https://app.kmhis.com/
International Versionhttps://app.kmhis.com/multi/
了解软佳门诊管理系统详情https://www.kmhis.com/outpatient-management-system.html


扫码预约

手机扫码试用患者预约。请勿输入个人真实信息(点击图片可查看原图)

支持8种语言:简体中文、繁体中文、香港中文、English、藏文、泰文、老挝语、越南语


说真的。这类问题我见过太多了。每次看到医院同事为选型头疼。我就想,要是早点有人把这些经验分享出来就好了。毕竟。选择不对。后面全是麻烦。选择对了。省心省力。还能提升整个机构的运行效率。希望这篇能帮到正在纠结的你。

你如果有具体需求。也可以去 www.kmhis.com 看看。那里有更详细的技术方案和案例。

两千张表,三百万病人:一场没有”撤销”按钮的迁移

“如果现在停止迁移,数据会不一致,永远回不去了。”

凌晨两点,XX医院数据中心。老周盯着屏幕上的进度条,手在发抖。

迁移进度:87%。

总数据量:2.3 TB。

Tables 数量:2176张。

涉及的核心业务:三百万病人的历史病历、五年门诊记录、三年住院档案。

如果失败,后果不堪设想。

但迁移已经开始,没有”撤销”按钮。

1. 为什么这个迁移这么难?

这次迁移,不是简单的”升版本”,而是从旧架构V3.0,迁移到新架构V4.0

两个架构的区别:

– V3.0是单体数据库,所有业务数据在一张库

– V4.0是微服务架构,业务数据分库分表:门诊库、住院库、药房库、财务库、病历库…

以前的迁移,只需要在同一个数据库里改表结构,数据不动——这次,要把数据从”一张大饼”拆成”五块小饼”,还要保证每块小饼都能重新拼回原来的样子(如果失败回滚)。

难点:

1. 数据拆分逻辑复杂:比如门诊缴费记录,原来在payment表里,现在要拆成paymentheader(支付头)和paymentitems(支付明细);还要关联到outpatient_visit(门诊就诊)表。拆分规则涉及六张表。

2. 历史数据质量堪忧:三年积累的数据,有很多”脏数据”——重复记录、缺失字段、编码错误(比如性别填了”未知”),这些在V3.0时代都容忍了,但V4.0的schema有严格约束,脏数据会导入失败。

3. 没有”试错”机会:迁移窗口只有两天(五一假期门诊量少)。两次迁移机会——第一次失败,第二次必须在12小时内完成,否则影响初二开诊。如果两次都失败,就只好延期,等着杨院长问责。

老周带人准备了三个月:

– 写迁移工具(自己开发的data-migrator

– 清洗脏数据脚本

– 回滚方案

– 全量演练三次,每次都发现问题,每次都改,第三次演练才成功

但演练再成功,也不是真迁移。

2. 迁移开始后,第一个坑:脏数据

晚上八点,迁移开始。

前两个小时顺利:系统库、用户表、权限表…都是一马平川。

十点,开始迁移核心业务数据。

payment表开始迁移,1%…2%…

突然,报错。

“`
ERROR: Violation of NOT NULL constraint: column ‘patient_id’ cannot be null
“`

日志里指明,有一条记录的patient_id是NULL。

这是脏数据。

老周让小吴排查:SELECT COUNT(*) FROM payment WHERE patient_id IS NULL

结果:73条。

这些记录,都是V3.0时代的老数据,可能是创建记录时系统bug,patient_id没填。

小吴说:”跳过这73条吧,不影响整体。”

“不行。”老周说,”如果跳过,对账的时候会发现门诊对不上。而且,如果这73条都是大额缴费,财务损失谁负责?”

他们做了个决定:现场清洗

写了一条UPDATE语句,试图从其他表关联补全patientid。但关联发现,这73条记录对应的visitid也缺失,无法追溯到具体是哪次就诊。

死循环。

“只能手工造一个patient_id了。”小吴说,”造一个虚拟患者,把这73条付款挂到他名下。等迁移完成,我们在新系统里加一个’未知患者’账户,把这些数据放进去,后续再处理。”

老周犹豫。虚拟数据虽然能过关,但数据准确性打了折扣。

“有没有其他办法?”

“或者,我们暂停迁移,先回滚,把脏数据彻底清理完再迁?”

回滚意味着放弃这次窗口,五一假期只剩一天了,不够。

时间不等人。

老周咬了咬牙:”现场清洗——把有问题的数据,标上’待处理’标签,迁过去后我们在新系统里专门建一个’脏数据沙箱’,隔离存放。”

这是妥协,但迁移不能停。

3. 第二个坑:数据不一致

凌晨一点,进度到63%。

小吴发现一个问题:visitdate字段,在V3.0里是datetime类型,V4.0里拆分成visitdate(日期)和visit_time(时间)。迁移工具把小吴写得有bug:在拆分日期和时间时,时区处理错了。

V3.0存储的是本地时间(东八区),迁移工具当成UTC时间处理,减了8小时。

结果:所有就诊时间的visit_time,都比实际时间晚8小时。

比如一次早上8点的就诊,迁过去后变成了凌晨0点。

“天呐…”小吴脸白了。

老周也傻了。

这不是小问题。时间错误,会影响排班、统计、甚至医保结算(医保要求精确到小时)。

“修复这个bug,但已经迁过去的数据怎么处理?”

更可怕的是:已经迁了63%的数据,现在发现一个重大bug,是继续迁(错上加错),还是回滚?

继续,所有数据都错,无法挽回。

回滚,63%的数据要清理,重新迁,时间不够。

老周深吸一口气:”调出这个bug的影响范围数据。我们现场修复——迁过去的63%,我们另写一个’修正脚本’,把时间加8小时。”

小吴心算了一下:数据量800万条,修正脚本跑一遍要2小时。

“时间够吗?”

“不够也要够。”老周说。

4. “修正脚本”成为赛跑

老周和团队吃了两片咖啡因,开始写修正脚本。

脚本逻辑很简单:

“`sql
UPDATE outpatient_visits
SET visit_time = DATEADD(hour, 8, visit_time)
WHERE visit_time IS NOT NULL
“`

但要跑800万行,必须在2小时内完成,否则夜深了,医院的业务开始恢复,没机会再改。

他们优化:

1. 分批更新,每次10万行,commit 后继续

2. 加索引:在visit_time上建临时索引,加速 update

3. 关掉binlog,减少IO

4. 调大innodbbufferpool_size,确保数据在内存里

脚本跑起来,每分钟更新12万行。

一小时,600万。

凌晨三点,修正完成。

迁移继续。

5. 最后一个坑:外键约束冲突

早上七点,进度97%。

只剩最后一批数据迁移:prescription(处方)表。

报错:

“`
ERROR: Cannot add or update a child row: a foreign key constraint fails (`prescription` constraint `fk_prescription_visit`)
“`

意思是:有一条prescription记录,引用的visitid,在outpatientvisit表里找不到。

脏数据 again。

但这次很奇怪:前96%的数据都关联成功,为什么最后3%会丢?

小吴排查:最后这批数据,是2024年12月31日跨年的那批。那几天系统做了一次数据归档——把半年前的记录移到历史库。

但归档工具可能有bug,把某些visit_id漏了。

“跳过吧,”小吴说,”就几条处方,影响不大。”

“不行。”老周说,”处方是核心业务,漏一条,病用药记录就不全。而且,这是系统性问题的体现——如果这里漏了,其他地方呢?”

他们决定:现场补数据

方法:从旧库(V3.0)里,把这批visit_id对应的记录,手动补出来,再导入新库。

旧库还没关,可以查。

但旧库是生产环境,不能直接操作。他们只能查,不能改。

查询:SELECT * FROM outpatientvisit WHERE visitid IN (xxx, yyy, zzz)

发现这三条visitid对应的记录,已经被归档到outpatientvisit_history表了。

迁移工具没考虑到这种情况——只迁了主表,没迁历史表,导致引用断裂。

小吴把这些历史记录也迁过去,但迁到outpatient_visit主表(违反了业务逻辑,历史记录不应该混在主表里)。

“标记为历史记录。”老周说。

6. 100%完成后,还有验证

早上八点,迁移工具显示:100%。

所有人松了一口气。

但老周没放松:”迁移完成,不算完成;数据验证通过,才算完成。”

他们有一套验证流程:

1. 行数对比:每张表的记录数,新库 vs 旧库,差异率<0.1%

2. 总和校验:对金额、数量等关键字段,做SUM对比,应该相等

3. 样本抽查:随机抽取1000条记录,逐字段对比,应该一致

4. 业务逻辑验证:跑一遍核心业务流程(挂号→开处方→缴费),结果应该一致

前三个通过,第四个出问题。

模拟一次门诊全流程:挂一个号,开三个药,缴费。

在V4.0里,挂号的visitid,和处方的visitid,对不上。

又一轮排查发现:visit表的id字段是自增的,迁移过程中,新库的自增起点没设置对,导致新生成的ID和旧的不一样。但prescription表里的visit_id是直接迁过来的(旧的ID值),而新挂号的ID是新产生的(新的自增值),两者当然对不上。

“这是一个’活数据’问题,不是迁移问题。”小吴说。

老周明白了:迁移只迁了历史数据,但迁移完成后,新产生的数据用的ID和旧数据不连续。这会影响对账、追溯等需要全局ID唯一性的场景。

解决的方案:重置自增ID的起点,让它从旧库的最大ID+1开始。

但问题是:迁移后已经产生了一条新挂号记录(验证用的),ID是1。重置起点后,这条记录的ID会和后面的冲突。

只能删除这条验证数据,重置ID,再重新验证一次。

折腾到中午十二点,全部通过。

7. 事后反思:我们做对了什么?

这次迁移后,老周写了长篇复盘。

他的结论:

1. “现场清洗”是必须的能力

– 不要指望数据100%干净再迁

– 要能在迁移过程中,实时发现脏数据,实时处理(跳过、修正、隔离)

2. 修正脚本应该提前准备好

– 不是所有bug都能在迁移前发现

– 为每一类可能的数据问题,提前写好”修正脚本模板”,迁移时填参数就能跑

3. 验证必须自动化

– 人工抽查不够,要有程序自动跑完整的数据验证流程

– 验证通过率应该>99.99%

4. 要有”回滚点”概念

– 每完成一个业务单元(如门诊库),就做一个”回滚点”

– 后面的阶段失败,可以回滚到这个点,而不是全部重来

5. “迁移”不只是”搬数据”

– 还包括:ID生成策略、自增主键连续性、时间戳时区、字符集转换…

– 任何细节出错,都会导致业务逻辑错误

互动话题

你经历过最复杂的数据迁移是什么?有什么经验教训?

> 基于真实医院场景改编,人物均为化名


立即免费试用门诊系统https://app.kmhis.com/
International Versionhttps://app.kmhis.com/multi/
了解软佳门诊管理系统详情https://www.kmhis.com/outpatient-management-system.html


扫码预约

手机扫码试用患者预约。请勿输入个人真实信息(点击图片可查看原图)

支持8种语言:简体中文、繁体中文、香港中文、English、藏文、泰文、老挝语、越南语


说真的。这类问题我见过太多了。每次看到医院同事为选型头疼。我就想,要是早点有人把这些经验分享出来就好了。毕竟。选择不对。后面全是麻烦。选择对了。省心省力。还能提升整个机构的运行效率。希望这篇能帮到正在纠结的你。

你如果有具体需求。也可以去 www.kmhis.com 看看。那里有更详细的技术方案和案例。