socialgekon.com
  • Основен
  • Разпределени Екипи
  • Публикуване
  • Управление На Проекти
  • Мобилен Дизайн
Наука За Данни И Бази Данни

Дълбоко потапяне в производителността на Entity Framework при използване на „Съдържа“

По време на ежедневната си работа използвам Entity Framework. Той е много удобен, но в някои случаи работата му е бавна. Въпреки че има много добри статии за подобрения на производителността на EF и са дадени някои много добри и полезни съвети (например, избягвайте сложни заявки, параметри в Skip and Take, използвайте изгледи, избирайте само необходимите полета и т.н.), не толкова много наистина да се направи, когато трябва да използвате сложни Contains на две или повече полета - с други думи, когато присъедините данни към списък с памет .

Проблем

Нека проверим следния пример:

var localData = GetDataFromApiOrUser(); var query = from p in context.Prices join s in context.Securities on p.SecurityId equals s.SecurityId join t in localData on new { s.Ticker, p.TradedOn, p.PriceSourceId } equals new { t.Ticker, t.TradedOn, t.PriceSourceId } select p; var result = query.ToList();

Кодът по-горе изобщо не работи в EF 6 и въпреки това прави работа в EF Core, присъединяването всъщност се извършва локално - тъй като в моята база данни има десет милиона записа, всичко от тях се изтеглят и цялата памет се изразходва. Това не е грешка в EF. Очаква се. Не би ли било фантастично обаче, ако имаше нещо, което да реши това? В тази статия ще направя някои експерименти с различен подход, за да заобиколя това затруднение в изпълнението.

Решение

Ще изпробвам различни начини да постигна това, започвайки от най-простия до по-напредналия. На всяка стъпка ще предоставя код и показатели, като време и използване на паметта. Имайте предвид, че ще прекъсна изпълнението на програмата за сравнителен анализ, ако тя работи по-дълго от десет минути.

Кодът за програмата за сравнителен анализ се намира в следното хранилище . Той използва C #, .NET Core, EF Core и PostgreSQL. Използвах машина с Intel Core i5, 8 GB RAM и SSD.

Схемата на DB за тестване изглежда така:

Таблици в базата данни: цени, ценни книжа и ценови източници

Само три таблици: цени, ценни книжа и ценови източници. Таблицата с цените има десетки милиони записи.

Вариант 1. Прост и наивен

Нека опитаме нещо просто, само за да започнем.

var result = new List(); using (var context = CreateContext()) { foreach (var testElement in TestData) { result.AddRange(context.Prices.Where( x => x.Security.Ticker == testElement.Ticker && x.TradedOn == testElement.TradedOn && x.PriceSourceId == testElement.PriceSourceId)); } }

Алгоритъмът е прост: За всеки елемент в тестовите данни намерете подходящ елемент в базата данни и го добавете към колекцията от резултати. Този код има само едно предимство: Той е много лесен за изпълнение. Освен това е четлив и поддържаем. Очевидният му недостатък е, че е най-бавният. Въпреки че и трите колони са индексирани, режийните разходи за мрежова комуникация все още създават пречка за ефективността. Ето показателите:

Резултати от първия експеримент

Така че за голям обем отнема приблизително една минута. Консумацията на памет изглежда разумна.

Вариант 2. Наивен с паралел

Сега нека се опитаме да добавим паралелизъм към кода. Основната идея тук е, че удрянето на базата данни в паралелни нишки може да подобри цялостната производителност.

var result = new ConcurrentBag(); var partitioner = Partitioner.Create(0, TestData.Count); Parallel.ForEach(partitioner, range => { var subList = TestData.Skip(range.Item1) .Take(range.Item2 - range.Item1) .ToList(); using (var context = CreateContext()) { foreach (var testElement in subList) { var query = context.Prices.Where( x => x.Security.Ticker == testElement.Ticker && x.TradedOn == testElement.TradedOn && x.PriceSourceId == testElement.PriceSourceId); foreach (var el in query) { result.Add(el); } } } });

Интересното е, че за по-малки набори от тестови данни този подход работи по-бавно от първото решение, но за по-големи проби е по-бърз (в този случай приблизително 2 пъти). Консумацията на памет се променя малко, но не значително.

Резултати от втория експеримент

