性能建模
在许多情况下,建模的方式可能对应用程序的性能产生深远的影响;尽管适当规范化且“正确”的模型通常是一个很好的起点,但在实际应用程序中,一些实用的折衷方案可以很好地实现良好的性能。 由于一旦应用程序在生产环境中运行就很难更改模型,因此有必要在创建初始模型时注意性能。
非规范化和缓存
非规范化是将冗余数据添加到架构的做法,通常是为了在查询时消除联接。 例如,对于包含博客和帖子的模型,其中每篇帖子都有相应的评分,你可能需要频繁显示博客的平均评分。 对此的简单方法是按博客对帖子进行分组,并将平均值计算作为查询的一部分;但这需要在两个表之间进行成本高昂的联接。 非规范化方法会将计算出的所有帖子的平均值添加到博客上的一个新列,使其可以立即访问,而无需联接或计算。
上述内容可以看作是一种缓存形式,也就是将来自帖子的聚合信息缓存在博客上;与任何缓存一样,问题在于如何使已缓存的值与正在缓存的数据保持同步。 在许多情况下,已缓存的数据可以稍微滞后一点;例如,在上述示例中,博客的平均评分在任何给定点上并未完全同步,这通常是合理的。 在这种情况下,可以时不时地重新计算;否则,必须设置一个更复杂的系统,使已缓存的值保持同步。
下面详细介绍 EF Core 中的一些非规范化和缓存方法,并指向文档中的相关部分。
存储的计算列
如果要缓存的数据是同一个表中其他列的乘积,存储的计算列可能是一个理想的解决方案。 例如, Customer
可能具有 FirstName
和 LastName
列,但我们可能需要按客户的 全名进行搜索。 存储的计算列由数据库自动维护(只要该行发生更改,数据库就会重新计算),甚至你可为其定义索引以加快查询速度。
输入更改时更新缓存列
如果缓存的列需要从表行之外引用输入,则不能使用计算列。 但是,只要列的输入发生更改,仍然可以重新计算该列;例如,可以在每次更改、添加或删除帖子时重新计算博客的平均评分。 请务必明确需要重新计算时的确切条件,否则已缓存的值将不同步。
实现此目的的一种方法是,通过常规的 EF Core API 自行执行更新。 SaveChanges
事件或侦听器可用于自动检查是否有任何帖子正在更新,并以该方法执行重新计算。 请注意,这通常需要额外的数据库往返,因为必须发送额外命令。
对于对性能更加敏感的应用程序,可以定义数据库触发器以自动在数据库中执行重新计算。 这可减少额外的数据库往返操作,自动与主更新发生在同一事务中,并且更易于设置。 EF 不提供任何特定的 API 来创建或维护触发器,但完全可以通过原始 SQL 创建空迁移并添加触发器定义。
具体化视图
具体化视图与常规视图类似,不同之处在于前者的数据存储在磁盘上(“具体化”),而不是在每次查询视图时进行计算。 如果不希望只是向现有数据库添加单个缓存列,而希望缓存复杂且开销较高的查询结果的整个结果集(就像它是常规表一样),则此工具非常有用。然后可以以非常低的费用查询这些结果,而无需进行任何计算或联接。 与计算列不同,具体化视图在其基础表发生更改时不会自动更新,必须手动刷新。 如果缓存的数据可能滞后,可通过计时器来刷新视图;另一种方法是设置数据库触发器,以在某些数据库事件发生后查看具体化视图。
EF 目前不提供任何特定的 API 来创建或维护视图、具体化视图或其他视图,但完全可以通过原始 SQL 创建空迁移并添加视图定义。
继承映射
建议在继续本部分之前,先阅读继承专用页面。
EF Core 目前支持将继承模型映射到关系数据库的三种技术:
- 每个层次结构的表 (TPH) ,其中类的整个 .NET 层次结构映射到单个数据库表。
- 每个类型一张表 (TPT),其中 .NET 层次结构中的每个类型都映射到数据库中的不同表。
- table-per-concrete-type (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 更高效。 请注意,实际结果始终取决于所执行的特定查询和层次结构中的表数,因此其他查询可能会显示不同的性能差距;建议使用此基准代码作为模板来测试其他查询。