Poniższy artykuł to część serii poświęconej zwiększaniu wydajności baz danych w Technologii Ruby on Rails. Tu skupimy się na problemie n+1 zapytań. Jeśli interesuje Cię przyśpieszenie aplikacji, sugeruję przeczytać od początku :). Aplikacja opisywana poniżej jest dostępna na Githubie wraz z wynikami badań.

W aplikacjach zmieniających się na przełomie czasu, dużym problemem jest wydajne komunikowanie się z bazą danych. Niektóre aplikacje serwerowe wykorzystują aż kilkadziesiąt serwerów baz danych podpiętych pod kilkaset serwerów, gdyż pojedyncza baza byłaby zbyt obciążona ilością kierowanych do niej zapytań.

Redukcja liczby zapytań do bazy danych w celu pobrania potrzebnych rekordów może więc odciążyć serwery baz danych aplikacji, a przez to umożliwić podpięcie mniejszej ich ilości i zredukować koszt utrzymywania aplikacji. Zmniejszenie długości pojedynczego zapytania natomiast przekłada się na zwiększenie ilość zapytań, na które serwer może odpowiedzieć w każdej sekundzie.

Problem n+1 zapytań

Problem n+1 zapytań jest szczególnie popularny w aplikacjach bazujących na technologii Ruby on Rails. Polega na wysłaniu do bazy nadmiernej ilości zapytań (dokładnie n+1 dla n zapytań) – jednego, by pobrać listę obiektów, oraz po jednym dla każdego z obiektów na liście. Występuje, kiedy w pętli przechodzącej po kolekcji rekordów dla każdego obiektu wywoływany jest obiekt powiązany z nim relacją jeden-do-wielu.

Najprostszym przykładem ilustrującym ten problem jest relacja artykułu i komentarzy. Wyświetlenie listy dziesięciu komentarzy, gdzie dla każdego komentarza wyświetlony będzie także tytuł artykułu do którego dany komentarz należy, spowoduje wykonanie jednego zapytania do bazy danych w celu pobrania komentarzy oraz po jednym zapytaniu dla każdego komentarza w celu pobrania jego artykułu. Widać to dokładnie w logach serwera widocznych na listingu:

Implementacja powyższego opisu jest pokazana poniżej:

 

 

W przypadku gdy aplikacja generuje problem n+1 zapytań przy ładowaniu listy komentarzy, każde jedno żądanie HTML do serwera obciąża go odrobinę bardziej niż to potrzebne. Widać to bardzo dobrze na wykresach prezentujących czas odpowiedzi serwera gdy problem n+1 zapytań występował (Rysunek 1) oraz kiedy został on wyeliminowany (Rysunek 2).

dataplus1

dataplus1ok

Jedyne, co należy zrobić aby pozbyć się problemu n+1 zapytań w przykładzie powyżej, to zmienić plik app\controllers\comments_controller.rb  na kod poniżej

Metoda inline: includes informuje Ruby on Rails, by artykuły, do których odwołania są w pobieranych komentarzach załadować w jednym zapytaniu do bazy danych, co eliminuje problem n+1 zapytań. Dzięki temu zabiegowi lista zapytań do bazy danych widoczna na listingu poniżej zostanie zredukowana do 2, co znacząco przyśpiesza aplikację. Dla pobrania 100 obiektów zawierających zaledwie po kilka atrybutów czas odpowiedzi uległ zredukowaniu o 39 ms w przypadku najkrótszej odpowiedzi. Dla najdłuższych odpowiedzi różnica wyniosła 251 ms.

Na wykresach 1 i 2 widać znaczną różnicę w wydajności aplikacji. Po usunięciu problemu n+1 zapytań, 94,3% zapytań zostało wykonanych poniżej 1 sekundy, gdzie w porównaniu do poprzedniego wykresu, z zaimplementowanym problemem było to tylko 14,3%. Warto zauważyć, że po usunięciu usterki 81,3% zapytań zostało ukończone poniżej 84 ms, gdzie na wykresie z zaimplementowanym błędem nie było ani jednego takiego zapytania.

W powyższym teście zapytania były generowane jeden po drugim, a witryna ładowała tylko jeden rodzaj wadliwej kolekcji obiektów, niemniej różnica jest bardzo widoczna. W przypadku 15 000 zapytań na sekundę, czego osiągnięciem w samej Japonii chwali się cookpad.com, takie obciążenie ma ogromne znaczenie.

Znajdowanie problemu zapytań n+1

Problemu n+1 zapytań należy w szczególności unikać, niemniej w rzeczywistości jest to trudne, gdyż struktura bazy danych zmienia się bardzo dynamicznie, szczególnie w początkowej fazie projektu, a czas na wprowadzenie zmian jest często ograniczony. Biorąc to pod uwagę wykorzystanie dodatkowych narzędzi, które ułatwiają znajdowanie wystąpień problemu zapytań n+1 może okazać się użyteczne.

W celu znalezienia wszystkich wystąpień problemu n+1 w aplikacji Ruby on Rails pomocnym narzędziem jest wtyczka Bullet.

Podsumowanie

Podczas badań spostrzeżono, że w przypadku gdy wszystkie pobierane komentarze należą do tego samego artykułu, problem n+1 zapytań nie ma widocznego znaczenia, różnica w wynikach oscylowała w granicach 3 ms. Jest to spowodowane faktem, że Ruby on Rails wykonuje cache’owanie zapytań do bazy danych, przez co pierwsze zapytanie jest rzeczywiście kierowane do bazy, lecz w przypadku każdego kolejnego skierowania do bazy tego samego zapytania jego wynik jest pobierany z pamięci podręcznej.

W dużych aplikacjach, dostarczających ogromne ilości mieszanej treści, jest on jednak dość niebezpieczny, gdyż znacznie szybsze jest wykonanie jednego zapytania do bazy, które zwróci 100 rekordów, niż 100 zapytań, które zwrócą po jednym rekordzie.

Źródła:

Sekrety Produktywności

Sekrety Produktywności

DARMOWE szkolenie mailingowe! 10 porad jak zwiększyć produktywność w programowaniu.

Dziękujemy za zapisanie się na kurs. Wkrótce otrzymasz wiadomość email z potwierdzeniem zapisu.

Polub nas na facebooku!

Jeśli podoba Ci się ten blog, polub nas na facebooku! Dzięki temu nie ominą Cię nowe treści!

You have Successfully Subscribed!