文章 John Pan · 6 分鐘 前 6m read

Health Connect 中的高性能消息搜索

Health Connect 中的高性能消息搜索

问题

您是否曾在繁忙的界面中尝试使用“消息查看器”进行搜索,却因查询超时而失败?随着数据量的增加,这可能会成为一个相当严重的问题。举个例子,我正在处理的 Health Connect 实例每天处理约 1.55 亿条消息头,且消息保留期为 21 天。 为提升搜索性能,我们扩展了内置的 SearchTable,添加了常用字段,希望通过为这些字段建立索引来加快查询速度。尽管如此,某些查询仍然无法完成。

更多信息:定义搜索表类。

image

对于我们这些从事 HL7 集成工作的人员来说,我们深知日常的故障排查和问题响应是我们工作的重要组成部分。 快速识别并解决这些问题,对于确保数据流的稳定和准确至关重要。由于搜索性能不佳,我们不得不针对每个具体问题收集非常详细的信息,才能在 Health Connect 中找到相关示例。 如果没有非常严格的时间限制(几分钟内),我们往往无法在超时之前让“消息查看器”搜索返回任何结果。这导致在确定实际问题以及后续解决问题方面都出现了显著延迟。

解决方案

这种情况是不可接受的,因此我们必须找到解决方案,以更好地服务客户。WRC应运而生。

image

我们提交了一份工单,我很高兴地报告,已经找到了修复方案。该方案需要遍历我们自定义的 SearchTable 字段,并识别出哪些字段的唯一性不足,因此不应被“消息查看器”搜索功能作为索引处理。

例如,在我们的环境中,MSH.4(或 FacilityID)用于标识 HL7 消息所属的实体医院地点。虽然为了便于筛选,将该字段包含在 SearchTable 中很重要,但每天每家医疗机构都会接收大量消息。 这意味着将该字段用作“消息查看器”搜索的索引效率较低。与此相反的是 PID.18(或患者账号)这类字段。由于与每个账号关联的消息数量极少,因此将该字段用作索引能显著提高搜索速度。

在相应项目中添加 Unselective 参数,可告知消息搜索应将哪些字段视为索引。这实质上修改了用于提取消息的 SQL 语句。下文将展示基于字段是否用作索引而产生的查询差异,以及如何利用这些查询来确定哪些字段应设置为 Unselective。

带索引与不带索引的查询

Unselective="false"(已建立索引)的 SQL 查询计划

image

该查询会遍历 SearchTable 中的值,并对每一行与 MessageHeader 表进行交叉引用。对于唯一且关联消息较少的值(例如患者账号),这种方式更为高效。

Unselective="true"(%NOINDEX)SQL 查询计划

image

该查询遍历 MessageHeader 表,并对每一行与 SearchTable 中的值进行交叉引用。对于关联结果较多的值(例如 FacilityID),此方法返回结果的速度更快。

如何识别问题字段

据我观察,识别哪些字段需要标记为 Unselective 的最佳方法是使用 Show Query。 在“消息查看器”中使用每个字段分别创建一个搜索(通过 Add Criterion 添加 SearchTable 字段),然后点击 Show Query,即可查看“消息查看器”根据所选过滤条件提取消息时实际使用的 SQL 语句。

image

我们的第一个示例使用了“搜索表”中一个未添加“未选中”参数的字段。请注意 EnsLib_HL7.SearchTable.PropId = 19 和 EnsLib_HL7.SearchTable.PropValue = '2009036'。这分别表示作为筛选条件添加的“搜索表”字段以及正在检查的值。 请注意,ProdId 对于每个搜索表字段都是唯一的,并且可能会因环境而异。

image

如何在消息查看器中添加“显示查询”功能

如果“消息查看器”中未启用“显示查询”按钮,您可以在指定的命名空间中设置以下全局配置。

set ^Ens.Debug("UtilEnsMessages","sql")=1

查看消息查看器使用的 SQL 查询

SQL - Unselective="false"

SELECT TOP 100 
head.ID As ID, {fn RIGHT(%EXTERNAL(head.TimeCreated),999 )} As TimeCreated, 
head.SessionId As Session, head.Status As Status, 
CASE head.IsError 
WHEN 1 
THEN 'Error' ELSE 'OK' END As Error, head.SourceConfigName As Source, 
head.TargetConfigName As Target, head.SourceConfigName, head.TargetConfigName, 
head.MessageBodyClassName As BodyClassname, 
(SELECT LIST(PropValue) 
FROM EnsLib_HL7.SearchTable 
WHERE (head.MessageBodyId = DocId) And PropId=19) As SchTbl_FacilityID, 
EnsLib_HL7.SearchTable.PropId As SchTbl_PropId 
FROM Ens.MessageHeader head, EnsLib_HL7.SearchTable 
WHERE (((head.SourceConfigName = 'component_name' OR head.TargetConfigName = 'component_name')) 
AND head.MessageBodyClassName=(('EnsLib.HL7.Message')) 
AND (head.MessageBodyId = EnsLib_HL7.SearchTable.DocId) 
AND EnsLib_HL7.SearchTable.PropId = 19 AND EnsLib_HL7.SearchTable.PropValue = '2009036') 
ORDER BY head.ID Desc

接下来,将该查询复制到 SQL 中,并手动修改以添加 %NOINDEX。这正是指示查询不要将该值视为索引的设置。

SQL - Unselective="true" - %NOINDEX

SELECT TOP 100 
head.ID As ID, {fn RIGHT(%EXTERNAL(head.TimeCreated),999 )} As TimeCreated, 
head.SessionId As Session, head.Status As Status, 
CASE head.IsError 
WHEN 1 
THEN 'Error' ELSE 'OK' END As Error, head.SourceConfigName As Source, 
head.TargetConfigName As Target, head.SourceConfigName, head.TargetConfigName, 
head.MessageBodyClassName As BodyClassname, 
(SELECT LIST(PropValue) 
FROM EnsLib_HL7.SearchTable 
WHERE (head.MessageBodyId = DocId) And PropId=19) As SchTbl_FacilityID, 
EnsLib_HL7.SearchTable.PropId As SchTbl_PropId 
FROM Ens.MessageHeader head, EnsLib_HL7.SearchTable 
WHERE (((head.SourceConfigName = 'component_name' OR head.TargetConfigName = 'component_name')) 
AND head.MessageBodyClassName='EnsLib.HL7.Message' 
AND (head.MessageBodyId = EnsLib_HL7.SearchTable.DocId) 
AND EnsLib_HL7.SearchTable.PropId = 19 AND %NOINDEX EnsLib_HL7.SearchTable.PropValue = (('2009036'))) 
ORDER BY head.ID Desc

如果第一个和第二个查询的返回时间存在显著差异,那么你就找到了需要修改的字段。在本例中,查询时间从几分钟后超时缩短到了几乎瞬间返回。

应用修复方案——修改代码

一旦确定了需要修复的字段,您就可以在自定义的 SearchTable 类中,为每个受影响的 Item 添加 Unselective="true"。请参见下例。

自定义 SearchTable 类

/// Custom HL7 Search Table adds additional fields to index.
Class CustomClasses.SearchTable.CustomSearchTable Extends EnsLib.HL7.SearchTable
{

XData SearchSpec [ XMLNamespace = "http://www.intersystems.com/EnsSearchTable" ]
{
<Items>
		// Increase performance by setting Unselective="true" on fields that are not highly unique.
		// This essentially tells Message Search to not use an index on these fields.

		// facility ID in MSH.4
   		<Item DocType=""	PropName="FacilityID" Unselective="true">		[MSH:4]		</Item>
   		// Event Reason in EVN.4
   		<Item DocType=""	PropName="EventReason" Unselective="true">		[EVN:4]		</Item>
   		// Patient Account (Add PV1.19 to prebuilt PID.18 search)
   		<Item DocType=""	PropName="PatientAcct">		[PV1:19]	</Item>
   		// Document type
   		<Item DocType=""	PropName="DocumentType" Unselective="true">	[TXA:2]		</Item>
   		// Placer Order ID
   		<Item DocType=""	PropName="PlacerOrderID">	[OBR:2.1]	</Item>
   		// Filler Order ID
   		<Item DocType=""	PropName="FillerOrderID">	[OBR:3.1]	</Item>
   		// Universal Service ID
   		<Item DocType=""	PropName="ServiceID" Unselective="true">		[OBR:4.1]	</Item>
   		// Universal Service ID
   		<Item DocType=""	PropName="ProcedureName" Unselective="true">	[OBR:4.2]	</Item>		
   		// Diagnostic Service Section
   		<Item DocType=""	PropName="ServiceSectID" Unselective="true">	[OBR:24]	</Item>
   		// Appointment ID
   		<Item DocType=""	PropName="AppointmentID">	[SCH:2]		</Item>
   		// Provider Fields
   		<Item DocType=""	PropName="ProviderNameMFN">	[STF:3()]		</Item>
   		<Item DocType=""	PropName="ProviderIDsMFN">	[MFE:4().1]		</Item>
   		<Item DocType=""	PropName="ProviderIDsMFN">	[STF:1().1]		</Item>
   		<Item DocType=""	PropName="ProviderIDsMFN">	[PRA:6().1]		</Item>
	</Items>
}

Storage Default
{
<Type>%Storage.Persistent</Type>
}

}

总结

快速消息搜索对于日常集成操作至关重要。通过利用 Unselective 属性,即使数据库规模不断扩大,现在也能保持这一功能。借助这一快速且易于实施的更改,您将能够重回正轨,在 Health Connect 环境中自信地提供服务并排查问题。