Słowo na niedzielę: piękno Rails a wydajność SQL

Nie, to nie ma być wątek o tym, czy Railsy są szybkie, albo dlaczego są wolne :wink: Ale z okazji niedzieli może ktoś zechce się podzielić swoją opinią.

Po przeczytaniu bardzo ciekawego dla mnie tekstu http://mysqldump.azundris.com/archives/72-Rubyisms.html (poleconego na http://blog.rubyonrails.com/2007/7/20/rails-and-mysql) zacząłem się przyglądać swojej prostej aplikacji i urodziły mi się dwa pytania.

Pierwsze dotyczy opisanego problemu [color=blue]SELECT *[/color]. Zauważyłem, że moja prostolinijnie napisana aplikacja oczywiście jest pełna takich zapytań, generowanych czy to poprzez normalne find czy poprzez dynamiczne findery. Co prawda nie mam zbyt wiele pól typu text, żeby tak bardzo marwić się o wydajność mojej aplikacji (wewnętrzna firmowa, dla kilku/nastu osób), ale oczywiście chciałbym się uczyć robić rzeczy porządnie.

Mamy więc sytuację, gdzie mając dwa modele: Project i Client powiązane relacją (wiadomo, Project belongs_to :client) stosuję te piękne railsowe skróty:

    @project = Project.find(params[:id])  # w kontrolerze
    ...
    @project.client.name  # w widoku

Chodzi o ten ostatni wiersz. Powoduje on oczywiście wygenerowanie [color=blue]SELECT * FROM clients WHERE (clients.id = 8)[/color] czyli odczytanie wszystkich pól z rekordu client[8], choć przy wykonywaniu tej konkretnej akcji jest potrzebne tylko kilka pól (nick, name) z dość rozbudowanego modelu Client, zawierającego pola typu text. Dla akcji typu [color=red]/project/show/92[/color] nie jest to może problem, bo odczytujemy tylko jeden rekord w relacji. Ale przy wyświetlaniu listy projektów [color=red]/project/index[/color] w logu ląduje ciąg nieprzyjemnych:

Client Load (0.000298)   SELECT * FROM clients WHERE (clients.`id` = 8) 
  Client Load (0.000268)   SELECT * FROM clients WHERE (clients.`id` = 5) 
  Client Load (0.000224)   SELECT * FROM clients WHERE (clients.`id` = 101) 
  Client Load (0.000214)   SELECT * FROM clients WHERE (clients.`id` = 1) 
  Client Load (0.000219)   SELECT * FROM clients WHERE (clients.`id` = 100) 
  Client Load (0.000245)   SELECT * FROM clients WHERE (clients.`id` = 4) 
  Client Load (0.000233)   SELECT * FROM clients WER (clients.`id` = 2) 
  Client Load (0.000213)   SELECT * FROM clients WHERE (clients.`id` = 8) 
  Client Load (0.000203)   SELECT * FROM clients WHERE (clients.`id` = 1) 
  Client Load (0.000213)   SELECT * FROM clients WHERE (clients.`id` = 4) 

To jest oczywiście log z aplikacji odpalonej w środowisku development, ale w production log twierdzi, że na zapytania SQL zostało zużyte 28-29% czasu obsługi requesta, więc całkiem sporo? Jeszcze gorzej jest dla wyświetlania listy zadań, gdzie modelami w relacji są: projekt i klient, a do wyświetlenia listy zadań jest potrzebne tylko kilka pól z tych modeli zależnych.

Oczywiście mogę do odpowiednich modeli dodać kolejną metodę, np. client_short, która wykona find z użyciem opcji :select, dzięki czemu:

# zamiast tego:
@project.client.name

# będzie to:
@project.client_short.name

No i teraz pytanie jest proste: czy jest jakieś bardziej eleganckie rozwiązanie?

Oczywiście nadal kocham Rails za to, że przy szybkim tworzeniu aplikacji mogę po prostu napisać @project.client.name i mieć z głowy, a dopiero w razie problemów z wydajnością mogę trudne miejsca optymalizować czymś mniej pięknym ale wydajniejszym. Ale może jest w Railsach coś równie pięknego, ale polepszającego wydajność? :slight_smile:

