我们目前正在使用 .NET 8 和 EF Core 8。
我们用来计算结果并对其进行分页的查询遇到了时间问题。我们从一个需要 40 秒执行的查询开始,我们能够将 40 秒的时间减少到 4 秒,将 Azure SQL 弹性池中每个数据库的 e-DTU 从 5 个增加到 50 个
我们有一个事件系统,其中包含操作表、事件表(该表用于存储所有模块的事件)和声明(我们查询的表)。 Action 有 20 行,Event 有 640.338 行,Claim 有 62.000 行。
这就是表格的样子。
索赔和事件之间的关系是软的,因为事件表也用于其他表,它们使用列 ReferenceId 进行关联。
这就是 C# 代码的样子(工具生成此代码)
var query = Context.Set<ClaimEntity>().AsQueryable();
query = query.Where(x => x.Events.AsQueryable().OrderByDescending(e => e.CreatedOn).Select(e => e.Action.Code).FirstOrDefault() == "CREATE_CLAIM");
var total = query.Count();
这是它发送到 SQL Server 的查询
SELECT COUNT(*)
FROM [dc].[Claim] AS [c]
WHERE
(
SELECT TOP(1) [a].[Code]
FROM [dc].[Event] AS [e]
INNER JOIN [dc].[Action] AS [a] ON [e].[ActionId] = [a].[Id]
WHERE [c].[Id] = [e].[ReferenceId]
ORDER BY [e].[CreatedOn] DESC
) = N'CREATE_CLAIM'
AND
[c].[DeletedOn] IS NULL
我们尝试使用 SSMS 数据库引擎优化顾问,它提供了此优化选项。
我们尝试了这个,但是执行时间还是4秒
CREATE NONCLUSTERED INDEX [_dta_index_Event_19_34099162__K3_K2_K10] ON [dc].[Event]
(
[ReferenceId] ASC,
[ActionId] ASC,
[CreatedOn] ASC
)WITH (SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF) ON [PRIMARY]
这样的数据结构效率很低,因为您想根据与代码匹配的最新事件来计算索赔。正确的索引将有助于确保数据库能够尽快处理这些查询,但也会有限制,因为它确实需要“接触”很多值。
这是考虑反规范化的一个好例子,以引入存储在声明级别或作为最近事件的标志存储的“当前”代码。这两种解决方案都涉及创建“健康检查”,以定期查找并报告任何数据不一致的情况。然后可以更有效地对其进行索引。我还会对这样的当前代码使用数字枚举而不是字符串,这样索引更轻松。
例如,如果我们在 ClaimEntity 上添加 CurrentStatus:
public enum ClaimStatus
{
None = 0,
Created,
//...
}
在 ClaimEntity 上添加/更新事件的行为可以封装在实体中,因此该方法是创建新事件的唯一方法,可以通过“ClaimEntity.AddEvent(code, ...)”之类的方法来完成。该方法验证所需的事件详细信息,创建事件并将其添加到 ClaimEntity.Events 集合中,并更新声明的当前状态。通过这种方式,获取“Created”事件的查询简化为:
var query = Context.Set<ClaimEntity>().AsQueryable();
query = query.Where(x => x.CurrentStatus == ClaimStatus.Created);
var total = query.Count();
健康检查可以是后台进程,也可以是数据库本身运行的进程,用于根据最新状态查询声明,查找任何不匹配的情况。如果发现任何问题,可以对其进行纠正,但应报告给开发团队,以追踪如何允许它们不同步。
另一种方法是向事件添加类似“IsCurrent”标志的内容,其中将事件添加到声明的行为会将任何/所有先前的 IsCurrent 标志设置为
False
,然后再添加 IsCurrent 设置为 的新行True
。同样,这需要进行健康检查来搜索任何存在多个 IsCurrent == True 集或没有 IsCurrent == True 集的声明。 (存在事件的地方)这里的查询将简化为:
var query = Context.Set<ClaimEntity>().AsQueryable();
query = query.Where(x => x.Events.FirstOrDefault(e => e.IsCurrent).Code == "CREATE_CLAIM");
var total = query.Count();
这避免了需要对每个索赔的整个事件进行排序。同样,IsCurrent 标志需要适当索引。