Настройка Производительности в EF Core
Сегодня посоветую вам видео из серии EF Core Community Standup, посвящённое настройке производительности в Entity Framework. Для знающих SQL, вроде меня, советы могут показаться довольно очевидными, хотя интересно было узнать, что оптимизации можно делать не в SQL, а прямо в LINQ запросе к EF.
Для примера взято приложение книжного магазина. Есть основная сущность Books. Сущности Authors (авторы) и Tags (теги), которые относятся к Books как многие-ко-многим, а сущность Reviews (отзывы и оценки) – один-ко-многим.
Нам для отображения доступных книг нужно выбрать данные книги (название, цена, год публикации), авторов и теги для каждой книги, а также количество отзывов и среднюю оценку.
Очевидным вариантом запроса будет что-то вроде:
var books = context.BooksКак можно улучшить этот запрос:
.Include(book => book.AuthorsLink
.OrderBy(bookAuthor => bookAuthor.Order))
.ThenInclude(bookAuthor => bookAuthor.Author)
.Include(book => book.Reviews)
.Include(book => book.Tags)
.ToList();
1. Добавить метод
AsNoTracking() для всех запросов, не предполагающих изменения данных.2. Не загружать ненужные данные. Используйте проекцию в методе
Select для выбора только нужных полей:Books.Select(p => new BookListDto {
Title = p.Title,
Price = p.Price,
Published = p.Published,
…
3. Не включать зависимости, а выбирать только то, что надо из них. Например, нам нужны только имена авторов через запятую: AuthorsOrdered = string.Join(", ",
p.AuthorsLink.OrderBy(q => q.Order)
.Select(q => q.Author.Name)),
…
String.Join будет выполняться на клиенте, но можно использовать пользовательские функции SQL (User-Defined Functions).4. По возможности выполнять вычисления на стороне БД. Например, для количества отзывов и средней оценки (заметьте, что требуется приведение к обнуляемому типу и тип зависит от конкретной СУБД):
ReviewsCount = p.Reviews.Count(),5. В конфигурации сущностей EF добавить индексы для каждого свойства сущности, по которому планируется производить сортировку или фильтрацию.
ReviewsAverageVotes =
p.Reviews.Select(y =>
(double?)y.NumStars).Average(),
…
Чтобы не загромождать запрос, все эти изменения можно разместить в методе расширения
MapBookToDto(this IQueryable<Book> books), таким образом сам запрос будет выглядеть примерно так:var booksQuery = _context.BooksЕсли эти советы всё равно не дают достаточной производительности, можно, например, денормализовать БД и включить столбцы с рассчитанными значениями количества отзывов и средней оценкой в таблицу Books. А при добавлении/удалении отзыва пересчитывать эти значения.
.AsNoTracking()
.MapBookToDto()
. …
В видео по ссылке ниже весь процесс оптимизации производительности описан более подробно и наглядно.
Источники:
- Видео https://youtu.be/VgNFFEqwZPU
- Текст https://www.thereformedprogrammer.net/five-levels-of-performance-tuning-for-an-ef-core-query/