Проблема производительности Entity Framework - PullRequest
18 голосов
/ 13 сентября 2011

У меня возникла интересная проблема с производительностью 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();

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

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

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

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

Ответы [ 2 ]

20 голосов
/ 13 сентября 2011

Почему Equals называется 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).

1 голос
/ 13 сентября 2011

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

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

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

...