Loading...
stambi4a avatar stambi4a 126 Точки

[Useful Info]Databases Advanced - Entity Framework - Performance Measurements или наистина ли N + 1 проблема е проблем?

   На фона на твърденията от много места в нета , че  Lazy трябва да се избягва(заради N + 1 проблема), че ToList преди края е скрит  N + 1 проблем, че Native заявките са най-бързи, че е по-добре да се работи с IQueryable отколкото с IEnumerable и т.н., направих няколко теста, за да видя горе долу колко по-бърз или по-бавен е даден подход.

   Използвах базата BookShopSystem с 1000 редa във всяка от използваните таблици, за версията на EF - най-последната стабилна Entity Framework 6.1.3, както и последният SQL Server - 2016.  В началото преди всяко измерване правих по една малка заявка за да може да е конектната към базата.

    Резултатите ме изненадаха доста, в смисъл, че поне за мен опровергават всички горни твърдения.

    Това са Моделите, като не съм включил тези които не съм използвал при замерванията.

    Това е Measurements  класа, като с коментари съм описал накратко целта на даден метод-тест. Някои от тестовете впоследствие установих, че са излишни, тъй като се отнасят до оптимизациите на Linq, но все пак ги оставих, за да се видят и тези оптимизации.

    Това е Таблицата с резултатите, които получих.

    Ето няколко извода които извлякох от тестовете:

  1. Нещо, което се подразбира, но все пак съм го виждал и на лекции - използването на Console.WriteLine при замервания не само, че добавя време, но и обърква реалните пропорции м/у времената на заявките.
  2. В някои случаи използването на Include сякаш се пренебрегва, например когато след това се търси Count на резултатите, в други материализацията с ToList след Include показва много (10 пъти в конкретният тест 5) по-голяма разлика спрямо случаят без Include, при слаба материализация с FirstOrDefault - 2 пъти повече време на съответните заявки.
  3. Include не помага при foreach, нито при ToList(което е същото, тоест N + 1 проблема). Даже времето с Include  e няколко пъти повече(над 4 от тестове 7, 8, 9, 10). Има ли смисъл да се използва въобще Eager loading с Include, за да си спестите заявки. SQL Server използва сторнати процедури за проблема N+1 и тяхното комбинирано изпълнение в тестовете е много по-бързо от едната заявка с Include. Предполагам, че това все пак зависи от конкретната база или колко записи има в джойнатият обект. 
  4. Използването на ToList в края не е по-бързо от използването му в началото или по средата, при условие, че предходните linq изрази в единият случай са същите като следващите в другият случай.
  5. Select [Column] като предпочитан вариант нa Include(Select *) не е по-бърз от съответният Include, за предпочитане е заради паметта. Ако анонимните обекти са проблем, то може да се селектне в Data Transfer Object или т.н. DTO.
  6. Native заявка в Entity Framework не е по-бърза от съответните заявки (тестове 17, 18), затова и може би се радва на ползване само при заявки без връщанe на резултат - add, update, delete.

   

    На мен ли така ми се струва или оптимизациите в EF и SQL са на много високо ниво? Да, използваната база е проста, малка, представлява само 1 случай от хилядите възможни, но така или иначе досега не съм видял замервания които да опровергават горните изводи. Затова ми е интересно дали някой колега се е занимавал с performance и какво може да сподели по темата. 

    Благодаря.

1
Databases Advanced - Entity Framework 27/11/2016 10:25:22
val4o89 avatar val4o89 240 Точки

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

По точка 4 - измери ли използваната памет? Ако ToList-неш преди да си селектнал необходимите ти данни в анонимен обект ще направиш излишно по-голям трафик, от колкото ти трябва. Като в магазина, първо реши какво ти трябва, и после го кажи на продавачката, а не да кажеш "дай ми шоколадите", тя да ти ги даде и ти чак тогава да избираш кои точно шоколади искаш да купиш.

В крайна сметка се гони максимален performance, което не значи само бързина на действие, а и минимално използване на ресурсите на машината. Ако някой ми изхарчи мобилния интернет, щото съм цъкнал на една картинка, a приложението ми дръпне гигабайт данни, че да ги пресее локално... няма да ми хареса и ще звъня на *88 да питам кво става :D

1
28/11/2016 16:24:42
stambi4a avatar stambi4a 126 Точки

Точка 1 я споменах, защото съм виждал такова измерване от  лекция.

Performance и  памет са различни и често противоречащи си неща, например кеширането. Идеята на Include e точно performance в определени заявки, защото е ясно, че паметта и мрежовият трафик са доста.  До каква степен и дали да се използва Include можем да прочетем тук

Направих обаче замервания на паметта за случите с ToList.

По точка 12, паметта при използване на Where.Select.ToList беше почти една и съща(около 7.2 MB) независимо от условието в where(тоест кои шоколади искам да си купя). Разликата в паметта при нула върнати и 1000 върнати резултати е горе долу същата,+/-5% Това показва, че изпълнението на самата заявка е много по скъпо като памет от данните, които връща. Използването на ToList.Where.Select даде около 3.3 МБ. Само ToList() дава същият резултат. Само Where е 5.1 MB, Select също 5.1 МБ. Очевидно в тези случаи, че ToList() в началото е повече от 2 пъти по-бърза от ToList() в края. ToList() e по-евтина от Where и Select(). Това е при стратегия MigrateDatabaseToLatestVersion.

При DropCreateDatabaseIfModelChanges стратегия само ToList() e 5.5 MB, Select 3.3 MB, Where също 3.3 МБ. Слагането на ToList() преди или след Where() и Select() няма никакво почти никакво значение, има 50 KB повече ако е в началото.

Тези измервания са правени с GC, не с memory profiler.

Та от разгледаните случаи изглежда, че ToList() дърпа повече памет в някои стратегии, докато в други намалява паметта, а разликата в скоростта с него не е много голяма. За съжаление тука никакви метафори не вървят, вземат се предвид много повече неща от това, дали първо да избираме шоколадите и след това да ни ги дават. Може понякога клиента да е по-оправен от продавача и алгоритъмът му на събиране от купчината да е по-добър. Освен всичко друго, може и продавачът да е зает, да му е по-лесно да ти даде цялата купчина и да ти каже "аз съм зает, ако искаш оправяй се, нали можеш и сам".

0
28/11/2016 19:43:44
val4o89 avatar val4o89 240 Точки

Имах предвид, че ако съм потребител и искам да видя една картинка от базата, моята машина няма нужда да тегли всички картинки, че да ми селектне тази, която аз искам да видя(смисъла на примера със шоколадите). Ако направиш ToList() преди да си избрал конкретно това, което искаш да видиш, ще събереш сташно много информация, която не ти трябва.

1
stambi4a avatar stambi4a 126 Точки

И аз си мислех така преди. Въпросът е дали sql прави по-бързо това, което прави само linq. От това, което мерих, излиза, че паметта използвана с ToList() първо при някои стратегии е по-малко, а скоростта е кажи-речи същата. Излиза, че linq на локално ниво се справя доста добре, дори и цената в памет да се използва локално може да е примамлива. А иначе, споменатият от тебе конкретен пример звучи като случай 3, където се използва ToList().FirstOrDefault(), което наистина е безсмислено, но го направих доколкото си спомням, за да видя дали има някакви оптимизации. Е явно няма, а такава заявка надали някой ще направи и без това.

0
RoYaL avatar RoYaL Trainer 6849 Точки

Да, SQL го прави по-бързо, ако таблицата е добре индексирана. Търсене по username в таблица с потребители, където username е unique key (B-дърво или hash), ще произведе от log2 до k сложност, докато търсенето в списъчна структура в паметта ще произведе N сложност. В първия вариант ще трябват 30тина стъпки за 2 млрд елемента, а във втория - 2 млрд стъпки ;)

В допълнение се добавя и overhead-а от това въобще да се транспортират по мрежата тези 2 млрд записа, за да се запишат в паметта + 2 млрд * N byte-a RAM или иначе казано директен OutOfMemoryException.

Имай предвид, че за малки обеми данни почти не си заслужава теста, особено в конзолно приложение, чиято база е на същата машина. Самото запалване на приложението и първата връзка до базата ще са много по-бавни от колкото заявка за 1000 или 5000 записа. Предполага се, че подобни приложения са Keep-Alive и не се пали всеки път връзката до базата :)

1
stambi4a avatar stambi4a 126 Точки

E, aз си правя връзка с базата преди всяко измерване като викам Count.

Що се отнася до натоварването на паметта, взимането на записите с ToList() има чудовищен overhead. Например за 1 таблица с 2 стринга и PK  int, За 2 000 000 записа .mdf-a имаше 136 МБ големина, като паметта за стринговете + PK беше 59 MB. (къде ли отиват другите 77MB??). При вземането на тези записи с TоList() паметта беше 890 МB, което си е 450B(surprise) на запис. Без кеширането  падна на 154 МB,  почти колкото големината на mdf-a.

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

0
29/11/2016 17:26:12
Можем ли да използваме бисквитки?
Ние използваме бисквитки и подобни технологии, за да предоставим нашите услуги. Можете да се съгласите с всички или част от тях.
Назад
Функционални
Използваме бисквитки и подобни технологии, за да предоставим нашите услуги. Използваме „сесийни“ бисквитки, за да Ви идентифицираме временно. Те се пазят само по време на активната употреба на услугите ни. След излизане от приложението, затваряне на браузъра или мобилното устройство, данните се трият. Използваме бисквитки, за да предоставим опцията „Запомни Ме“, която Ви позволява да използвате нашите услуги без да предоставяте потребителско име и парола. Допълнително е възможно да използваме бисквитки за да съхраняваме различни малки настройки, като избор на езика, позиции на менюта и персонализирано съдържание. Използваме бисквитки и за измерване на маркетинговите ни усилия.
Рекламни
Използваме бисквитки, за да измерваме маркетинг ефективността ни, броене на посещения, както и за проследяването дали дадено електронно писмо е било отворено.