Многие, кто использует EF Core в работе в качестве ORM
(Object-Relational Mapping) встречаются с множеством прелестей этого инструмента:
Достаточная гибкость
Скорость разработки
Возможность поддерживать Legacy
Написание хранимок и прочее...
И в одном из проектов я наткнулся на формат его использования, который приводил к бОльшим нагрузкам и поеданию памяти.
Закрывая очередную задачу по бизнес-логике я заметил в выводе терминала очень много странных записей, которых там быть не должно.
Покопавшись дальше я увидел сообщение о том, что *100 записей были добавлены в ChangeTracker
.
Странно, но я же просто открыл страницу и нафига мне тут отслеживать изменения?
После я сразу же полез в код и обнаружил, что в части проекта напрочь забыто про такую вещь как Tracking behavior
И тут ради интереса мне захотелось посмотреть влияние трэкинга на быстродействие запросов не формате "Так однозначно быстрее", а с пруфами и метриками. Ссылки на статьи прикреплю ниже*
Перейдем к результатам
Изначально я собрал проект на .NET Core и подключил пакеты и создал тестовую базу данн/ых на PostrgeSQL (ссылку на github прицеплю ниже).
код бенчмарка
public class Benchmark
{
[Benchmark]
public async Task<int> WithTracking()
{
using var dbContext = new AppDbContext();
var products = await dbContext.Product.AsTracking().ToListAsync();
return products.Count;
}
[Benchmark]
public async Task<int> WithoutTracking()
{
using var dbContext = new AppDbContext();
var products = await dbContext.Product.AsNoTracking().ToListAsync();
return products.Count;
}
[Benchmark]
public async Task<int> WithTrackingAndOrder()
{
using var dbContext = new AppDbContext();
var products = await dbContext.Product.AsTracking().OrderBy(x => x.Name).ToListAsync();
return products.Count;
}
[Benchmark]
public async Task<int> WithoutTrackingAndOrder()
{
using var dbContext = new AppDbContext();
var products = await dbContext.Product.AsNoTracking().OrderBy(x => x.Name).ToListAsync();
return products.Count;
}
}
Итак, 1-й набор данных на 100 записей:
Method |
Mean |
Error |
StdDev |
---|---|---|---|
WithTracking |
521.6 us |
10.28 us |
11.43 us |
WithoutTracking |
395.2 us |
6.43 us |
5.70 us |
WithTrackingAndOrder |
683.7 us |
12.76 us |
13.11 us |
WithoutTrackingAndOrder |
548.2 us |
10.76 us |
20.72 us |
Для такого объема данных разница уже существенна, но для пользователя не особо заметна. (Кстати, именно поэтому на стартах проектов чаще всего забивают на тонкости оптимизации)
Далее - 1.000 записей в таблице
Method |
Mean |
Error |
StdDev |
---|---|---|---|
WithTracking |
2,193.3 us |
38.75 us |
36.25 us |
WithoutTracking |
833.0 us |
12.46 us |
10.41 us |
WithTrackingAndOrder |
3,707.3 us |
55.82 us |
46.61 us |
WithoutTrackingAndOrder |
2,073.3 us |
26.42 us |
24.71 us |
Здесь мы видим уже существенный (быстрее более чем в 2 раза) прув от трэкинга.
Что дальше?
10.000 записей
Method |
Mean |
Error |
StdDev |
Median |
---|---|---|---|---|
WithTracking |
30.820 ms |
0.5955 ms |
1.5895 ms |
30.323 ms |
WithoutTracking |
7.939 ms |
0.1398 ms |
0.1308 ms |
7.942 ms |
WithTrackingAndOrder |
42.891 ms |
0.8493 ms |
0.7944 ms |
42.776 ms |
WithoutTrackingAndOrder |
22.248 ms |
0.4247 ms |
0.3765 ms |
22.346 ms |
В простом случае (без учета OrderBy
) мы видим, что опция AsNoTracking
работает в 4 раза быстрее. И в то же время AsNoTracking
и OrderBy
дают непропорциональные результаты результаты. Я думаю, что это связано с оптимизациями производительности, внедренными в EF Core и работой с коллекциями.
100.000 записей
Method |
Mean |
Error |
StdDev |
---|---|---|---|
WithTracking |
312.45 ms |
5.936 ms |
6.351 ms |
WithoutTracking |
81.35 ms |
1.508 ms |
1.411 ms |
WithTrackingAndOrder |
453.65 ms |
8.072 ms |
7.551 ms |
WithoutTrackingAndOrder |
208.51 ms |
4.040 ms |
4.149 ms |
Как видите, результата очень схож и коррелирует с выборкой в 10.000 записей.
Ради интереса, 1млн записей
Method |
Mean |
Error |
StdDev |
---|---|---|---|
WithTracking |
3,511.5 ms |
70.10 ms |
122.77 ms |
WithoutTracking |
851.1 ms |
13.34 ms |
12.48 ms |
WithTrackingAndOrder |
4,083.4 ms |
49.27 ms |
43.68 ms |
WithoutTrackingAndOrder |
1,634.6 ms |
31.82 ms |
40.25 ms |
Тенденция схожа и никаких коллизий тут не отмечено.
Выводы, кэп?
Мы обсудили и сравнили результаты с одним из параметров - AsNoTracking
.
Как видно, этот параметр значительно влияет на эффективность запросов и обработку данных, и он работает в 4 раза быстрее на наборе данных из более чем 10 000 объектов.
Как жить?
1) Вы можете установить это значение по-умолчанию как AsNoTracking
, и, при необходимости, когда вам потребуется отслеживать изменения в объектах, вы сможете получить объекты с помощью метода AsTracking
services.AddDbContext<DatabaseContext>(options =>
{
options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
...
});
2) Использовать маппинг в DTO-модели при получении данных и избегать загрузку всех связанных сущностей и ненужных данных
public List<ProductDTO> GetProducts()
{
return _context.Products
.Select(x => _mapper.Map<ProductDTO>(x))
.ToArray();
}
3) Избегать загрузки больших коллекций в память, которые планируется править.
Берите на заметку и быстрых решений!
Ссылки:
Контакты:
Комментарии (29)
CrazyElf
08.11.2023 11:56+4Что-то я не понял, в таблицах везде написано, что
WithoutTracking
дольше работает, чемWithTracking
. Данные перепутаны местами?mushegovdima Автор
08.11.2023 11:56-1Да, похоже на то... Переставлял для удобства анализа. Сейчас перезалью результаты)
CrazyElf
08.11.2023 11:56А по сути да, обычно редко когда делают коллекции прямо вот из сущностей
EF
. Делают из нихDTO
, а потом бизнес-объекты. А если нужно что-то заменить в базе, то заново читают из БД сущность с тем жеid
, что у той, которая в бизнес-объекте, и её уже изменяют и сохраняют обратно. Держать в памяти тонны сущностейEF
в любом случае плохая практика. Ну, если только вы их не планируете так же массово изменять и писать обратно в БД.
iamkisly
08.11.2023 11:56+3Это здорово, что вы написали большую статью с бенчмарками.. но это базовая функциональность EF Core. Банально заходим в msdn и читаем
Запросы без отслеживания полезны, если результаты используются в сценарии только для чтения. Обычно они быстрее выполняются, так как нет необходимости настраивать сведения об отслеживании изменений.
Бенчмарк конечно наглядный, но стоило развить тему и так же написать какая база, сколько каких свойств было у запрашиваемых сущностей, каких типов, загружались ли связанные сущности по Foreign Key. Хотя такие бенчмарки тоже уже есть. В общем это обычный непримечательный случай траблшутинга с не очень понятными вводными, который не раз уже описывался на Хабре.
Интерес в комментариях же из-за низкого порога вхождения в язык, и большого количества нахватавшихся по верхам неофитов.. я бы советовал прочесть тот же Entity Framework Core in Action, тем более есть отличный перевод от dotnet.ru
mushegovdima Автор
08.11.2023 11:56+1Интерес личный был именно в том, "насколько"?
Запилил, поделился :)
Мне кажется именно в этом цель сообществаMyraJKee
08.11.2023 11:56На Хабре требования к качеству материала весьма высоки... Господа программисты тут оооочеень разборчивы... )
Alekseyz
08.11.2023 11:56Давно уже есть Queryable Extensions
ProjectTo<ProductDTO>(configuration)
вместо
Select(x => _mapper.Map<ProductDTO>(x))
sh-vasily
08.11.2023 11:56Интересно было бы сравнить такой вариант по скорости с dapper
iamkisly
08.11.2023 11:56Вопрос в том как и что сравнивать.. билдить запрос из модели? a кэшировать expression tree? или в обоих случаях сырым запросом? С параметрами или конкатенацией? Ещё бы проконтролировать что БД один и тот же план запроса использует во всех случаях.
lexxei_ru
08.11.2023 11:56После spring приложений дали поддерживать проект на EF. Честно говоря это дикий ужас, кто эти люди, которые предлагают писать бэк на этом...
mushegovdima Автор
08.11.2023 11:56хах, знакомо :)
В прошлом копался в NHibernate, но переезд был быстр и понятен.
Тут можно развернуться по-сути, особенно если нужно "Побыстрее и еще вчера"
iamkisly
08.11.2023 11:56+4Могу сказать тоже самое про spring. Ощущения субъективны, так что поменьше пафоса)
hVostt
08.11.2023 11:56+3EF шикарный, просто лучший! NHibernate -- калька с Hibernate, вот это действительно был ужас, я с дрожью вспоминаю проекты на нём, очень тяжёлые и монструозные решения, но тогда и альтернатив особо не было. Сейчас же, на EF писать прям кайф. Я не знаю что там за "дикий ужас" вы узрели, но либо перед вами там так наговнокодили, что даже умудрились EF через одно место использовать, или у вас просто религиозные шоры, аля, спринг уан лав, остальное всё УГ — тут уже ничего не попишешь. Ведь что для настоящего инженера важно? Правильно, объективность в топку, цвет фломастеров наше всё :)
lexxei_ru
08.11.2023 11:56-1Да там понаписали костылей до меня, что проект на VisualStudio даже не собирается, только под Rider можно запустить. Тут в целом наверное претензия наверное не к EF, а в целом .net платформе, если не legacy проект выглядит ужасно. Но скорее всего это разработчики такие веселые были. Помимо спринга еще писал на Django. Там тоже как то всё намного понятнее
lexxei_ru
08.11.2023 11:56про Hibernate и в спринге хорошего сказать не могу - слишком много приносит ошибок, которые на первый взгляд кажутся неадекватными
zerg903
08.11.2023 11:56установить это значение по-умолчанию как AsNoTracking
Если в коде есть разделение ответственности на команды и запросы, то можно выделить 2 интерфейса для контекста:
public interface IDbReadOnly : IDisposable, IAsyncDisposable { IQueryable<Order> Orders { get; } IQueryable<Product> Products { get; } // ... IQueryable<T> Query<T>() where T : class, IModel; } public interface IDb : IDbReadOnly { void Add<T>(T entry) where T : class, IModel; void Remove<T>(T entry) where T : class, IModel; void Attach<T>(T entry) where T : class, IModel; Task SaveAsync(CancellationToken cancellationToken = default); } public class AppDb : DbContext, IDb { // ... }
Соответственно, запросы используют IDbReadOnly, для которого на уровне DI контейнера включено AsNoTracking.
Vitimbo
Разве C# программист не должен вбить в себя привычку прописывать AsNoTracking везде, кроме мест, где этого быть не должно?
CrazyElf
Я обычно никогда не прописывал, просто потому что обычно в "сыром" виде сущности
EF
в доменной модели не нужны. Их обычно тут же преобразуют в объекты домена, как-то аггрегируют и т.д.Vitimbo
То есть через .Select(x => new Dto....) получаете новый объект? Разве в таком случае не следует также отключать отслеживание для быстродействия?
microuser
В таком случае объект ef даже не создаётся, поэтому и отключать нечего. А в общем имеет смысл делать две модели, одну для запросов на чтение без трекинга, и на запись с трекингом
AgentFire
Почему не создаётся? Там пулинг по дженерику?
А, все,понял, акцент на слове ef.
mushegovdima Автор
в таком случае нет смысла отключать трэйкинг, ибо он и не будет использоваться (он работает только если напрямую доставать объекты классов из базы)
Getequ
Откуда программисту на С# вообще узнать о AsNoTracking если он работал только с NHibernate, а про EF слышит только на презенташках/новостях от MS ?
Молодёж и подавно не знает об msdn
Vitimbo
Если человек пользуется технологией, но не знает, как она работает, то проблема явно не в трекинге или другом фреймворке. Молодежь также не знает, что такое nhibernate, поскольку все туториалы предлагают использовать ef. Если только не заниматься археологией. А понятние о трекинге всплывает довольно быстро.