Многие, кто использует 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) Избегать загрузки больших коллекций в память, которые планируется править.
Берите на заметку и быстрых решений!
Ссылки:
Контакты: