[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
проблема). Даже времето сInclude
e няколко пъти повече(над 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.
Съгласен съм, че при такива размери на таблиците изглежда. че само сортирането е по-добре да се извършва локално.