Îmbunătățirea performanței aplicației Rails cu încărcare proastă

Utilizatorii folosesc aplicații rapide și apoi se îndrăgostesc de ele și le fac parte din viața lor. Aplicațiile lentoare, pe de altă parte, doar deranjează utilizatorii și pierd venituri. În acest tutorial, ne vom asigura că nu vom pierde mai mulți bani sau utilizatori și nu vom înțelege diferitele modalități de îmbunătățire a performanței.

Înregistrările active și ORM sunt instrumente foarte puternice în Ruby on Rails, dar numai dacă știm cum să dezlănțuim și să folosim acea putere. La început veți găsi multe modalități de a efectua o activitate similară în RoR,dar numai atunci când sapi un pic mai adânc ai de fapt pentru a cunoaște costurile de a folosi unul peste altul. 

Este aceeași poveste în cazul ORM și asociațiilor în rails. Ei sigur ne fac viața mai ușoară, dar în unele situații putem acționa și ca o suprasolicitare.

Să luăm un exemplu

Dar, înainte de asta, să generăm rapid o aplicație falsă pentru a juca cu ea.

Pasul 1 

Porniți terminalul și tastați aceste comenzi pentru a crea o nouă aplicație:

rails new blog cd blog

Pasul 2

Generați aplicația:

rails g scaffold Numele autorului: string rails g scaffold Titlul postului: string body: text autor: references

Pasul 3

Implementați-o pe serverul dvs. local:

rake db: migrați șinele

Și asta a fost! Acum ar trebui să aveți o aplicație falsă.

Acesta este modul în care ambele modele (Autor și Post) ar trebui să arate. Avem postări care aparțin autorului și avem autori care pot avea multe postări. Aceasta este asocierea / relația de bază dintre aceste două modele pe care le vom juca.

# Post Post class class < ActiveRecord::Base belongs_to :author end # Author Model class Author < ActiveRecord::Base has_many :posts end

Aruncati o privire la controlerul posturilor - asa ar trebui sa arate. Accentul nostru principal va fi numai la metoda indicelui.

# Controller postsController < ApplicationController def index @posts = Post.order(created_at: :desc) end end

Și nu în ultimul rând, afișarea indexului nostru de posturi. A ta poate părea să aibă niște linii suplimentare, dar acestea sunt cele pe care vreau să te concentrezi, mai ales asupra liniei post.author.name.

 <% @posts.each do |post| %>  <%= post.title %> <%= post.body %> <%= post.author.name %>  <% end %>  

Să facem niște date fictive înainte de a începe. Mergeți la consola dvs. de șine și adăugați următoarele linii. Sau puteți merge la http: // localhost: 3000 / posturi / noi și  http: // localhost: 3000 / autori / noi pentru a adăuga manual unele date. 

