Подтвердить что ты не робот

Проблема производительности платформы Entity Framework

Я сталкиваюсь с интересной проблемой производительности с Entity Framework. Я использую Code First.

Вот структура моих сущностей:

В книге может быть много обзоров. Обзор связан с одной книгой. В обзоре может быть одно или несколько комментариев. Комментарий связан с одним обзором.

public class Book
{
    public int BookId { get; set; }
    // ...
    public ICollection<Review> Reviews { get; set; }
}

public class Review 
{
    public int ReviewId { get; set; }
    public int BookId { get; set; }
    public Book Book { get; set; }
    public ICollection<Comment> Comments { get; set; }
}

public class Comment
{
     public int CommentId { get; set; }
     public int ReviewId { get; set; }
     public Review Review { get; set; }
}

Я заполнил свою базу данных большим количеством данных и добавил соответствующие индексы. Я пытаюсь получить одну книгу с 10 000 обзорами по этому запросу:

var bookAndReviews = db.Books.Where(b => b.BookId == id)
                       .Include(b => b.Reviews)
                       .FirstOrDefault();

В этой книге есть 10000 обзоров. Производительность этого запроса составляет около 4 секунд. Выполнение одного и того же запроса (через SQL Profiler) фактически возвращается в кратчайшие сроки. Я использовал тот же запрос и SqlDataAdapter и пользовательские объекты для извлечения данных, и это происходит менее чем за 500 миллисекунд.

Используя ANTS Performance Profiler, похоже, что основная часть времени расходуется на несколько разных вещей:

Метод Equals называется 50 миллионов раз.

Кто-нибудь знает, почему ему нужно было бы назвать это 50 миллионов раз и как я мог бы увеличить производительность для этого?

4b9b3361

Ответ 1

Почему Равным называется 50M раз?

Звучит довольно подозрительно. У вас есть 10.000 отзывов и 50.000.000 звонков на Equals. Предположим, что это вызвано отображением идентичности, внутренне реализованным EF. Карта идентичности гарантирует, что каждый объект с уникальным ключом отслеживается контекстом только один раз, поэтому, если контекст уже имеет экземпляр с тем же ключом, что и загруженная запись из базы данных, он не будет реализовывать новый экземпляр и вместо этого использует существующий. Теперь, как это может совпадать с этими числами? Мое страшное предположение:

=============================================
1st      record read   |  0     comparisons
2nd      record read   |  1     comparison
3rd      record read   |  2     comparisons
...
10.000th record read   |  9.999 comparisons

Это означает, что каждая новая запись сравнивается с каждой существующей записью в карте идентификации. Применяя математику для вычисления суммы всех сравнений, мы можем использовать нечто, называемое "Арифметическая последовательность":

a(n) = a(n-1) + 1
Sum(n) = (n / 2) * (a(1) + a(n))
Sum(10.000) = 5.000 * (0 + 9.999) => 5.000 * 10.000 = 50.000.000

Надеюсь, я не ошибся в своих предположениях или расчетах. Подождите! Надеюсь, я ошибся, потому что это не кажется хорошим.

Попробуйте отключить отслеживание изменений = надежно отключить проверку идентификационной карты.

Это может быть сложно. Начните с:

var bookAndReviews = db.Books.Where(b => b.BookId == id)
                             .Include(b => b.Reviews)
                             .AsNoTracking()
                             .FirstOrDefault();

Но есть большая вероятность, что ваше свойство навигации не будет заполнено (потому что оно обрабатывается отслеживанием изменений). В этом случае используйте этот подход:

var book = db.Books.Where(b => b.BookId == id).AsNoTracking().FirstOrDefault();
book.Reviews = db.Reviews.Where(r => r.BookId == id).AsNoTracking().ToList();

В любом случае вы можете увидеть, какой тип объекта передается в Equals? Я думаю, что это должно сравнивать только первичные ключи, и даже 50M целочисленные сравнения не должны быть такой проблемой.

Как побочная заметка EF медленная - это хорошо известный факт. Он также использует отражение внутри, когда материализует объекты, поэтому просто 10.000 записей могут занимать "некоторое время". Если вы этого не сделали, вы также можете отключить создание динамического прокси (db.Configuration.ProxyCreationEnabled).

Ответ 2

Я знаю, что это звучит слабо, но вы пробовали наоборот, например:

var reviewsAndBooks = db.Reviews.Where(r => r.Book.BookId == id)
                       .Include(r => r.Book);

Я заметил иногда лучшую производительность от EF, когда вы подходите к своим запросам таким образом (но у меня не было времени выяснить, почему).