Вариант 3. Множество съдържа

Нека опитаме друг подход:

  • Подгответе 3 колекции от уникални стойности на Ticker, PriceSourceId и Date.
  • Изпълнете заявката с филтриране с едно изпълнение, като използвате 3 съдържа.
  • Проверете отново локално (вижте по-долу).
var result = new List(); using (var context = CreateContext()) { var tickers = TestData.Select(x => x.Ticker).Distinct().ToList(); var dates = TestData.Select(x => x.TradedOn).Distinct().ToList(); var ps = TestData.Select(x => x.PriceSourceId) .Distinct().ToList(); var data = context.Prices .Where(x => tickers.Contains(x.Security.Ticker) && dates.Contains(x.TradedOn) && ps.Contains(x.PriceSourceId)) .Select(x => new { x.PriceSourceId, Price = x, Ticker = x.Security.Ticker, }) .ToList(); var lookup = data.ToLookup(x => $'{x.Ticker}, {x.Price.TradedOn}, {x.PriceSourceId}'); foreach (var el in TestData) { var key = $'{el.Ticker}, {el.TradedOn}, {el.PriceSourceId}'; result.AddRange(lookup[key].Select(x => x.Price)); } }

Този подход е проблематичен. Времето за изпълнение зависи много от данните. Може да извлече само необходимите записи (в този случай ще бъде много бързо), но може да върне много повече (може би дори 100 пъти повече).

Нека разгледаме следните тестови данни:

Данни за отговора

Тук попитам цените за Ticker1, търгувани на 01.01.2018 и за Ticker2, търгувани на 02.01.2018. Четири записа обаче всъщност ще бъдат върнати.

Уникалните стойности за Ticker са Ticker1 и Ticker2. Уникалните стойности за TradedOn са 2018-01-01 и 2018-01-02.

И така, четири записа съответстват на този израз.

Ето защо е необходима локална повторна проверка и защо този подход е опасен. Показателите са както следва:

Резултати от третия експеримент

Ужасно потребление на памет! Тестовете с големи обеми се провалиха поради изчакване от 10 минути.

Вариант 4. Конструктор на предикати

Нека променим парадигмата: Нека изградим добро старо Expression за всеки набор от тестови данни.

var result = new List(); using (var context = CreateContext()) { var baseQuery = from p in context.Prices join s in context.Securities on p.SecurityId equals s.SecurityId select new TestData() { Ticker = s.Ticker, TradedOn = p.TradedOn, PriceSourceId = p.PriceSourceId, PriceObject = p }; var tradedOnProperty = typeof(TestData).GetProperty('TradedOn'); var priceSourceIdProperty = typeof(TestData).GetProperty('PriceSourceId'); var tickerProperty = typeof(TestData).GetProperty('Ticker'); var paramExpression = Expression.Parameter(typeof(TestData)); Expression wholeClause = null; foreach (var td in TestData) { var elementClause = Expression.AndAlso( Expression.Equal( Expression.MakeMemberAccess( paramExpression, tradedOnProperty), Expression.Constant(td.TradedOn) ), Expression.AndAlso( Expression.Equal( Expression.MakeMemberAccess( paramExpression, priceSourceIdProperty), Expression.Constant(td.PriceSourceId) ), Expression.Equal( Expression.MakeMemberAccess( paramExpression, tickerProperty), Expression.Constant(td.Ticker)) )); if (wholeClause == null) wholeClause = elementClause; else wholeClause = Expression.OrElse(wholeClause, elementClause); } var query = baseQuery.Where( (Expression)Expression.Lambda( wholeClause, paramExpression)).Select(x => x.PriceObject); result.AddRange(query); }

Полученият код е доста сложен. Изграждането на изрази не е най-лесното и включва размисъл (който сам по себе си не е толкова бърз). Но това ни помага да изградим една заявка, използвайки много … (.. AND .. AND ..) OR (.. AND .. AND ..) OR (.. AND .. AND ..) .... Това са резултатите:

Резултати от четвъртия експеримент

Дори по-лошо от който и да е от предишните подходи.

Опция 5. Таблица с данни за споделени заявки

Нека опитаме още един подход:

