【问题标题】:Entity Framework Core - Very slow performanceEntity Framework Core - 性能非常慢
【发布时间】:2020-02-17 20:31:40
【问题描述】:

我有以下实体(我将展示我正在使用的属性,因为我不想让它比需要的大):

属性:其中一个属性可以是另一个属性的子属性,并且与GeoLocation 具有一对一关系,并且可以有多个MultimediaOperation

public partial class Property
{
    public Property()
    {
        InverseParent = new HashSet<Property>();
        Multimedia = new HashSet<Multimedia>();
        Operation = new HashSet<Operation>();
    }

    public long Id { get; set; }
    public string GeneratedTitle { get; set; }
    public string Url { get; set; }
    public DateTime? DatePublished { get; set; }
    public byte StatusCode { get; set; }
    public byte Domain { get; set; }
    public long? ParentId { get; set; }

    public virtual Property Parent { get; set; }
    public virtual GeoLocation GeoLocation { get; set; }
    public virtual ICollection<Property> InverseParent { get; set; }
    public virtual ICollection<Multimedia> Multimedia { get; set; }
    public virtual ICollection<Operation> Operation { get; set; }
}

GEOLOCATION:如前所述,它与Property具有1-1关系

public partial class GeoLocation
{
    public int Id { get; set; }
    public double? Latitude { get; set; }
    public double? Longitude { get; set; }
    public long? PropertyId { get; set; }

    public virtual Property Property { get; set; }
}

多媒体:它可以为单个Property 保存多个不同大小的图像。这里的细节是Order 指定要在客户端应用程序中显示的图像的顺序,但它并不总是以 1 开头。在某些情况下,Property 具有以开头的 Multimedia 文件3 或 x。

public partial class Multimedia
{
    public long Id { get; set; }
    public long? Order { get; set; }
    public string Resize360x266 { get; set; }
    public long? PropertyId { get; set; }

    public virtual Property Property { get; set; }
}

OPERATIONS: 定义了Property 可以拥有的所有操作,使用OperationType 命名此操作。 (出租、出售等)

public partial class Operation
{
    public Operation()
    {
        Price = new HashSet<Price>();
    }

    public long Id { get; set; }
    public long? OperationTypeId { get; set; }
    public long? PropertyId { get; set; }

    public virtual OperationType OperationType { get; set; }
    public virtual Property Property { get; set; }
    public virtual ICollection<Price> Price { get; set; }
}

public partial class OperationType
{
    public OperationType()
    {
        Operation = new HashSet<Operation>();
    }

    public long Id { get; set; }
    public string Name { get; set; }

    public virtual ICollection<Operation> Operation { get; set; }
}

PRICE: 定义每个操作的价格和货币类型。 (即:物业可以有租金选项 - Operation - X 金额以美元货币计算,但在使用其他货币类型的情况下,另一个价格注册为相同的Operation

public partial class Price
{
    public long Id { get; set; }
    public float? Amount { get; set; }
    public string CurrencyCode { get; set; }
    public long? OperationId { get; set; }

    public virtual Operation Operation { get; set; }
}

也就是说,我想获取所有的记录(实际上大约是 40K-50K),但只针对少数几个属性。正如我之前提到的,Multimedia 表对于每个Property 可以有很多记录,但我只需要第一个具有较小Order 值并按DatePublished 排序的记录。之后,我需要将结果转换为 MapMarker 对象,如下:

public class MapMarker : EstateBase
{
    public long Price { get; set; }
    public int Category { get; set; }
    public List<Tuple<string, string, string>> Prices { get; set; }
}

为了实现这一点,我做了以下工作:

public async Task<IEnumerable<MapMarker>> GetGeolocatedPropertiesAsync(int quantity)
{
    var properties = await GetAllProperties().AsNoTracking()
        .Include(g => g.GeoLocation)
        .Include(m => m.Multimedia)
        .Include(p => p.Operation).ThenInclude(o => o.Price)
        .Include(p => p.Operation).ThenInclude(o => o.OperationType)
        .Where(p => p.GeoLocation != null 
            && !string.IsNullOrEmpty(p.GeoLocation.Address) 
            && p.GeoLocation.Longitude != null 
            && p.GeoLocation.Latitude != null 
            && p.StatusCode == (byte)StatusCode.Online 
            && p.Operation.Count > 0)
        .OrderByDescending(p => p.ModificationDate)
        .Take(quantity)
        .Select(p => new {
            p.Id,
            p.Url,
            p.GeneratedTitle,
            p.GeoLocation.Address,
            p.GeoLocation.Latitude,
            p.GeoLocation.Longitude,
            p.Domain,
            p.Operation,
            p.Multimedia.OrderBy(m => m.Order).FirstOrDefault().Resize360x266
        })
        .ToListAsync();

    var mapMarkers = new List<MapMarker>();

    try
    {
        foreach (var property in properties)
        {
            var mapMarker = new MapMarker();
            mapMarker.Id = property.Id.ToString();
            mapMarker.Url = property.Url;
            mapMarker.Title = property.GeneratedTitle ?? string.Empty;
            mapMarker.Address = property.Address ?? string.Empty;
            mapMarker.Latitude = property.Latitude.ToString() ?? string.Empty;
            mapMarker.Longitude = property.Longitude.ToString() ?? string.Empty;
            mapMarker.Domain = ((Domain)Enum.ToObject(typeof(Domain), property.Domain)).ToString();
            mapMarker.Image = property.Resize360x266 ?? string.Empty;
            mapMarker.Prices = new List<Tuple<string, string, string>>();
            foreach (var operation in property.Operation)
            {
                foreach (var price in operation.Price)
                {
                    var singlePrice = new Tuple<string, string, string>(operation.OperationType.Name, price.CurrencyCode, price.Amount.ToString());
                    mapMarker.Prices.Add(singlePrice);
                }
            }
            mapMarkers.Add(mapMarker);
        }
    }
    catch (Exception ex)
    {

        throw;
    }

    return mapMarkers;
}

但结果需要超过 14 分钟,并且此方法可以在一分钟内调用多次。我想优化它以在尽可能短的时间内返回结果。我已经尝试删除 ToListAsync(),但在 foreach 循环中也需要很多时间,这很有意义。

那么,你认为我能在这里做什么? 提前致谢。

更新: 这里是GetAllProperties() 方法,我忘了包括这个。

private IQueryable<Property> GetAllProperties()
{
    return _dbContext.Property.AsQueryable();
}

以及 Entity Framework 对 SQL Server 进行的 SQL 查询:

SELECT [p].[Id], [p].[Url], [p].[GeneratedTitle], [g].[Address], [g].[Latitude], [g].[Longitude], [p].[Domain], (
    SELECT TOP(1) [m].[Resize360x266]
    FROM [Multimedia] AS [m]
    WHERE [p].[Id] = [m].[PropertyId]
    ORDER BY [m].[Order]), [t].[Id], [t].[CreationDate], [t].[ModificationDate], [t].[OperationTypeId], [t].[PropertyId], [t].[Id0], [t].[CreationDate0], [t].[ModificationDate0], [t].[Name], [t].[Id1], [t].[Amount], [t].[CreationDate1], [t].[CurrencyCode], [t].[ModificationDate1], [t].[OperationId]
FROM [Property] AS [p]
LEFT JOIN [GeoLocation] AS [g] ON [p].[Id] = [g].[PropertyId]
LEFT JOIN (
    SELECT [o].[Id], [o].[CreationDate], [o].[ModificationDate], [o].[OperationTypeId], [o].[PropertyId], [o0].[Id] AS [Id0], [o0].[CreationDate] AS [CreationDate0], [o0].[ModificationDate] AS [ModificationDate0], [o0].[Name], [p0].[Id] AS [Id1], [p0].[Amount], [p0].[CreationDate] AS [CreationDate1], [p0].[CurrencyCode], [p0].[ModificationDate] AS [ModificationDate1], [p0].[OperationId]
    FROM [Operation] AS [o]
    LEFT JOIN [OperationType] AS [o0] ON [o].[OperationTypeId] = [o0].[Id]
    LEFT JOIN [Price] AS [p0] ON [o].[Id] = [p0].[OperationId]
) AS [t] ON [p].[Id] = [t].[PropertyId]
WHERE (((([g].[Id] IS NOT NULL AND ([g].[Address] IS NOT NULL AND (([g].[Address] <> N'') OR [g].[Address] IS NULL))) AND [g].[Longitude] IS NOT NULL) AND [g].[Latitude] IS NOT NULL) AND ([p].[StatusCode] = CAST(1 AS tinyint))) AND ((
    SELECT COUNT(*)
    FROM [Operation] AS [o1]
    WHERE [p].[Id] = [o1].[PropertyId]) > 0)
ORDER BY [p].[ModificationDate] DESC, [p].[Id], [t].[Id], [t].[Id1]

更新 2: 正如@Igor 提到的,这是执行计划结果的链接: https://www.brentozar.com/pastetheplan/?id=BJNz9KdQI

【问题讨论】:

  • 什么是GetAllProperties()?
  • @M.Spiller 我只是在最后包含了那个。我忘记了。
  • 是数据库查询/查询还是代码?看看 Sql Server 在做什么。有很多方法可以做到这一点,但一种简单的方法是使用 Sql Profiler。分析生成的查询及其查询计划。您还可以使用各种工具(一些内置于 VS 或第三方工具)分析 .net 性能。
  • @Igor 我刚刚添加了 EF 在问题末尾提出的 SQL 咨询。
  • @Igor,我现在就在做。完成后我会发布结果。

标签: sql-server performance entity-framework linq asp.net-core


【解决方案1】:

好的,一些事情应该会有所帮助。 #1。 .Include().Select() 通常应相互排斥。

您正在选择:

p.Id,
p.Url,
p.GeneratedTitle,
p.GeoLocation.Address,
p.GeoLocation.Latitude,
p.GeoLocation.Longitude,
p.Domain,
p.Operation,
p.Multimedia.OrderBy(m => m.Order).FirstOrDefault().Resize360x266

但随后在您的 foreach 循环中访问 Price 和 OperationType 实体。

编辑 更新了集合操作的例子。 (呜呜呜)

我会推荐:

p.Id,
p.Url,
p.GeneratedTitle,
p.GeoLocation.Address,
p.GeoLocation.Latitude,
p.GeoLocation.Longitude,
p.Domain,
Operations = p.Operation.Select( o => new 
{
   OperationTypeName = o.OperationType.Name,
   o.Price.Amount,
   o.Price.CurrencyCode
}).ToList(),
p.Multimedia.OrderBy(m => m.Order).FirstOrDefault().Resize360x266

然后调整您的 foreach 逻辑以使用返回的属性而不是返回的实体和相关实体值。

使用类似图像字段(多媒体)的内容加载 40-50k 记录可能总是会出现问题。为什么需要一次性加载全部 50k?

这看起来像是在地图上放置标记的东西。像这样的解决方案应该考虑至少应用半径过滤器以在地图上给定中心点的合理半径内获取标记,或者如果加载更大的区域(缩小地图)计算区域并按区域过滤数据或获取计数落在该区域并以 100 个左右的批次加载/渲染位置,而不是潜在地等待 所有 位置加载。需要考虑的事情。

【讨论】:

  • 谢谢。我认为您对在这种情况下最好的方法是正确的。我真的很感激这些信息。但是,谈到 foreach 循环,我不能只获取 Name、Amount 和 CurrencyCode 属性,因为它们在集合中,无法以这种方式访问​​
  • 啊,是的,我一开始就看到了,但是当我写这个例子的时候我就忘记了。我更新了示例以使用 Select 检索操作值的集合,这应该会有所帮助。
  • 好吧,经过几天的尝试和尝试。我终于应用了你在这里提到的内容,我认为这是一种更好的方法,最好使用好的策略,而不是试图“修复”并制作原则上使用糟糕设计的东西。非常感谢......我的应用程序现在就像一个魅力。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-24
  • 2021-12-02
  • 2019-02-24
  • 2014-06-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多