Jeszcze drugie pytanie zostało. Też chodzi o czytanie pól typu text. Mój model Task zawiera tekstowe pole description, często używane. Lista zadań (tasks) jest często wyświetlana i bywa długa, więc o wydajność warto powalczyć. Normalnie w widoku typu lista ([color=red]task/index[/color]) pole to nie jest wyświetlane, więc teoretycznie mógłbym odpowiedni find wyposażyć w parametr :select, żeby uniknąć czytania niepotrzebnego pola, dzięki czemu zniknie [color=blue]SELECT * FROM tasks WHERE …[/color]. Ale nie mogę tego zrobić, ponieważ na liście zadań, dla zadań z wypełnionym polem description wyświetlany jest taki fajniutki trójkącik, w który jak się tupnie myszką, to ajaxowo otwiera się div z wyświetloną zawartością tego pola. Tak więc pobierając listę zadań muszę wiedzieć, czy dany atrybut jest pusty (choć nie muszę pobierać jego zawartości). Czy to da się jakoś ładnie zrobić?

Mógłbym do modelu dołożyć atrybut typu boolean, w którym zapisana byłaby informacja o tym, czy dany atrybut typu text jest wypełniony czy NULL. Ale czy to nie jest jakiś overkill?

Znajomy programujący w PHP powiedział mi coś o odpowiednich klauzulach w SQL, które pozwalają warunkowo czytać rekordy czy warunkowo wybierać pola, ale nie wiem jak się to przełoży na generowanie atrybutów w obiektach Rails.

Co sądzicie? Czy może jednak dołożenie pola boolean jest wystarczająco dobrym rozwiązaniem?

Pytanie 1:
Oczywiście że istnieje rails way ;). Mógłbym napisać rozwiązanie, tylko poco skoro możesz je tutaj obejrzeć Eager Loading :slight_smile:
Pytanie 2:
Tak jak napisałeś można dodać nowe pole, albo użyć find_by_sql i dać ISNULL(pole_text), ale pewnie jest lepsze rozwiązanie.
Sam jestem ciekawy jak to zrobić,może ktoś z doświadczonych forumowiczów nas oświeci ?

[quote=wijet]Pytanie 1:
Oczywiście że istnieje rails way ;). Mógłbym się rozpisywać o rozwiązaniu, tylko poco skoro możesz je tu obejrzeć Eager Loading :)[/quote]
Dzięki. Muszę w końcu pewnego dnia dokończyć czytanie Agile… :smiley:

Zrobiłem mały benchmarking trzech wariantów:
1. aktualnej, lamerskiej, wersji aplikacji, gdzie jest proste [color=blue]Task.find(…)[/color], a informacja o projekcie jest pobierana dynamicznym finderem [color=blue]@task.project.name[/color],
2. wersji gdzie dynamiczny finder zastąpiłem [color=blue]@task.project_short.name[/color], gdzie project_short to metoda pobierająca minimalną ilość danych o projekcie poprzez [color=blue]Project.find(self.project_id, :select => “id, name”)[/color],
3. wersji z użyciem eager loading, czyli pobieranie zadań jest poprzez [color=blue]Task.find(…, :include => :project)[/color], a dynamiczny finder z wariantu 1. pozostaje na swoim miejscu.

Do testowania szybkości użyłem programu ab (pewnie zły wybór?). Dla zapytania generującego stronę z 400 obiektami Task (ab generuje 100 powtórzeń) rezultaty są następujące:

1. 90 s  (db: 19%)
2. 83 s  (db: 14%)
3. 69 s  (db: 7%)

Ciekawa jest obserwacja, że lepsze rezultaty uzyskała metoda 3., która co prawda pobiera pełne dane o każdym projekcie z relacji, ale robi to jednym zapytaniem do bazy. Metoda 2. pobiera minimalną ilość danych o projekcie, ale nadal generuje 400 zapytań, czyli dla każdego obiektu Task jedno.

No, to nauczyłem się dziś czegoś… :slight_smile: Dzięki!