Entity Framework Core - очень низкая производительность - PullRequest
0 голосов
/ 17 февраля 2020

У меня есть следующие объекты (я покажу свойства, с которыми я работаю, потому что я не хочу делать их больше, чем нужно):

ИМУЩЕСТВО: Где свойство может быть дочерним по отношению к другому и иметь отношение 1-1 с GeoLocation и может иметь несколько Multimedia и Operation

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: Как уже упоминалось, оно имеет отношение 1-1 с Property

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; }
}

ОПЕРАЦИИ: определяет все операции, которые может выполнять Property, используя OperationType для названия этой операции. (аренда, продажа и т. д. c.)

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; }
}

ЦЕНА: определяет цену для каждой операции и тип валюты. (то есть: свойство может иметь вариант аренды - Operation - для суммы X в валюте USD, но другая цена, зарегистрированная для той же 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 l oop это тоже занимает много времени, и в этом есть смысл.

Итак, что, по-вашему, я могу здесь сделать? Заранее спасибо.

ОБНОВЛЕНИЕ: Вот метод GetAllProperties(), я забыл включить этот.

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

И запрос SQL, что Entity Framework делает против 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

1 Ответ

1 голос
/ 18 февраля 2020

Хорошо, несколько вещей, которые должны помочь. # 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 l oop обращаетесь к объектам Price и OperationType из него .

Редактировать Обновлен пример для коллекции операций. (Whups)

Вместо этого я бы порекомендовал:

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 c, чтобы использовать возвращаемые свойства, а не возвращаемую сущность и связанные значения сущности.

Загрузка 40-50 тыс. Записей с чем-то вроде этого поля изображения (MultiMedia) потенциально всегда будет проблематичной c. Зачем вам нужно загружать все 50k в один go?

Это похоже на то, что может поставить маркеры на карте. Решения, подобные этому, должны предусматривать применение фильтра радиуса как минимум для получения маркеров в пределах разумного радиуса от заданной центральной точки на карте или при загрузке большей области (уменьшенной карты), вычислении областей и фильтрации данных по регионам или получении рассчитывать падение в этом регионе и загрузку / рендеринг местоположений в пакетах по 100 или около того, вместо того, чтобы потенциально ожидать загрузки всех местоположений. Что-то, чтобы рассмотреть.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...