Pull to refresh

EF Core. Как 1 строчка может добавить x4 к быстродействию запросов к БД?

Level of difficultyEasy
Reading time3 min
Views18K

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

Берите на заметку и быстрых решений!

Ссылки:

Контакты:

Tags:
Hubs:
Total votes 13: ↑6 and ↓7+2
Comments35

Articles