[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, но все пак ги оставих, за да се видят и тези оптимизации.
Това е Таблицата с резултатите, които получих.
Ето няколко извода които извлякох от тестовете:
- Нещо, което се подразбира, но все пак съм го виждал и на лекции - използването на
Console.WriteLineпри замервания не само, че добавя време, но и обърква реалните пропорции м/у времената на заявките. - В някои случаи използването на
Includeсякаш се пренебрегва, например когато след това се търсиCountна резултатите, в други материализацията сToListследIncludeпоказва много (10 пъти в конкретният тест 5) по-голяма разлика спрямо случаят безInclude, при слаба материализация сFirstOrDefault -2 пъти повече време на съответните заявки. Includeне помага при foreach, нито приToList(което е същото, тоестN + 1проблема). Даже времето сIncludee няколко пъти повече(над 4 от тестове 7, 8, 9, 10). Има ли смисъл да се използва въобще Eager loading с Include, за да си спестите заявки.SQL Serverизползва сторнати процедури за проблемаN+1и тяхното комбинирано изпълнение в тестовете е много по-бързо от едната заявка сInclude. Предполагам, че това все пак зависи от конкретната база или колко записи има в джойнатият обект.- Използването на
ToListв края не е по-бързо от използването му в началото или по средата, при условие, че предходните linq изрази в единият случай са същите като следващите в другият случай. Select [Column]като предпочитан вариант нa Include(Select *) не е по-бърз от съответниятInclude, за предпочитане е заради паметта. Ако анонимните обекти са проблем, то може да се селектне вData Transfer Objectили т.н.DTO.Native заявкавEntity Frameworkне е по-бърза от съответните заявки (тестове 17, 18), затова и може би се радва на ползване само при заявки без връщанe на резултат -add, update, delete.
На мен ли така ми се струва или оптимизациите в EF и SQL са на много високо ниво? Да, използваната база е проста, малка, представлява само 1 случай от хилядите възможни, но така или иначе досега не съм видял замервания които да опровергават горните изводи. Затова ми е интересно дали някой колега се е занимавал с performance и какво може да сподели по темата.
Благодаря.
Точка 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() дърпа повече памет в някои стратегии, докато в други намалява паметта, а разликата в скоростта с него не е много голяма. За съжаление тука никакви метафори не вървят, вземат се предвид много повече неща от това, дали първо да избираме шоколадите и след това да ни ги дават. Може понякога клиента да е по-оправен от продавача и алгоритъмът му на събиране от купчината да е по-добър. Освен всичко друго, може и продавачът да е зает, да му е по-лесно да ти даде цялата купчина и да ти каже "аз съм зает, ако искаш оправяй се, нали можеш и сам".
Имах предвид, че ако съм потребител и искам да видя една картинка от базата, моята машина няма нужда да тегли всички картинки, че да ми селектне тази, която аз искам да видя(смисъла на примера със шоколадите). Ако направиш ToList() преди да си избрал конкретно това, което искаш да видиш, ще събереш сташно много информация, която не ти трябва.
И аз си мислех така преди. Въпросът е дали sql прави по-бързо това, което прави само linq. От това, което мерих, излиза, че паметта използвана с ToList() първо при някои стратегии е по-малко, а скоростта е кажи-речи същата. Излиза, че linq на локално ниво се справя доста добре, дори и цената в памет да се използва локално може да е примамлива. А иначе, споменатият от тебе конкретен пример звучи като случай 3, където се използва ToList().FirstOrDefault(), което наистина е безсмислено, но го направих доколкото си спомням, за да видя дали има някакви оптимизации. Е явно няма, а такава заявка надали някой ще направи и без това.
Да, 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 и не се пали всеки път връзката до базата :)
E, aз си правя връзка с базата преди всяко измерване като викам Count.
Що се отнася до натоварването на паметта, взимането на записите с ToList() има чудовищен overhead. Например за 1 таблица с 2 стринга и PK int, За 2 000 000 записа .mdf-a имаше 136 МБ големина, като паметта за стринговете + PK беше 59 MB. (къде ли отиват другите 77MB??). При вземането на тези записи с TоList() паметта беше 890 МB, което си е 450B(
) на запис. Без кеширането падна на 154 МB, почти колкото големината на mdf-a.
Съгласен съм, че при такива размери на таблиците изглежда. че само сортирането е по-добре да се извършва локално.