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 -设计的数据库。查看您的数据库架构,它已高度规范化,可能过于标准化 - 例如,shipconstructionshipweb tables 中的列(那些在您的数据库图表右侧的列) 都可以向下推入 ships table。这将使 ships 在列方面变得更宽并导致一些数据重复,但也会消除巨大的 10 连接 。正如您已经发现的那样,联接是杀手。

如果您只使用每个 table 中的少数列,您可以编写一个仅选择这些列的数据库视图。它将比 EF 生成的任何东西都更有效率,但维护起来可能有点麻烦。

最后,除了 EF 之外,您应该关注的是缓存。如果那些 table 中的数据不经常更改,则将相应的实体缓存在内存中,这样您甚至不必访问数据库。您也可以很容易地为此编写一个抽象,如果它存在于缓存中,它将 return 一个实体;如果没有,则从数据库中获取它,将其放入缓存中,然后 returns 它。这样只有你第一次查找一个实体会很慢。

优化数据存储性能远不止于此,还有许多不同的切片和切块方式来平衡您的需求。最终,唯一正确的答案是什么最适合您的问题 space,您需要进行研究和测试以帮助您找到平衡点。但至少现在您对某些选项有了一些了解。