Entity Framework,1 个额外包含会显着改变响应时间
Entity Framework, 1 extra include drasticly changes response time
情况
我目前正在 ASP .NET Core 中的 Web API 上工作。为此,我使用 Entity Framework 进行数据库相关操作。此 API return 有关游戏碧蓝航线的信息。我的一个控制器用于检索有关“船舶”的信息。一艘船是一个非常大的物体,需要包含很多东西。下面您将看到我如何检索包含所有链接信息的船舶集。最近,我向名为“ShipQuotes”的所谓“Ship”对象添加了一个新的 属性,它是一个小对象列表(Id + 4 字符串属性)。当然我也必须包括这个,否则 API 会 return 一个空列表。
await _context.Ships
.Include(s => s.Stars)
.Include(s => s.Skins)
.Include(s => s.Skills)
.Include(s => s.LimitBreaks)
.Include(s => s.Gallery)
.Include(s => s.EquippableSlots)
.Include(s => s.Quotes)
.Include(s => s.BaseStats)
.Include(s => s.Level100Stats)
.Include(s => s.Level120Stats)
.Include(s => s.Level100RetrofitStats)
.Include(s => s.Level120RetrofitStats)
.Include(s => s.EnhanceValue)
.Include(s => s.ScrapValue)
.Include(s => s.Construction)
.Include(s => s.Construction.Availability)
.Include(s => s.Artist)
.Include(s => s.Pixiv)
.Include(s => s.Twitter)
.Include(s => s.Web)
.Include(s => s.VoiceActor)
.SingleAsync(ship => ship.Name == name);
问题(?)
我不确定这是否是个问题,但我不知道为什么会这样,但是;在我添加“.Include(s => s.Quotes)”之后,我的 API 的响应时间急剧下降。响应平均为 25kb,所以我认为这与我的 internet/network 没有任何关系(数据库托管在与 Web api 相同的网络上)
一个示例响应,其中还加载了报价大约需要 30 秒
如果我们从对象中排除“引号”属性,它将在 1 秒内加载
一艘船平均有 30 个类似这样的报价:
{
"id": "057404ac-ae41-494f-9825-f4fedbd61cd4",
"skin": "Default Skin",
"event": "Task",
"audioUrl": "---------- EXTERNAL LINK REMOVED FOR Whosebug POST ----------",
"eN_Transcription": "We still have missions that haven't been completed. You should check on their progress.",
"jP_Transcription": "ミッションがまだ終わっていないぞ。進捗を確認したほうがいい",
"cN_Transcription": "还有未完成的任务哦,还是确认下进度比较好"
}
这是船模(在swagger上看到的)
舰船模型
{
"shipId": "string",
"name": "string",
"rarity": "string",
"stars": {
"stars": "string",
"count": 0
},
"nation": "string",
"type": "string",
"thumbnailImage": "string",
"skins": [
{
"name": "string",
"imageUrl": "string",
"backgroundUrl": "string",
"chibiUrl": "string",
"live2dModel": true,
"obtainedFrom": "string"
}
],
"skills": [
{
"iconUrl": "string",
"name": "string",
"description": "string",
"color": "string"
}
],
"limitBreaks": [
{
"limitBreaks": [
"string"
]
}
],
"gallery": [
{
"description": "string",
"url": "string"
}
],
"equippableSlots": [
{
"maxEfficiency": 0,
"minEfficiency": 0,
"type": "string",
"max": 0
}
],
"quotes": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"skin": "string",
"event": "string",
"audioUrl": "string",
"eN_Transcription": "string",
"jP_Transcription": "string",
"cN_Transcription": "string"
}
],
"baseStats": {
"luck": 0,
"armor": "string",
"speed": 0,
"health": 0,
"firepower": 0,
"antiAir": 0,
"torpedo": 0,
"evasion": 0,
"aviation": 0,
"oilConsumption": 0,
"reload": 0,
"antiSubmarine": 0,
"oxygen": 0,
"ammunition": 0,
"accuracy": 0,
"huntingRange": "string"
},
"level100Stats": {
"luck": 0,
"armor": "string",
"speed": 0,
"health": 0,
"firepower": 0,
"antiAir": 0,
"torpedo": 0,
"evasion": 0,
"aviation": 0,
"oilConsumption": 0,
"reload": 0,
"antiSubmarine": 0,
"oxygen": 0,
"ammunition": 0,
"accuracy": 0,
"huntingRange": "string"
},
"level100RetrofitStats": {
"luck": 0,
"armor": "string",
"speed": 0,
"health": 0,
"firepower": 0,
"antiAir": 0,
"torpedo": 0,
"evasion": 0,
"aviation": 0,
"oilConsumption": 0,
"reload": 0,
"antiSubmarine": 0,
"oxygen": 0,
"ammunition": 0,
"accuracy": 0,
"huntingRange": "string"
},
"level120Stats": {
"luck": 0,
"armor": "string",
"speed": 0,
"health": 0,
"firepower": 0,
"antiAir": 0,
"torpedo": 0,
"evasion": 0,
"aviation": 0,
"oilConsumption": 0,
"reload": 0,
"antiSubmarine": 0,
"oxygen": 0,
"ammunition": 0,
"accuracy": 0,
"huntingRange": "string"
},
"level120RetrofitStats": {
"luck": 0,
"armor": "string",
"speed": 0,
"health": 0,
"firepower": 0,
"antiAir": 0,
"torpedo": 0,
"evasion": 0,
"aviation": 0,
"oilConsumption": 0,
"reload": 0,
"antiSubmarine": 0,
"oxygen": 0,
"ammunition": 0,
"accuracy": 0,
"huntingRange": "string"
},
"enhanceValue": {
"firepower": 0,
"torpedo": 0,
"aviation": 0,
"reload": 0
},
"scrapValue": {
"coins": 0,
"oil": 0,
"medals": 0
},
"construction": {
"constructionTime": "string",
"availability": {
"light": "string",
"heavy": "string",
"special": "string",
"limited": "string",
"exchange": "string"
}
},
"artist": {
"name": "string",
"url": "string"
},
"pixiv": {
"name": "string",
"url": "string"
},
"twitter": {
"name": "string",
"url": "string"
},
"web": {
"name": "string",
"url": "string"
},
"voiceActor": {
"name": "string",
"url": "string"
}
}
我的问题
造成这种剧烈变化的原因可能是什么? “报价单”的内容并不大,但数据库显示流量非常大
下面您可以看到我的数据库流量的屏幕截图,我在其中请求我包含在此 post 中的对象。
关系?
谢谢❤
[编辑 -> post 的最后一点未完成]
Include
通过生成 LEFT JOIN
急切地加载相关实体。但这会导致行重复。对于 30 次报价,其余船舶数据将重复 30 次。对于大型或复杂的实体来说,这总是一个问题,但即使是有经验的开发人员也会忘记它,因为这样的复杂实体在简单的应用程序中并不常见。对于中等复杂的实体,可以将数据库实体映射到 Select
子句中的 API DTO。
除非您遇到 ERP、CRM 或 BOM 问题,否则所有重要实体和查询至少都那么复杂,您 确实 想要加载 10 个相关实体一个主实体有几十个实例。
EF Core 5 添加了 split queries 来处理这种情况。在文档示例中,以下查询:
using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Include(blog => blog.Posts)
.ToList();
}
生成
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Post] AS [p] ON [b].[BlogId] = [p].[BlogId]
ORDER BY [b].[BlogId], [p].[PostId]
添加 AsSplitQuery()
将其拆分为单独的查询:
using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Include(blog => blog.Posts)
.AsSplitQuery()
.ToList();
}
生成
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url]
FROM [Blogs] AS [b]
ORDER BY [b].[BlogId]
SELECT [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title], [b].[BlogId]
FROM [Blogs] AS [b]
INNER JOIN [Post] AS [p] ON [b].[BlogId] = [p].[BlogId]
ORDER BY [b].[BlogId]
您实际上是从数据库中的 每个 table 中进行选择 并将结果连接在一起 EF Core 使操作变得如此简单,但这并不意味着您应该正在做。
一般来说,select
加入超过 5 个 table 会导致性能问题,因此是查询设计不当的症状 and/or -设计的数据库。查看您的数据库架构,它已高度规范化,可能过于标准化 - 例如,shipconstruction
到 shipweb
tables 中的列(那些在您的数据库图表右侧的列) 都可以向下推入 ships
table。这将使 ships
在列方面变得更宽并导致一些数据重复,但也会消除巨大的 10 连接 。正如您已经发现的那样,联接是杀手。
如果您只使用每个 table 中的少数列,您可以编写一个仅选择这些列的数据库视图。它将比 EF 生成的任何东西都更有效率,但维护起来可能有点麻烦。
最后,除了 EF 之外,您应该关注的是缓存。如果那些 table 中的数据不经常更改,则将相应的实体缓存在内存中,这样您甚至不必访问数据库。您也可以很容易地为此编写一个抽象,如果它存在于缓存中,它将 return 一个实体;如果没有,则从数据库中获取它,将其放入缓存中,然后 returns 它。这样只有你第一次查找一个实体会很慢。
优化数据存储性能远不止于此,还有许多不同的切片和切块方式来平衡您的需求。最终,唯一正确的答案是什么最适合您的问题 space,您需要进行研究和测试以帮助您找到平衡点。但至少现在您对某些选项有了一些了解。
情况
我目前正在 ASP .NET Core 中的 Web API 上工作。为此,我使用 Entity Framework 进行数据库相关操作。此 API return 有关游戏碧蓝航线的信息。我的一个控制器用于检索有关“船舶”的信息。一艘船是一个非常大的物体,需要包含很多东西。下面您将看到我如何检索包含所有链接信息的船舶集。最近,我向名为“ShipQuotes”的所谓“Ship”对象添加了一个新的 属性,它是一个小对象列表(Id + 4 字符串属性)。当然我也必须包括这个,否则 API 会 return 一个空列表。
await _context.Ships
.Include(s => s.Stars)
.Include(s => s.Skins)
.Include(s => s.Skills)
.Include(s => s.LimitBreaks)
.Include(s => s.Gallery)
.Include(s => s.EquippableSlots)
.Include(s => s.Quotes)
.Include(s => s.BaseStats)
.Include(s => s.Level100Stats)
.Include(s => s.Level120Stats)
.Include(s => s.Level100RetrofitStats)
.Include(s => s.Level120RetrofitStats)
.Include(s => s.EnhanceValue)
.Include(s => s.ScrapValue)
.Include(s => s.Construction)
.Include(s => s.Construction.Availability)
.Include(s => s.Artist)
.Include(s => s.Pixiv)
.Include(s => s.Twitter)
.Include(s => s.Web)
.Include(s => s.VoiceActor)
.SingleAsync(ship => ship.Name == name);
问题(?)
我不确定这是否是个问题,但我不知道为什么会这样,但是;在我添加“.Include(s => s.Quotes)”之后,我的 API 的响应时间急剧下降。响应平均为 25kb,所以我认为这与我的 internet/network 没有任何关系(数据库托管在与 Web api 相同的网络上)
一个示例响应,其中还加载了报价大约需要 30 秒 如果我们从对象中排除“引号”属性,它将在 1 秒内加载
一艘船平均有 30 个类似这样的报价:
{
"id": "057404ac-ae41-494f-9825-f4fedbd61cd4",
"skin": "Default Skin",
"event": "Task",
"audioUrl": "---------- EXTERNAL LINK REMOVED FOR Whosebug POST ----------",
"eN_Transcription": "We still have missions that haven't been completed. You should check on their progress.",
"jP_Transcription": "ミッションがまだ終わっていないぞ。進捗を確認したほうがいい",
"cN_Transcription": "还有未完成的任务哦,还是确认下进度比较好"
}
这是船模(在swagger上看到的)
舰船模型
{
"shipId": "string",
"name": "string",
"rarity": "string",
"stars": {
"stars": "string",
"count": 0
},
"nation": "string",
"type": "string",
"thumbnailImage": "string",
"skins": [
{
"name": "string",
"imageUrl": "string",
"backgroundUrl": "string",
"chibiUrl": "string",
"live2dModel": true,
"obtainedFrom": "string"
}
],
"skills": [
{
"iconUrl": "string",
"name": "string",
"description": "string",
"color": "string"
}
],
"limitBreaks": [
{
"limitBreaks": [
"string"
]
}
],
"gallery": [
{
"description": "string",
"url": "string"
}
],
"equippableSlots": [
{
"maxEfficiency": 0,
"minEfficiency": 0,
"type": "string",
"max": 0
}
],
"quotes": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"skin": "string",
"event": "string",
"audioUrl": "string",
"eN_Transcription": "string",
"jP_Transcription": "string",
"cN_Transcription": "string"
}
],
"baseStats": {
"luck": 0,
"armor": "string",
"speed": 0,
"health": 0,
"firepower": 0,
"antiAir": 0,
"torpedo": 0,
"evasion": 0,
"aviation": 0,
"oilConsumption": 0,
"reload": 0,
"antiSubmarine": 0,
"oxygen": 0,
"ammunition": 0,
"accuracy": 0,
"huntingRange": "string"
},
"level100Stats": {
"luck": 0,
"armor": "string",
"speed": 0,
"health": 0,
"firepower": 0,
"antiAir": 0,
"torpedo": 0,
"evasion": 0,
"aviation": 0,
"oilConsumption": 0,
"reload": 0,
"antiSubmarine": 0,
"oxygen": 0,
"ammunition": 0,
"accuracy": 0,
"huntingRange": "string"
},
"level100RetrofitStats": {
"luck": 0,
"armor": "string",
"speed": 0,
"health": 0,
"firepower": 0,
"antiAir": 0,
"torpedo": 0,
"evasion": 0,
"aviation": 0,
"oilConsumption": 0,
"reload": 0,
"antiSubmarine": 0,
"oxygen": 0,
"ammunition": 0,
"accuracy": 0,
"huntingRange": "string"
},
"level120Stats": {
"luck": 0,
"armor": "string",
"speed": 0,
"health": 0,
"firepower": 0,
"antiAir": 0,
"torpedo": 0,
"evasion": 0,
"aviation": 0,
"oilConsumption": 0,
"reload": 0,
"antiSubmarine": 0,
"oxygen": 0,
"ammunition": 0,
"accuracy": 0,
"huntingRange": "string"
},
"level120RetrofitStats": {
"luck": 0,
"armor": "string",
"speed": 0,
"health": 0,
"firepower": 0,
"antiAir": 0,
"torpedo": 0,
"evasion": 0,
"aviation": 0,
"oilConsumption": 0,
"reload": 0,
"antiSubmarine": 0,
"oxygen": 0,
"ammunition": 0,
"accuracy": 0,
"huntingRange": "string"
},
"enhanceValue": {
"firepower": 0,
"torpedo": 0,
"aviation": 0,
"reload": 0
},
"scrapValue": {
"coins": 0,
"oil": 0,
"medals": 0
},
"construction": {
"constructionTime": "string",
"availability": {
"light": "string",
"heavy": "string",
"special": "string",
"limited": "string",
"exchange": "string"
}
},
"artist": {
"name": "string",
"url": "string"
},
"pixiv": {
"name": "string",
"url": "string"
},
"twitter": {
"name": "string",
"url": "string"
},
"web": {
"name": "string",
"url": "string"
},
"voiceActor": {
"name": "string",
"url": "string"
}
}
我的问题
造成这种剧烈变化的原因可能是什么? “报价单”的内容并不大,但数据库显示流量非常大
下面您可以看到我的数据库流量的屏幕截图,我在其中请求我包含在此 post 中的对象。
关系?
谢谢❤
[编辑 -> post 的最后一点未完成]
Include
通过生成 LEFT JOIN
急切地加载相关实体。但这会导致行重复。对于 30 次报价,其余船舶数据将重复 30 次。对于大型或复杂的实体来说,这总是一个问题,但即使是有经验的开发人员也会忘记它,因为这样的复杂实体在简单的应用程序中并不常见。对于中等复杂的实体,可以将数据库实体映射到 Select
子句中的 API DTO。
除非您遇到 ERP、CRM 或 BOM 问题,否则所有重要实体和查询至少都那么复杂,您 确实 想要加载 10 个相关实体一个主实体有几十个实例。
EF Core 5 添加了 split queries 来处理这种情况。在文档示例中,以下查询:
using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Include(blog => blog.Posts)
.ToList();
}
生成
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Post] AS [p] ON [b].[BlogId] = [p].[BlogId]
ORDER BY [b].[BlogId], [p].[PostId]
添加 AsSplitQuery()
将其拆分为单独的查询:
using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Include(blog => blog.Posts)
.AsSplitQuery()
.ToList();
}
生成
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url]
FROM [Blogs] AS [b]
ORDER BY [b].[BlogId]
SELECT [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title], [b].[BlogId]
FROM [Blogs] AS [b]
INNER JOIN [Post] AS [p] ON [b].[BlogId] = [p].[BlogId]
ORDER BY [b].[BlogId]
您实际上是从数据库中的 每个 table 中进行选择 并将结果连接在一起 EF Core 使操作变得如此简单,但这并不意味着您应该正在做。
一般来说,select
加入超过 5 个 table 会导致性能问题,因此是查询设计不当的症状 and/or -设计的数据库。查看您的数据库架构,它已高度规范化,可能过于标准化 - 例如,shipconstruction
到 shipweb
tables 中的列(那些在您的数据库图表右侧的列) 都可以向下推入 ships
table。这将使 ships
在列方面变得更宽并导致一些数据重复,但也会消除巨大的 10 连接 。正如您已经发现的那样,联接是杀手。
如果您只使用每个 table 中的少数列,您可以编写一个仅选择这些列的数据库视图。它将比 EF 生成的任何东西都更有效率,但维护起来可能有点麻烦。
最后,除了 EF 之外,您应该关注的是缓存。如果那些 table 中的数据不经常更改,则将相应的实体缓存在内存中,这样您甚至不必访问数据库。您也可以很容易地为此编写一个抽象,如果它存在于缓存中,它将 return 一个实体;如果没有,则从数据库中获取它,将其放入缓存中,然后 returns 它。这样只有你第一次查找一个实体会很慢。
优化数据存储性能远不止于此,还有许多不同的切片和切块方式来平衡您的需求。最终,唯一正确的答案是什么最适合您的问题 space,您需要进行研究和测试以帮助您找到平衡点。但至少现在您对某些选项有了一些了解。