Jak profilować zapytania w Entity Framework?

Wprowadzenie

Entity Framework jest bardzo fajnym narzędziem bardzo ułatwiającym pracę z bazami danych. Budowanie zapytań SQL na podstawie Linq, tworzenie struktury bazy danych na podstawie obiektów w C#, czy mechanizm migracji, powodują, że wielu programistów .NET wybiera Entity Framework. Niestety Entity Framework ma również swoje ciemne strony. Wystarczy chociażby wrócić do wpisu, w którym poruszałem problem aktualizowanie oraz usuwania danych.

Pracując z Entity Framework należy analizować jakie operacje są wykonywane na bazie. Na przykład bardzo łatwo o wykonywanie zbyt dużej liczby operacji na bazie danych, czy wykonywanie ich w nieefektywny sposób. Dlatego warto od czas do czasu (a najlepiej cały czas) korzystać z jakiegoś profilera, w którym będziemy widzieli dokładnie co dzieje się w bazie. Można skorzystać z jakiegoś profilera do silnika bazy danych, którego używamy. Czasami lepszym rozwiązaniem jest skorzystanie z dedykowane profilera dla Entity Framework, który daje większe możliwości. W tym wpisie właśnie pokaże jeden z dostępnych profilerów – EFProf – http://hibernatingrhinos.com/products/efprof.

W wpisie przyjrzymy się również jakie są najczęściej popełniane błędy, które jest wstanie nam wyłapać profiler. Zobaczysz też w jaki sposób można je poprawić, aby aplikacja działała wydajniej.

EFProf

Entity Framwork Profiler jest jednym z dostępnych profilerów dla Entity Framework. Używam go od wielu lat i jestem zadowolony z jego działania. Jest on płatny, ale można też przetestować profiler za darmo przez 30 dni.

Profiler udostępnia szereg informacji o wykonywanych zapytaniach. Mamy przede wszystkim listę wszystkich obiektów kontekstowych oraz zapytań wykonywanych przez poszczególne obiekty:

efprof 1

Widzimy również informacje o poszczególnych zapytaniach. Ich treść, czas, liczbę zwróconych wierszy. Dodatkowo mamy też informację o stosie wywołań. Jest to istotne szczególne, gdy korzystamy z lazy loading. Wtedy jesteśmy w stanie bardzo łatwo znaleźć miejsca, gdzie są wykonywane dodatkowe zapytania (szczególnie, gdy w aplikacji występuje problem N + 1 zapytań, o który więcej będzie w dalszej części wpisu). Możemy również łatwo przejść do miejsca w kodzie w Visual Studio z profilera (klikamy dwa razy na wywołanie w stosie).

Skorzystanie z profilera w aplikacji jest bardzo proste. Wystarczy dodać referencje do biblioteki HibernatingRhinos.Profiler.Appender.dll i przy starcie aplikacji wywołać metodę HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();

Profiler udostępnia też sporo ciekawych statystyk oraz umożliwia analizę zapytań.

Alerty

Kolejną bardzo fajną funkcjonalnością (z której korzystam najwięcej) są alerty. Rozpoczęcie prazy z Entity Framework jest bardzo proste, ale niestety równie prosto o popełnienie prostych błędów, który później dość mocno rzutują na wydajność aplikacji. Właśnie EFProf potrafi wyłapywać takie sytuacje i informuje nas o tym właśnie w formie alertów.

Na zrzucie ekranu powyżej, w liście obiektów kontekstowych widać kolorowe kropki (szara – problemy mniej istotne oraz czerwona – problemy bardzo istotne). Informują one o tym, że dany kontekst wykonał problematyczne zapytanie lub zapytania. Podobne kropki widać w liście wykonanych zapytań. Po wybraniu zapytania możemy przejrzeć alerty w zakładce Alerts pod listą zapytań:

efprof 2

Jak widać na zrzucie powyżej, zaznaczone zapytanie wygenerowało dwa alerty. Jeden bardzo istotny (SELECT N + 1 – kolor czerwony) oraz drugi mniej istotne (Unbounded result set – kolor szary). Profiler udostępnia linki (read more), które w skazują na artykuły z informacją o błędzie, jakie mogą mieć konsekwencje w działaniu aplikacji oraz jak je naprawić.

Zobaczmy kilka najczęściej popełnianych błędów.

SELECT N + 1

Lazy Loading jest mechanizmem, który ułatwia tworzenie aplikacji. Nie musimy pisać kolejnych zapytań, aby pobrać powiązane dane. Niestety korzystanie z Lazy Loadingu na dłuższą metę może mieć sporo konsekwencje w wydajności. Jednym z problemów z Lazy Loading jest problem N + 1 zapytań. Najlepiej problem zobaczyć na przykładzie.

W przykładowej aplikacji przygotowałem dwie klasy, który posłużą nam do testów. Jest to klasa Category oraz klasa Product. Są one powiązane między sobą relacją jeden do wielu:

Dane z bazy są pobierane za pomocą prostego repozytorium:

Testowa akcja w kontrolerze zwraca w formie jsona listę wszystkich kategorii z listą produktów przypisaną do każdej z kategorii. Kontroler pobiera dane za pomocą repozytorium oraz mapuje je z wykorzystaniem Automappera do viewmodeli i zwraca z akcji:

Wykonanie testowej akcji w profile wygląda tak:

efprof 3