autori = Author.create ([nume: 'John', nume: 'Doe', name: 'Manish' autor.first) Post.create (titlul: 'Tuts + is Awesome', corp: ', autor: authors.second) Post.create (titlu:' Long Live Tuts + ', organism: 

Acum că sunteți pregătiți, să începem serverul cu rails s și lovit localhost: 3000 / posturi.

Veți vedea câteva rezultate pe ecranul dvs. ca acesta.

Deci, totul pare bine: fără erori, și aduce toate înregistrările împreună cu numele asociate de autor. Dar dacă analizați jurnalul de dezvoltare, veți vedea că multe dintre interogări sunt executate ca mai jos.

Post de încărcare (0,6ms) SELECT "posturi". * FROM "posturi" ORDER BY "posturi". " =? LIMIT 1 [["id", 3]] Încărcare autor (0,1ms) SELECT "autori". * FROM "autori" WHERE "autori". LIMIT 1 [["id", 2]] Încărcare autor (0,1ms) SELECT "autori". * FROM "autori" WHERE "autori". LIMIT 1 [["id", 1]]

Ei bine, bine, sunt de acord că sunt doar patru întrebări, dar imaginați-vă că aveți 3000 de postări în baza dvs. de date, în loc de doar trei. În acest caz, baza noastră de date va fi inundată cu 3 000 + 1 interogări, motiv pentru care această problemă este numită N + 1 problemă.

De ce avem această problemă?

Deci, în mod implicit, în Ruby on Rails, ORM a activat încărcarea leneșă, ceea ce înseamnă că întârzie încărcarea datelor până în momentul în care avem nevoie de ea.

În cazul nostru, mai întâi este controlorul unde este rugat să preia toate posturile.

def index @posts = Post.order (created_at:: desc) sfârșit

În al doilea rând, este punctul de vedere, în cazul în care am buclă prin posturile preluate de către controlor și trimite o interogare pentru a obține numele autorului pentru fiecare post separat. De aici N + 1 problemă. 

<% @posts.each do |post| %> ... <%= post.author.name %>  <% end %>

Cum rezolvăm problema?

Pentru a ne salva de astfel de situații, Rails ne oferă o facilitate numită încărcare incorectă.

Încărcarea încărcată vă permite să încărcați în prealabil datele asociate (autori)pentru toate posturi din baza de date, îmbunătățește performanța generală prin reducerea numărului de interogări și vă oferă datele pe care doriți să le afișați în vizualizările dvs., dar singura capturare aici este cea pe care o utilizați. Te-am prins!

Da, pentru că avem trei dintre ele și toate servesc aceluiași scop, dar, în funcție de caz, oricare dintre ele se poate dovedi a fi o reducere sau o depășire a performanței din nou.

preload () eager_load () include ()

Acum s-ar putea să întrebi pe cine să folosească în acest caz? Să începem cu prima.

indexul def @posts = Post.order (created_at:: desc) .preload (: author) end

Salvați-l. Apăsați din nou adresa URL localhost: 3000 / posturi.

Deci, nu s-au schimbat rezultatele: totul se încarcă exact în același mod, dar sub hota în jurnalul de dezvoltare, acele tone de interogări au fost schimbate în următoarele două.

SELECT "posturi". * FROM "posturi" ORDER BY "posturi". "Create_at" autori DESC DESC "AUTORI"

Preload utilizează două interogări separate pentru a încărca datele principale și datele asociate. De fapt, este mai bine decât să ai o interogare separată pentru fiecare nume de autor (problema N + 1), dar acest lucru nu este suficient pentru noi. Datorită abordării separate a interogărilor, aceasta va arunca o excepție în scenarii cum ar fi:

  1. Ordonează postările după numele autorilor.
  2. Găsiți numai postări de la autorul "John".

Să încercăm toate scenariile Cu eager_load () One by One

1. Ordonați postările după numele autorului

# Ordonează postările după numele autorilor. def index @posts = Post.order ("autors.name") eager_load (: autor) sfârșit

Rezultatul interogării în jurnalele de dezvoltare:

SELECT "posturi" "id" AS t0_r0, "posturi" "titlu" AS t0_r1, "posturi". "Body" AS t0_r2, "posts". , "autori" "AS" t1_r1, "autori" "AS" t1_r1, "autori". Din "mesaje" LEFT OUTOUN JOIN "autori" pe "autori". "Id" = "posturi". "Autor_id" ORDINARE de authors.name 

2. Găsiți mesaje numai de la autorul "John"

# Găsiți numai postări de la autorul "John". def index @posts = Post.order (created_at:: desc) .eager_load (: author) .de unde ("authors.name =?", "Manish")

Rezultatul interogării în jurnalele de dezvoltare:

SELECT "posturi" "id" AS t0_r0, "posturi" "titlu" AS t0_r1, "posturi". "Body" AS t0_r2, "posts". , "autori" "AS" t1_r1, "autori" "AS" t1_r1, "autori". Din "posturi" LEFT OUTOUN JOIN "autori" ON "autori". "Id" = "posturi" "author_id" WHERE (autors.name = 'Manish' 

3. Scenariul N + 1

indexul def @posts = Post.order (created_at:: desc) .eager_load (: author) end 

Rezultatul interogării în jurnalele de dezvoltare:

SELECT "posturi" "id" AS t0_r0, "posturi" "titlu" AS t0_r1, "posturi". "Body" AS t0_r2, "posts". , "autori" "AS" t1_r1, "autori" "AS" t1_r1, "autori". Din "posturi" LEFT OUTOUN JOIN "autori" ON "autori". "Id" = "posturi" "autor_id" ORDER BY "posts". 

Deci, dacă te uiți la interogările rezultate din toate cele trei scenarii, există două lucruri în comun. 

Primul, eager_load () utilizează întotdeauna LEFT OUTOUN JOIN indiferent de caz. În al doilea rând, el obține toate datele asociate într-o singură interogare, care cu siguranță depășește preîncărcare () în situațiile în care dorim să utilizăm datele asociate pentru sarcini suplimentare cum ar fi comanda și filtrarea. Dar o singură interogare și LEFT OUTOUN JOIN poate fi, de asemenea, foarte scump în scenarii simple ca mai sus, în cazul în care tot ce ai nevoie este de a filtra autorii necesari. Este ca și cum ai folosi o bazooka pentru a ucide o zbuciumă mică.

Înțeleg că acestea sunt doar două exemple simple, iar în scenariile din lumea reală poate fi foarte greu să se decidă cel care este cel mai bun pentru situația dvs. Acesta este motivul pentru care Rails ne-a dat include () metodă.

Cu include (), Recordul activ are grijă de decizia dificilă. E mult mai inteligent decât ambele preîncărcare () și eager_load () metode și decide care dintre acestea să se utilizeze singură.

Să încercăm toate scenariile Cu include ()

1. Ordonați postările după numele autorului

# Ordonează postările după numele autorilor. def index @posts = Post.order ("authors.name") include capăt (: autor)

Rezultatul interogării în jurnalele de dezvoltare:

SELECT "posturi" "id" AS t0_r0, "posturi" "titlu" AS t0_r1, "posturi". "Body" AS t0_r2, "posts". , "autori" "AS" t1_r1, "autori" "AS" t1_r1, "autori". Din "mesaje" LEFT OUTOUN JOIN "autori" pe "autori". "Id" = "posturi". "Autor_id" ORDINARE de authors.name

2. Găsiți mesaje numai de la autorul "John"

# Găsiți numai postări de la autorul "John". Definiție index definesc indexul poștal = Post.order (created_at:: desc). include (: autor) .de unde ("authors.name =?", "Manish") # Pentru șine 4 Nu uitați să adăugați .references (: ) în final @posts = Post.order (created_at:: desc) .include (: autor) .de unde ("authors.name =?", "Manish").

Rezultatul interogării în jurnalele de dezvoltare:

SELECT "posturi" "id" AS t0_r0, "posturi" "titlu" AS t0_r1, "posturi". "Body" AS t0_r2, "posts". , "autori" "AS" t1_r1, "autori" "AS" t1_r1, "autori". Din "posturi" LEFT OUTOUN JOIN "autori" ON "autori". "Id" = "posturi" "author_id" WHERE (autors.name = 'Manish' 

3. Scenariul N + 1

def index @posts = Post.order (created_at:: desc). include ((: autor) sfârșit 

Rezultatul interogării în jurnalele de dezvoltare:

SELECT "posturi". * FROM "posturi" ORDER BY "posturi". "Create_at" autori DESC DESC "AUTORI"

Acum, dacă vom compara rezultatele cu eager_load () , primele două cazuri au rezultate similare, dar în ultimul caz a decis în mod inteligent să treacă la preîncărcare () pentru o performanță mai bună.

Minunat, corect?

Nu, pentru că în această cursă de performanță încărcarea uneori eageră poate fi prea scurtă. Sper că unii dintre voi ați observat deja că ori de câte ori folosesc metodele de încărcare dornică se alătură, ei folosesc doar LEFT OUTOUN JOIN. De asemenea, în fiecare caz încarcă prea mult date inutile în memorie - ele selectează fiecare coloană din tabel, în timp ce avem nevoie doar de numele autorului.

Bine ați venit la Joins

Chiar dacă Active Record vă permite să specificați condițiile pe asociațiile incarcate încărcate la fel se alătură (), modul recomandat este de a utiliza în schimb conexiunile. ~ Rails Documentation.

După cum se recomandă în documentația șinelor, se alătură () este cu un pas înainte în aceste situații. Se alătură tabelului asociat, dar numai încarcă datele de model necesare în memorie, cum ar fi posturi în cazul nostru. Prin urmare, nu încărcăm inutil date excesiv în memorie - deși dacă vrem să putem face acest lucru.

Să aruncăm o privire asupra câtorva exemple

1. Ordonați postările după numele autorului

# Ordonează postările după numele autorilor. def index @posts = Post.order ("autors.name")

Rezultatul interogării în jurnalele de dezvoltare:

SELECT "posturi". * FROM "posturi" INNER JOIN "autori" pe "autori". "Id" = " "id" =? LIMIT 1 [["id", 2]] SELECT "autori". * FROM "autori" WHERE "autori". LIMIT 1 [["id", 1]] SELECT "autori". * FROM "autori" WHERE "autori". LIMIT 1 [["id", 3]]

2. Găsiți mesaje numai de la autorul "John"

# Găsiți numai postări de la autorul "John". indexul def @posts = Post.order (published_at:: desc) .joins (: author). unde ("authors.name =?", "John")

Rezultatul interogării în jurnalele de dezvoltare:

SELECT "posturi". * FROM "posturi" INNER JOIN "autori" ON "autori". "Id" = "posts" DESC SELECT "autori". * FROM "autori" WHERE "autori". "Id" =? LIMIT 1 [["id", 3]] 

3. Scenariul N + 1

def index @posts = Post.order (publicat:: desc) .joins (: author) end

Rezultatul interogării în jurnalele de dezvoltare:

SELECT "posturi". * FROM "posturi" INNER JOIN "autori" pe "autori" "id" = "posturi" "author_id" ORDER BY "posts". "WHERE" autori "." Id "=? LIMIT 1 [["id", 3]] SELECT "autori". * FROM "autori" WHERE "autori". LIMIT 1 [["id", 2]] SELECT "autori". * FROM "autori" WHERE "autori". LIMIT 1 [["id", 1]]

Primul lucru pe care îl puteți observa din rezultatele de mai sus este că N + 1 problema este înapoi, dar să ne concentrăm mai întâi pe partea bună. 

Să analizăm prima interogare din toate rezultatele. Toți arătau mai mult sau mai puțin așa. 

SELECT "posturi". * FROM "posturi" INNER JOIN "autori" pe "autori" "id" = "posturi" "autor_id" ORDER BY authors.name

Realizează toate coloanele din postări. Acesta se alătură frumos atât meselor, cât și tipurilor sau filtrează înregistrările în funcție de condiție, dar fără a prelua date din tabelul asociat. Ceea ce am vrut, în primul rând.

Dar după primele întrebări, vom vedea 1 sau 3 sau N numărul de interogări în funcție de datele din baza dvs. de date, după cum urmează:

SELECTAȚI "autori". * FROM "autori" WHERE "autori". "Id" =? LIMIT 1 [["id", 2]] SELECT "autori". * FROM "autori" WHERE "autori". LIMIT 1 [["id", 1]] SELECT "autori". * FROM "autori" WHERE "autori". LIMIT 1 [["id", 3]]

Acum puteți întreba: de ce este asta? N + 1 problema înapoi? Este din cauza acestei linii în opinia noastră post.author.name.

 <% @posts.each do |post| %>  <%= post.title %> <%= post.body %> <%= post.author.name %>  <% end %>  

Această linie declanșează toate aceste interogări. Deci, în exemplul în care trebuia doar să comandăm postările noastre, nu trebuie să afișăm numele autorului în vederile noastre. În acest caz, putem remedia problema eliminând linia post.author.name din vedere.

Dar apoi ați putea întreba: "Hei MK, cum rămâne cu exemplele în care vrem să aflăm numele autorului în vedere?" 

Ei bine, în acest caz, se alătură () metoda nu o va rezolva singură. Va trebui să spunem se alătură () pentru a selecta numele autorului sau orice altă coloană din tabel, pentru cauză. Și o putem face prin adăugarea unui Selectați() declarație la sfârșit, după cum urmează:

def index @posts = Post.order (publicate:: desc) .joins (: author) .select ("posts. *, authors.name ca nume_furnizor") end

Am creat un nume "author_name" pentru authors.name. Vom vedea de ce doar o secundă.

Rezultatul interogării în jurnalele de dezvoltare:

SELECT posturi *, autors.name ca autor_name FROM "posturi" INNER JOIN "autori" ON "autori". "Id" = "posturi" "autor_id" ORDER BY "posts". 

Aici mergem: în final, o interogare SQL curată cu nr N + 1 problemă, fără date inutile, cu doar lucrurile de care avem nevoie. Singurul lucru rămas este să utilizați acest alias în viziunea și schimbarea dvs. post.author.name la post.author_name. Acest lucru se datorează faptului că numele_profesorului este acum un atribut al modelului Post, iar după această modificare se va vedea cum arată pagina:

Totul era exact același, dar sub capotă multe lucruri s-au schimbat. Dacă aș pune totul pe scurt, pentru a rezolva problema N + 1 ar trebui să mergeți încărcare incorectă, dar uneori, în funcție de situație, ar trebui să luați lucrurile sub control și utilizare se alătură pentru opțiuni mai bune. De asemenea, puteți să furnizați interogări SQL brute la se alătură () pentru o mai bună personalizare.

Îmbinările și încărcarea dornică permit, de asemenea, încărcarea mai multor asociații, dar la început lucrurile pot deveni foarte complicate și dificil de a decide cea mai bună opțiune. În astfel de situații, vă recomandăm să citiți aceste două tutoriale Envato Tuts + foarte bune pentru a vă înțelege mai bine participările și pentru a putea decide cea mai puțin costisitoare abordare în termeni de performanță:

  • O privire mai aprofundată la interogările selectate avansate 
  • Lucrul cu MySQL și INNER JOIN

Nu în ultimul rând, poate fi dificil să găsiți zone din aplicația dvs. pre-construită, unde ar trebui să îmbunătățiți performanța în general sau să găsiți N + 1 Probleme. În aceste cazuri vă recomandăm o bijuterie frumoasă numită Glonţ. Vă poate anunța când trebuie să adăugați încărcare proastă N + 1 interogări și atunci când utilizați încărcare nerăbdătoare.

Cod