Почему Entity Framework имеет проблемы с производительностью при расчете суммы - PullRequest
1 голос
/ 08 ноября 2019

Я использую Entity Framework в приложении C # и использую отложенную загрузку. При вычислении суммы свойства в коллекции элементов возникают проблемы с производительностью. Позвольте мне проиллюстрировать это упрощенной версией моего кода:

public decimal GetPortfolioValue(Guid portfolioId) {

    var portfolio = DbContext.Portfolios.FirstOrDefault( x => x.Id.Equals( portfolioId ) );
    if (portfolio == null) return 0m;

    return portfolio.Items
        .Where( i =>
            i.Status == ItemStatus.Listed
            &&
            _activateStatuses.Contains( i.Category.Status )
        )
        .Sum( i => i.Amount );
} 

Поэтому я хочу получить значение для всех моих элементов, которые имеют определенный статус, родительский статус которого также имеет определенный статус.

При регистрации запросов, сгенерированных EF, я вижу, что он сначала выбирает мой Portfolio (что нормально). Затем он выполняет запрос для загрузки всех Item сущностей, которые являются частью этого портфеля. И затем он начинает выбирать ВСЕ Category сущностей для каждого Item по одному. Поэтому, если у меня есть портфель, содержащий 100 элементов (каждый со своей категорией), он буквально выполняет 100 SELECT ... FROM categories WHERE id = ... запросов.

Так что, похоже, он просто извлекает всю информацию, сохраняет ее в своей памяти и затем вычисляетсумма. Почему он не выполняет простое соединение между моими таблицами и вычисляет его следующим образом?

Вместо выполнения 102 запросов для вычисления суммы из 100 элементов, я ожидал бы что-то вроде:

SELECT
    i.id, i.amount 
FROM
    items i 
    INNER JOIN categories c ON c.id = i.category_id
WHERE
    i.portfolio_id = @portfolioId
    AND
    i.status = 'listed'
    AND
    c.status IN ('active', 'pending', ...);

, по которому он мог бы затем рассчитать сумму (если он не может использовать SUM непосредственно в запросе).

В чем проблема и как я могу улучшить производительность, кроме написания чистого ADOзапрос вместо использования Entity Framework?

Чтобы завершить, вот мои сущности EF:

public class ItemConfiguration : EntityTypeConfiguration<Item> {
   ToTable("items");
   ...
   HasRequired(p => p.Portfolio);
}

public class CategoryConfiguration : EntityTypeConfiguration<Category> {
    ToTable("categories");
    ...
    HasMany(c => c.Products).WithRequired(p => p.Category);
}

РЕДАКТИРОВАТЬ на основе комментариев:

Я не сделалдумаю, что это важно, но _activeStatuses - это список перечислений.

private CategoryStatus[] _activeStatuses = new[] { CategoryStatus.Active, ... };

Но, вероятно, более важно то, что я не учел, что статус в базе данных является строкой ("active "," pending ", ...) но я сопоставляю их с перечислением, используемым в приложении. И поэтому EF не может это оценить? Фактический код:

... && _activateStatuses.Contains(CategoryStatusMapper.MapToEnum(i.Category.Status)) ...

EDIT2

Действительно, отображение является большой частью проблемы, но сам запрос кажется самой большой проблемой. Почему разница в производительности между этими двумя запросами так велика?

// Slow query
var portfolio = DbContext.Portfolios.FirstOrDefault(p => p.Id.Equals(portfolioId));
var value = portfolio.Items.Where(i => i.Status == ItemStatusConstants.Listed && 
                _activeStatuses.Contains(i.Category.Status))
                .Select(i => i.Amount).Sum();

// Fast query
var value = DbContext.Portfolios.Where(p => p.Id.Equals(portfolioId))
                .SelectMany(p => p.Items.Where(i => 
                    i.Status == ItemStatusConstants.Listed &&
                    _activeStatuses.Contains(i.Category.Status)))
                    .Select(i => i.Amount).Sum();

Первый запрос выполняет МНОГО маленьких SQL-запросов, а второй просто объединяет все в один более крупный запрос. Я ожидаю, что даже первый запрос запустит один запрос, чтобы получить значение портфеля.

Ответы [ 2 ]

1 голос
/ 08 ноября 2019

Да CategoryStatusMapper.MapToEnum нельзя преобразовать в SQL, что заставляет его запускать Where в .Net. Вместо отображения состояния в перечисление _activeStatuses должен содержать список целочисленных значений из перечисления, поэтому сопоставление не требуется.

private int[] _activeStatuses = new[] { (int)CategoryStatus.Active, ... };

Так что содержимое становится

... && _activateStatuses.Contains(i.Category.Status) ...

и могут быть преобразованы в SQL

ОБНОВЛЕНИЕ

Учитывая, что i.Category.Status является строкой в ​​базе данных, тогда

private string[] _activeStatuses = new[] { CategoryStatus.Active.ToString(), ... };
1 голос
/ 08 ноября 2019

Вызов portfolio.Items это лениво загрузит коллекцию в Items и , затем выполнит последующие вызовы, включая выражения Where и Sum. См. Также Загрузка статьи связанных сущностей .

Вы должны выполнить вызов непосредственно на DbContext, * Sum выражение может быть оценено на стороне сервера базы данных.

var portfolio = DbContext.Portfolios
    .Where(x => x.Id.Equals(portfolioId))
    .SelectMany(x => x.Items.Where(i => i.Status == ItemStatus.Listed && _activateStatuses.Contains( i.Category.Status )).Select(i => i.Amount))
    .Sum();

Вы также должны использовать соответствующий тип для экземпляра _activateStatuses, поскольку содержащиеся в нем значения должны соответствовать типу, сохраненному в базе данных. Если в базе данных сохраняются строковые значения, вам необходимо передать список строковых значений.

var _activateStatuses = new string[] {"Active", "etc"};

Вы можете использовать выражение Linq для преобразования перечислений в их строковые значения.


Примечания

  • Я бы порекомендовал вам отключить отложенную загрузку для вашего типа DbContext. Как только вы это сделаете, вы начнете ловить подобные проблемы во время выполнения с помощью исключений и сможете писать более производительный код.
  • Я не включил проверку ошибок, если портфель не был найден, но вы могли бы расширить этокод соответственно.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...