Jak widać wykonanych zostało łącznie 11 zapytań (aplikacja podczas tworzenia struktury bazy danych dodaje testowe dane z wykorzystaniem biblioteki Bogus). Pierwsze zapytanie zwraca listę aktywnych kategorii natomiast każde kolejne zwraca listę produktów z poszczególnych kategorii. Jak widać jest to bardzo nieefektywne. Z jednej strony powoduje, że akcja wykonuje się dużo dłużej. Z drugiej baza danych jest bardziej obciążona przez co może obsłużyć mniejszą liczbę użytkowników.

Profiler podpowiada nam, w którym miejscu naszego kodu są wykonywane dodatkowe zapytania (zrzut ekranu powyżej pokazuje zakładkę Stack Trace drugiego zapytania). Jak widać zapytanie odbywa się podczas mapowania i powodowane jest przez Lazy Loading – podczas mapowania właściwości Products z klasy Category. Użycie profilera tutaj jest o tyle przydatne, ponieważ w praktyce nie mamy jawnie wywołanego użycia właściwości Products przez co można łatwo to przegapić. Wiele krotnie widziałem projekty, w których pod spodem było wykonywane takie mapowanie z dodatkowymi zapytaniami, gdzie programiści nie byli tego świadomi do momentu uruchomienia profilera.

SELECT N + 1 – rozwiązanie

Problem ten można rozwiązać na kilka sposób. Najprostszym i najszybszym, który nie wymaga zbyt dużo zmian w kodzie, jest skorzystanie z metody Include. Metoda ta powoduje, że powiązane encje zostaną załadowane w jednym zapytaniu. W przykładzie dodałem nową metodę (GetAllActiveWithProducts) do repozytorium, która zwraca listę aktywnym kategorii z od razu załadowanymi produktami (Early Loading). W kontrolerze dodałem drugą akcje (EarlyLoading), która zwraca tego samego jsona, ale wykorzystuje nową metodę z repozytorium:

Wykonanie nowej akcji w profilerze wygląda tak:

efprof 4

Tym razem wykonało się tylko jedno zapytanie, które zwraca dane z dwóch tabel z użyciem LEFT OUTER JOIN.

Unbounded Result Set

Kolejnym często popełnianym błędem jest nielimitowanie ilości zwracanych danych. W testowych bazach danych na ogól nie mamy zbyt dużo danych. Przez co bardzo łatwo o przegapienie, że jakiś widok może mieć problem z wydajnością, gdy danych będzie zbyt dużo. Podczas testów, gdy widok wyświetlał 10 rekordów wszystko jest dobrze. Natomiast po wdrożeniu u klienta nagle zaczynają się problemu ponieważ aplikacja pobiera 100 tyś obiektów, które następnie próbuje wyświetlić.

Dlatego warto zawsze w jakiś sposób ograniczać liczbę zwracanych rekordów. EFProf przypomina nam o tym w formie alertu. Najlepiej zobaczyć to na przykładzie. Akcja Index z ProductController zwraca listę wszystkich produktów w bazie:

Wykonanie akcji Index w profilerze wygląda tak:

efprof 5

Widać, że wykonało się jedno zapytanie, które ma dwa alerty: Large number of rows returned oraz Unbounded result set. Pierwszy wyświetlił się ponieważ zapytanie zwróciło dużo danych (2300 wierszy). Co istotne wyświetla się ono, gdy mamy dużo danych w bazie. Nie zobaczymy go, gdy danych jest mało (zobacz pierwszy przykład, nie pojawił się on, gdy wyświetlane było 10 kategorii). Przy małych testowych bazach możemy w ogóle nie zobaczyć tego alertu.

Drugi alert myślę, że jest istotniejszy. Pojawia się, gdy w zapytaniu nie ma ograniczenia ilości danych. Warto za każdym razem zastanowić się, czy liczba danych będzie mała, czy dużo w produkcyjnych aplikacjach. Na przykład w tabeli typowo słownikowej (np. status zamówienie) nie będziemy mieć dużej ilości danych, więc nie musimy się tym problemem przejmować. Natomiast w przypadku produktów już tak.

Unbounded Result Set – rozwiązanie

Najprostszym rozwiązaniem tego problemu jest pobieranie danych w paczkach (np. po 100 elementów). Od strony interfejsu użytkownika możemy wykorzystać stronicowanie lub coraz popularniejsze w ostatnim czasie wirtualne ładowanie danych (aplikacja doładowuje nowe dane, gdy użytkownik dojdzie do końca wcześniej załadowanych danych).

W przykładzie dodałem proste stronicowanie:

Wynik wykonania akcji w profilerze:

efprof 6

Tym razem nie mamy już alertów i widać, że liczba zwróconych wierszy jest dużo mniejsza – nie będzie powodować problemów z wyświetlaniem danych w aplikacji.

Przykład

Tradycyjnie na githubie (https://github.com/danielplawgo/EFProfExample) znajduje się przykład do wpisu. Po jego pobraniu należy w web.config ustawić connection stringa do bazy testowej. Pierwsze uruchomienie aplikacji spowoduje wygenerowanie struktury bazy danych oraz dodanie danych testowych. Na stronie głównej są linki do poszczególnych akcji, które możesz przetestować.

Podsumowanie

Entity Framework jest bardzo fajnym narzędziem, które lubię wykorzystywać w projektach. W szczególności mechanizm migracji jest bardzo przydatny. Niestety nieumiejętne korzystaniem z Entity Framework może spowodować sporo problemów. Dlatego warto od czasu do czasu przeanalizować jak z niego korzystamy. Do tego przydaje się dedykowany profiler, który jest w stanie znaleźć problemy i nas o nich poinformować. Na przykładzie pokazałem Ci dwa najczęściej popełniane błędy, ale EFProf potrafi jeszcze dużo więcej.

3 thoughts on “Jak profilować zapytania w Entity Framework?

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *