性能建模

在许多情况下,建模的方式可能对应用程序的性能产生深远的影响;尽管适当规范化且“正确”的模型通常是一个很好的起点,但在实际应用程序中,一些实用的折衷方案可以很好地实现良好的性能。 由于一旦应用程序在生产环境中运行就很难更改模型,因此有必要在创建初始模型时注意性能。

非规范化和缓存

非规范化是将冗余数据添加到架构的做法,通常是为了在查询时消除联接。 例如,对于包含博客和帖子的模型,其中每篇帖子都有相应的评分,你可能需要频繁显示博客的平均评分。 对此的简单方法是按博客对帖子进行分组,并将平均值计算作为查询的一部分;但这需要在两个表之间进行成本高昂的联接。 非规范化方法会将计算出的所有帖子的平均值添加到博客上的一个新列,使其可以立即访问,而无需联接或计算。

上述内容可以看作是一种缓存形式,也就是将来自帖子的聚合信息缓存在博客上;与任何缓存一样,问题在于如何使已缓存的值与正在缓存的数据保持同步。 在许多情况下,已缓存的数据可以稍微滞后一点;例如,在上述示例中,博客的平均评分在任何给定点上并未完全同步,这通常是合理的。 在这种情况下,可以时不时地重新计算;否则,必须设置一个更复杂的系统,使已缓存的值保持同步。

下面详细介绍 EF Core 中的一些非规范化和缓存方法,并指向文档中的相关部分。

存储的计算列

如果要缓存的数据是同一个表中其他列的乘积,存储的计算列可能是一个理想的解决方案。 例如,Customer 可能包含 FirstNameLastName 列,但可能需要按客户的全名进行搜索。 存储的计算列由数据库自动维护(只要该行发生更改,数据库就会重新计算),甚至你可为其定义索引以加快查询速度。

输入更改时更新缓存列

如果缓存的列需要从表行之外引用输入,则不能使用计算列。 但是,只要列的输入发生更改,仍然可以重新计算该列;例如,可以在每次更改、添加或删除帖子时重新计算博客的平均评分。 请务必明确需要重新计算时的确切条件,否则已缓存的值将不同步。

实现此目的的一种方法是,通过常规的 EF Core API 自行执行更新。 SaveChanges事件侦听器可用于自动检查是否有任何帖子正在更新,并以该方法执行重新计算。 请注意,这通常需要额外的数据库往返,因为必须发送额外命令。

对于对性能更加敏感的应用程序,可以定义数据库触发器以自动在数据库中执行重新计算。 这可减少额外的数据库往返操作,自动与主更新发生在同一事务中,并且更易于设置。 EF 不提供任何特定的 API 来创建或维护触发器,但完全可以通过原始 SQL 创建空迁移并添加触发器定义

具体化视图/索引视图

具体化视图(也称为索引视图)与常规视图类似,区别是前者的数据存储在磁盘上(“具体化”),而不是在每次查询视图时进行计算。 这类视图在概念上类似于存储的计算列,因为它们缓存潜在昂贵计算的结果;但是,它们缓存的是整个查询的结果集,而不是单个列。 可以像任何常规表一样查询具体化视图,并且由于查询缓存在磁盘上,这类查询能非常快速地执行且成本低廉,而无需对定义视图的查询执行成本高昂的计算。

对具体化视图的具体支持因数据库而异。 在某些数据库中(例如 PostgreSQL),必须手动刷新具体化视图,以便其值与其基础表同步。 这通常通过计时器(在可接受一些数据滞后的情况下)或在特定条件下通过触发器或存储过程调用来完成。 另一方面,SQL Server 索引视图会在其基础表修改时自动更新;这可确保视图始终显示最新的数据,代价是更新速度变慢。 此外,SQL Server 索引视图对它们支持的内容有各种限制。有关详细信息,请参阅文档

EF 目前不提供任何特定的 API 来创建或维护视图(具体化/索引视图或其他视图),但完全可以通过原始 SQL 创建空迁移并添加视图定义

继承映射

建议在继续本部分之前,先阅读继承专用页面

EF Core 目前支持使用三种方法将继承模型映射到关系数据库:

  • 每个层次结构一张表 (TPH),其中类的整个 .NET 层次结构会映射到单个数据库表。
  • 每个类型一张表 (TPT),其中 .NET 层次结构中的每个类型都映射到数据库中的不同表。
  • 每个具体类型一张表 (TPC),其中 .NET 层次结构中的每个具体类型会映射到数据库中的一个不同的表,其中每个表包含相应类型所有属性的列。

继承映射技术的选择可能会对应用程序性能产生相当大的影响,建议在做出选择之前仔细衡量。

直观地说,TPT 可能看起来像是“更清洁”的技术;通过每个 .NET 类型一个单独表,数据库架构看起来类似于 .NET 类型层次结构。 此外,因为 TPH 必须在单个表中表示整个层次结构,所以无论行中实际保存的类型如何,行都具有所有列,并且不相关的列始终为空且不使用。 除了看似是一种“不简洁”的映射技术以外,很多人认为这些空列在数据库中占用的空间很大,并且可能会影响性能。

提示

如果数据库系统支持它(例如 SQL Server),请考虑对很少填充的 TPH 列使用“稀疏列”。

然而,测量表明,从性能角度来看,TPT 在大多数情况下是较差的映射技术;如果 TPH 中的所有数据都来自单个表,TPT 查询必须将多个表联接在一起,而联接是关系数据库中性能问题的主要原因之一。 数据库通常可以很好地处理空列,而 SQL Server 稀疏列等功能可以更进一步地减少此类开销。

TPC 的性能特征与 TPH 相似,但在选择所有类型的实体时速度略慢,因为这涉及到多个表。 但在查询单个叶类型的实体时,TPC 确实非常出色 - 查询仅使用单个表,并且无需筛选。

有关具体示例,请参阅此基准,该基准设置一个具有 7 类层次结构的简单模型;每种类型设定 5000 行种子(总计 35000 行),而基准仅从数据库加载所有行:

方法 平均值 错误 标准偏差 Gen 0 Gen 1 已分配
TPH 149.0 毫秒 3.38 毫秒 9.80 毫秒 4000.0000 1000.0000 40 MB
TPT 312.9 毫秒 6.17 毫秒 10.81 毫秒 9000.0000 3000.0000 75 MB
TPC 158.2 毫秒 3.24 毫秒 8.88 毫秒 5000.0000 2000.0000 46 MB

可以看出,对于此方案,TPH 和 TPC 的效率显著高于 TPT。 请注意,实际结果始终取决于所执行的特定查询和层次结构中的表数,因此其他查询可能会显示不同的性能差距;建议使用此基准代码作为模板来测试其他查询。