Добавих нова таблица към базата данни, която ще съдържа данни за заявки. За всяка заявка вече мога:

  • Стартиране на транзакция (ако все още не е стартирана)
  • Качване на данни за заявка в тази таблица (временно)
  • Изпълнете заявка
  • Отмяна на транзакция - за изтриване на качени данни
var result = new List(); using (var context = CreateContext()) { context.Database.BeginTransaction(); var reducedData = TestData.Select(x => new SharedQueryModel() { PriceSourceId = x.PriceSourceId, Ticker = x.Ticker, TradedOn = x.TradedOn }).ToList(); // Here query data is stored to shared table context.QueryDataShared.AddRange(reducedData); context.SaveChanges(); var query = from p in context.Prices join s in context.Securities on p.SecurityId equals s.SecurityId join t in context.QueryDataShared on new { s.Ticker, p.TradedOn, p.PriceSourceId } equals new { t.Ticker, t.TradedOn, t.PriceSourceId } select p; result.AddRange(query); context.Database.RollbackTransaction(); }

Първо метрики:

Резултати от петия експеримент

Резултатът е много добър. Много бързо. Консумацията на памет също е добра. Но недостатъците са:

  • Трябва да създадете допълнителна таблица в базата данни, за да изпълните само един вид заявка,
  • Трябва да започнете транзакция (която така или иначе консумира ресурси на СУБД) и
  • Трябва да напишете нещо в базата данни (при операция READ!) - и по принцип това няма да работи, ако използвате нещо като реплика за четене.

Но освен това, този подход е хубав - бърз и четим. И в този случай план за заявки се кешира!

Опция 6. Разширение MemoryJoin

Тук ще използвам NuGet пакет, наречен EntityFrameworkCore.MemoryJoin . Въпреки факта, че в името му има думата Core, той също поддържа EF 6. Нарича се MemoryJoin, но всъщност изпраща посочените данни за заявка като VALUES на сървъра и цялата работа се извършва на SQL сървъра.

Нека проверим кода.

var result = new List(); using (var context = CreateContext()) { // better to select needed properties only, for better performance var reducedData = TestData.Select(x => new { x.Ticker, x.TradedOn, x.PriceSourceId }).ToList(); var queryable = context.FromLocalList(reducedData); var query = from p in context.Prices join s in context.Securities on p.SecurityId equals s.SecurityId join t in queryable on new { s.Ticker, p.TradedOn, p.PriceSourceId } equals new { t.Ticker, t.TradedOn, t.PriceSourceId } select p; result.AddRange(query); }

Метрика:

Резултати от крайния експеримент

Това изглежда страхотно. Три пъти по-бърз от предишния подход - това го прави най-бързият досега. 3,5 секунди за 64K записи! Кодът е прост и разбираем. Това работи с реплики само за четене. Нека проверим генерираната заявка за три елемента:

SELECT 'p'.'PriceId', 'p'.'ClosePrice', 'p'.'OpenPrice', 'p'.'PriceSourceId', 'p'.'SecurityId', 'p'.'TradedOn', 't'.'Ticker', 't'.'TradedOn', 't'.'PriceSourceId' FROM 'Price' AS 'p' INNER JOIN 'Security' AS 's' ON 'p'.'SecurityId' = 's'.'SecurityId' INNER JOIN ( SELECT 'x'.'string1' AS 'Ticker', 'x'.'date1' AS 'TradedOn', CAST('x'.'long1' AS int4) AS 'PriceSourceId' FROM ( SELECT * FROM ( VALUES (1, @__gen_q_p0, @__gen_q_p1, @__gen_q_p2), (2, @__gen_q_p3, @__gen_q_p4, @__gen_q_p5), (3, @__gen_q_p6, @__gen_q_p7, @__gen_q_p8) ) AS __gen_query_data__ (id, string1, date1, long1) ) AS 'x' ) AS 't' ON (('s'.'Ticker' = 't'.'Ticker') AND ('p'.'PriceSourceId' = 't'.'PriceSourceId')

Както можете да видите, този път действителните стойности се предават от паметта на SQL сървъра в конструкцията VALUES. И това прави трика: SQL сървърът успя да извърши операция за бързо присъединяване и да използва правилно индексите.

Има обаче някои недостатъци (може да прочетете повече на моя Блог ):

  • Трябва да добавите допълнителен DbSet към вашия модел (обаче няма нужда да го създавате в DB)
  • Разширението не поддържа класове модели с много свойства: три свойства на низове, три свойства на датата, три свойства на водача, три свойства на плаващ / двоен и три свойства int / byte / long / decimal. Това е повече от достатъчно в 90% от случаите, предполагам. Ако обаче не е, можете да създадете персонализиран клас и да го използвате. И така, СЪВЕТ: трябва да предадете действителните стойности в заявка, в противен случай ресурсите се губят.

Заключение

Сред нещата, които съм тествал тук, определено бих избрал MemoryJoin. Някой друг може да възрази, че недостатъците са непреодолими и тъй като в момента не всички от тях могат да бъдат разрешени, трябва да се въздържаме от използването на разширението. Е, за мен това е все едно да кажа, че не трябва да използвате нож, защото можете да се порежете. Оптимизацията беше задача не за млади разработчици, а за някой, който разбира как работи EF. За тази цел този инструмент може значително да подобри производителността. Кой знае? Може би някой ден някой от Microsoft ще добави основна поддръжка за динамични ЦЕННОСТИ.

И накрая, Ето още няколко диаграми за сравняване на резултатите.

По-долу има диаграма за времето, необходимо за извършване на операция. MemoryJoin е единственият, който върши работата в разумен срок. Само четири подхода могат да обработват големи обеми: две наивни реализации, споделена таблица и MemoryJoin.

Отделено време в различни случаи за всеки експеримент

Следващата диаграма е за консумация на памет. Всички подходи показват повече или по-малко еднакви числа, с изключение на този с множество Contains. Това явление беше описано по-горе.

Консумация на памет в различни случаи за всеки експеримент

Разбиране на основите

Какво представлява DBset в Entity Framework?

DBSet е абстракция, която буквално представлява колекция от обекти (обикновено лениво заредени), съхранявани в таблица. Операциите, извършени на DBSet, всъщност се извършват върху действителните записи на базата данни чрез SQL заявки.

Какво прави Entity Framework?

Entity Framework е рамка за релационно картографиране на обекти, която осигурява стандартен интерфейс за достъп до данни, съхранявани в релационни бази данни (на различни доставчици).

Какво представлява първоначалният подход в Entity Framework?

Подходът с първи код означава, че разработчикът създава класове модели първо преди да бъде създадена действителната DB. Едно от най-големите предимства е съхраняването на модела на базата данни в системите за управление на източника.

Съвети и съображения при избора на шрифт (с инфографика)

Ui Design

Съвети и съображения при избора на шрифт (с инфографика)
Пълен преглед на най-добрите инструменти за визуализация на данни

Пълен преглед на най-добрите инструменти за визуализация на данни

Ui Design

Популярни Публикации
Инсталиране на Django на IIS: Урок стъпка по стъпка
Инсталиране на Django на IIS: Урок стъпка по стъпка
Въведение в търговията с дълбоко обучение в хедж фондове
Въведение в търговията с дълбоко обучение в хедж фондове
Ръководство за здрави модулни и интеграционни тестове с JUnit
Ръководство за здрави модулни и интеграционни тестове с JUnit
Разработване на мобилни уеб приложения: кога, защо и как
Разработване на мобилни уеб приложения: кога, защо и как
Как да накараме отдалечената работа да работи за вас
Как да накараме отдалечената работа да работи за вас
 
Месец в живота - Временни роли на финансовия директор и най-добри практики
Месец в живота - Временни роли на финансовия директор и най-добри практики
Android DDMS: Ръководство за Ultimate Android Console
Android DDMS: Ръководство за Ultimate Android Console
Щъркел, част 2: Създаване на анализатор на изрази
Щъркел, част 2: Създаване на анализатор на изрази
Плащане напред: Разбиране на изкупувания с ливъридж
Плащане напред: Разбиране на изкупувания с ливъридж
Убеждаване и преместване - Ръководство за принципите на дизайна на движението
Убеждаване и преместване - Ръководство за принципите на дизайна на движението
Категории
ИновацияДругиПродукти Хора И ЕкипиAgile TalentUi DesignНачин На ЖивотИнженерно УправлениеПланиране И ПрогнозиранеПъргавПубликуване

© 2023 | Всички Права Запазени

socialgekon.com