Migliorare le prestazioni della tua app Rails con caricamento Eager

Gli utenti amano le applicazioni veloci e accattivanti, e poi si innamorano di loro e li rendono parte della loro vita. Le applicazioni lente, d'altra parte, solo infastidiscono gli utenti e perdono introiti. In questo tutorial, ci assicureremo che non perdiamo più denaro o utenti e comprendiamo i diversi modi per migliorare le prestazioni.

Active Records e ORM sono strumenti molto potenti in Ruby on Rails, ma solo se sappiamo come scatenare e usare quel potere. All'inizio troverai un sacco di modi per eseguire un'attività simile in RoR,ma solo quando si scava un po 'più in profondità, si conoscono effettivamente i costi dell'utilizzo di uno sull'altro. 

È la stessa storia nel caso di ORM e Associazioni in Rails. Di certo rendono la nostra vita molto più facile, ma in alcune situazioni può anche comportarsi da eccessivo.

Facciamo un esempio

Ma prima, generiamo rapidamente un'applicazione fittizia con cui giocare.

Passo 1 

Avvia il tuo terminale e digita questi comandi per creare una nuova applicazione:

rota nuovo blog blog cd

Passo 2

Genera la tua domanda:

rails g scaffold Nome dell'autore: string rails g scaffold Titolo: body: text author: references

Passaggio 3

Distribuilo sul tuo server locale:

rake db: migrate rails s

E quello era! Ora dovresti avere un'applicazione fittizia in esecuzione.

Ecco come dovrebbero apparire entrambi i nostri modelli (Autore e Post). Abbiamo post che appartengono all'autore e abbiamo autori che possono avere molti post. Questa è l'associazione / relazione di base tra questi due modelli con cui giocheremo.

# Post Post della classe di modello < ActiveRecord::Base belongs_to :author end # Author Model class Author < ActiveRecord::Base has_many :posts end

Dai un'occhiata al tuo "Post Controller" - questo è come dovrebbe apparire. Il nostro obiettivo principale sarà solo sul suo metodo di indice.

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

E, ultimo ma non meno importante, la nostra vista indice dei post. Il tuo potrebbe sembrare avere delle linee in più, ma queste sono quelle su cui voglio che ti concentri, specialmente la linea post.author.name.

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

Creiamo solo alcuni dati fittizi prima di iniziare. Vai alla tua console delle rotaie e aggiungi le seguenti linee. O puoi semplicemente andare a http: // localhost: 3000 / messaggi / new e  http: // localhost: 3000 / autori / new per aggiungere alcuni dati manualmente. 

authors = Author.create ([nome: 'John', nome: 'Doe', nome: 'Manish']) Post.create (titolo: 'I love Tuts +', body: ", autore: authors.first) Post.create (titolo: 'Tuts + is Awesome', body: ", autore: authors.second) Post.create (titolo: 'Long Live Tuts +', body:", autore: authors.last) 

Ora che sei tutto pronto, iniziamo il server con rotaie s e colpisci localhost: 3000 / messaggi.

Vedrai alcuni risultati sul tuo schermo come questo.

Quindi tutto sembra a posto: nessun errore, e recupera tutti i record insieme ai nomi degli autori associati. Ma se dai un'occhiata al tuo log di sviluppo, vedrai tonnellate di query eseguite come di seguito.

Post Load (0.6 ms) SELECT "post". * FROM "posts" ORDER BY "posts". "Created_at" DESC Autore Load (0.5 ms) SELECT "authors". * FROM "authors" WHERE "authors". "Id" =? LIMIT 1 [["id", 3]] Autore Load (0.1 ms) SELECT "authors". * FROM "authors" WHERE "authors". "Id" =? LIMIT 1 [["id", 2]] Autore Load (0.1 ms) SELECT "authors". * FROM "authors" WHERE "authors". "Id" =? LIMIT 1 [["id", 1]]

Bene, ok, sono d'accordo che queste sono solo quattro domande, ma immagina di avere 3000 post nel tuo database invece di solo tre. In tal caso, il nostro database sarà inondato da 3.000 + 1 query, motivo per cui questo problema è chiamato N + 1 problema.

Perché otteniamo questo problema?

Quindi, per impostazione predefinita in Ruby on Rails, l'ORM ha il caricamento pigro abilitato, il che significa che ritarda il caricamento dei dati fino al punto in cui ne abbiamo effettivamente bisogno.

Nel nostro caso, per prima cosa è il controller dove viene chiesto di recuperare tutti i post.

def index @posts = Post.order (created_at:: desc) end

La seconda è la vista, in cui ciclichiamo i post recuperati dal controller e inviamo una query per ottenere separatamente il nome dell'autore per ciascun post. Quindi il N + 1 problema. 

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

Come risolviamo il problema?

Per salvarci da tali situazioni, Rails ci offre una funzione chiamata carico impaziente.

Il caricamento di Eager ti consente di precaricare i dati associati (autori)per tutto il messaggi dal database, migliora le prestazioni generali riducendo il numero di query e fornisce i dati che si desidera visualizzare nelle visualizzazioni, ma l'unica possibilità è quella da utilizzare. Gotcha!

Sì perché ne abbiamo tre, e tutti hanno lo stesso scopo, ma a seconda del caso, nessuno di essi può provare a ridurre o ad annullare nuovamente la performance.

preload () eager_load () includes ()

Ora potresti chiedere quale usare in questo caso? Bene, iniziamo con il primo.

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

Salvarla. Colpisci di nuovo l'URL localhost: 3000 / messaggi.

Quindi nessun cambiamento nei risultati: tutto si carica esattamente allo stesso modo, ma sotto il cofano nel registro di sviluppo, quelle tonnellate di domande sono state cambiate nei due seguenti.

SELEZIONA "posts". * FROM "posts" ORDER BY "posts". "Created_at" DESC SELECT "authors". * FROM "authors" WHERE "authors". "Id" IN (3, 2, 1)

Il precarico utilizza due query separate per caricare i dati principali e i dati associati. Questo è in realtà molto meglio che avere una query separata per ogni nome dell'autore (il problema N + 1), ma questo non è abbastanza per noi. A causa del suo approccio a query separate, genererà un'eccezione in scenari come:

  1. Ordina i post in base al nome dell'autore.
  2. Trova i post dall'autore "John" solo.

Proviamo tutti gli scenari con eager_load () uno per uno

1. Ordina i messaggi per nome dell'autore

# Ordina i post in base al nome dell'autore. def index @posts = Post.order ("authors.name"). eager_load (: author) end

Query risultante nei registri di sviluppo:

SELEZIONA "posts". "Id" AS t0_r0, "posts". "Title" AS t0_r1, "posts". "Body" AS t0_r2, "posts". "Author_id" AS t0_r3, "posts". "Created_at" AS t0_r4 , "post". "updated_at" AS t0_r5, "autori". "id" AS t1_r0, "authors". "nome" AS t1_r1, "authors". "created_at" AS t1_r2, "authors". "updated_at" AS t1_r3 DA "post" LEFT OUTER JOIN "authors" su "authors". "Id" = "posts". "Author_id" ORDER BY authors.name 

2. Trova messaggi dall'autore Solo "Giovanni"

# Trova solo i post dell'autore "John". def index @posts = Post.order (created_at:: desc) .eager_load (: author) .where ("authors.name =?", "Manish") fine

Query risultante nei registri di sviluppo:

SELEZIONA "posts". "Id" AS t0_r0, "posts". "Title" AS t0_r1, "posts". "Body" AS t0_r2, "posts". "Author_id" AS t0_r3, "posts". "Created_at" AS t0_r4 , "post". "updated_at" AS t0_r5, "autori". "id" AS t1_r0, "authors". "nome" AS t1_r1, "authors". "created_at" AS t1_r2, "authors". "updated_at" AS t1_r3 DA "post" LEFT OUTER JOIN "authors" su "authors". "Id" = "posts". "Author_id" WHERE (authors.name = 'Manish') ORDER BY "posts". "Created_at" DESC 

3. Scenario N + 1

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

Query risultante nei registri di sviluppo:

SELEZIONA "posts". "Id" AS t0_r0, "posts". "Title" AS t0_r1, "posts". "Body" AS t0_r2, "posts". "Author_id" AS t0_r3, "posts". "Created_at" AS t0_r4 , "post". "updated_at" AS t0_r5, "autori". "id" AS t1_r0, "authors". "nome" AS t1_r1, "authors". "created_at" AS t1_r2, "authors". "updated_at" AS t1_r3 DA "post" LEFT OUTER JOIN "authors" su "authors". "Id" = "posts". "Author_id" ORDER BY "posts". "Created_at" DESC 

Quindi, se osservi le query risultanti di tutti e tre gli scenari, ci sono due cose in comune. 

Primo, eager_load () usa sempre il SINISTRA ESTERNO qualunque sia il caso. In secondo luogo, ottiene tutti i dati associati in una singola query, che sicuramente superano il preload () metodo in situazioni in cui vogliamo utilizzare i dati associati per attività extra come l'ordinamento e il filtraggio. Ma una sola query e SINISTRA ESTERNO può anche essere molto costoso in scenari semplici come sopra, in cui tutto ciò che serve è filtrare gli autori necessari. È come usare un bazooka per uccidere una piccola mosca.

Capisco che questi sono solo due semplici esempi, e negli scenari del mondo reale là fuori può essere molto difficile decidere quello che è meglio per la tua situazione. Questa è la ragione per cui Rails ci ha dato il include () metodo.

Con include (), Active Record si prende cura della decisione difficile. È molto più intelligente di entrambi preload () e eager_load () metodi e decide quale usare da solo.

Proviamo tutti gli scenari con include ()

1. Ordina i messaggi per nome dell'autore

# Ordina i post in base al nome dell'autore. def index @posts = Post.order ("authors.name"). include (: author) end

Query risultante nei registri di sviluppo:

SELEZIONA "posts". "Id" AS t0_r0, "posts". "Title" AS t0_r1, "posts". "Body" AS t0_r2, "posts". "Author_id" AS t0_r3, "posts". "Created_at" AS t0_r4 , "post". "updated_at" AS t0_r5, "autori". "id" AS t1_r0, "authors". "nome" AS t1_r1, "authors". "created_at" AS t1_r2, "authors". "updated_at" AS t1_r3 DA "post" LEFT OUTER JOIN "authors" su "authors". "Id" = "posts". "Author_id" ORDER BY authors.name

2. Trova messaggi dall'autore Solo "Giovanni"

# Trova solo i post dell'autore "John". def index @posts = Post.order (created_at:: desc) .includes (: author) .where ("authors.name =?", "Manish") # Per rails 4 Non dimenticare di aggiungere .references (: autore ) alla fine, @posts = Post.order (created_at:: desc) .includes (: author) .where ("authors.name =?", "Manish"). references (: author) end

Query risultante nei registri di sviluppo:

SELEZIONA "posts". "Id" AS t0_r0, "posts". "Title" AS t0_r1, "posts". "Body" AS t0_r2, "posts". "Author_id" AS t0_r3, "posts". "Created_at" AS t0_r4 , "post". "updated_at" AS t0_r5, "autori". "id" AS t1_r0, "authors". "nome" AS t1_r1, "authors". "created_at" AS t1_r2, "authors". "updated_at" AS t1_r3 DA "post" LEFT OUTER JOIN "authors" su "authors". "Id" = "posts". "Author_id" WHERE (authors.name = 'Manish') ORDER BY "posts". "Created_at" DESC 

3. Scenario N + 1

def index @posts = Post.order (created_at:: desc) .includes (: author) end 

Query risultante nei registri di sviluppo:

SELEZIONA "posts". * FROM "posts" ORDER BY "posts". "Created_at" DESC SELECT "authors". * FROM "authors" WHERE "authors". "Id" IN (3, 2, 1)

Ora se confrontiamo i risultati con il eager_load () metodo, i primi due casi hanno risultati simili, ma nell'ultimo caso ha deciso di passare a preload () metodo per prestazioni migliori.

Fantastico, giusto?

No, perché in questa gara di prestazioni, a volte anche il carico impaziente può essere troppo breve. Spero che alcuni di voi abbiano già notato che ogni volta che usano metodi di caricamento ansiosi SI UNISCE, usano solo SINISTRA ESTERNO. Inoltre, in ogni caso caricano troppi dati inutili nella memoria: selezionano ogni singola colonna dalla tabella, mentre abbiamo solo bisogno del nome dell'autore.

Benvenuto in the Joins

Anche se Active Record consente di specificare le condizioni sulle associazioni caricate con interesse proprio come unisce (), il modo consigliato è usare invece i join. ~ Documentazione Rails.

Come raccomandato nella documentazione delle guide, il unisce () il metodo è un passo avanti in queste situazioni. Si unisce alla tabella associata, ma solo caricando i dati del modello richiesti in memoria come messaggi nel nostro caso. Pertanto, non stiamo caricando inutilmente dati ridondanti nella memoria, anche se, se vogliamo, possiamo farlo anche noi.

Immergiti in alcuni esempi

1. Ordina i messaggi per nome dell'autore

# Ordina i post in base al nome dell'autore. def index @posts = Post.order ("authors.name"). join (: autore) fine

Query risultante nei registri di sviluppo:

SELEZIONA "posts". * FROM "posts" INNER JOIN "authors" ON "authors". "Id" = "posts". "Author_id" ORDER BY authors.name SELEZIONA "authors". * FROM "authors" WHERE "authors" . "id" =? LIMIT 1 [["id", 2]] SELECT "authors". * FROM "authors" WHERE "authors". "Id" =? LIMIT 1 [["id", 1]] SELECT "authors". * FROM "authors" WHERE "authors". "Id" =? LIMIT 1 [["id", 3]]

2. Trova messaggi dall'autore Solo "Giovanni"

# Trova solo i post dell'autore "John". def index @posts = Post.order (published_at:: desc) .joins (: author) .where ("authors.name =?", "John") end

Query risultante nei registri di sviluppo:

SELEZIONA "post". * FROM "posts" INNER JOIN "authors" ON "authors". "Id" = "posts". "Author_id" WHERE (authors.name = 'Manish') ORDER BY "posts". "Created_at" DESC SELECT "authors". * FROM "authors" WHERE "authors". "Id" =? LIMIT 1 [["id", 3]] 

3. Scenario N + 1

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

Query risultante nei registri di sviluppo:

SELEZIONA "post". * FROM "posts" INNER JOIN "authors" ON "authors". "Id" = "posts". "Author_id" ORDER BY "posts". "Created_at" DESC SELECT "authors". * FROM "authors "DOVE" autori "." Id "=? LIMIT 1 [["id", 3]] SELECT "authors". * FROM "authors" WHERE "authors". "Id" =? LIMIT 1 [["id", 2]] SELECT "authors". * FROM "authors" WHERE "authors". "Id" =? LIMIT 1 [["id", 1]]

La prima cosa che potresti notare dai risultati sopra è che il N + 1 il problema è tornato, ma concentriamoci prima sulla parte buona. 

Esaminiamo la prima query da tutti i risultati. Tutti sembrano più o meno così. 

SELEZIONA "post". * FROM "posts" INNER JOIN "authors" ON "authors". "Id" = "posts". "Author_id" ORDER BY authors.name

Raccoglie tutte le colonne dai post. Si unisce piacevolmente a tabelle e ordinamenti o filtra i record a seconda della condizione, ma senza recuperare alcun dato dalla tabella associata. Quale è quello che volevamo in primo luogo.

Ma dopo le prime domande, vedremo 1 o 3 o N numero di query a seconda dei dati nel database, come questo:

SELEZIONA "autori". * DA "autori" DOVE "autori". "Id" =? LIMIT 1 [["id", 2]] SELECT "authors". * FROM "authors" WHERE "authors". "Id" =? LIMIT 1 [["id", 1]] SELECT "authors". * FROM "authors" WHERE "authors". "Id" =? LIMIT 1 [["id", 3]]

Ora potresti chiedere: perché è questo N + 1 problema? È a causa di questa linea, a nostro avviso post.author.name.

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

Questa riga attiva tutte quelle query. Quindi nell'esempio in cui dovevamo solo ordinare i nostri post, non abbiamo bisogno di visualizzare il nome dell'autore nelle nostre visualizzazioni. In tal caso possiamo risolvere questo problema rimuovendo la linea post.author.name dalla vista.

Ma allora potresti chiedere "Hey MK, che dire degli esempi in cui vogliamo visualizzare il nome dell'autore nella vista?" 

Bene, in quel caso, il unisce () il metodo non ha intenzione di risolverlo da solo. Dovremo dirlo unisce () per selezionare il nome dell'autore, o qualsiasi altra colonna dalla tabella per quella materia. E possiamo farlo aggiungendo a selezionare() dichiarazione alla fine, in questo modo:

def index @posts = Post.order (published_at:: desc) .joins (: author) .select ("posts. *, authors.name as nome_utore") fine

Ho creato un alias "author_name" per authors.name. Vedremo perché in appena un secondo.

Query risultante nei registri di sviluppo:

SELECT post. *, Authors.name as nome_autore DA "post" INNER JOIN "authors" su "authors". "Id" = "posts". "Author_id" ORDER BY "posts". "Created_at" DESC 

Eccoci: finalmente una query SQL pulita con no N + 1 problema, senza dati inutili, con solo le cose di cui abbiamo bisogno. L'unica cosa che rimane è usare quell'alias nella tua vista e cambiare post.author.namepost.author_name. Questo perché author_name è ora un attributo del nostro modello Post, e dopo questa modifica questo è come appare la pagina:

Tutto esattamente uguale, ma sotto la cappa molte cose sono cambiate. Se metto tutto in breve, per risolvere il N + 1 dovresti andare per carico impaziente, ma a volte, a seconda della situazione, dovresti prendere le cose sotto il tuo controllo e usarle si unisce per opzioni migliori. È inoltre possibile fornire query SQL raw al file unisce () metodo per più personalizzazione.

Anche il caricamento di joins e bisognerà consentire il caricamento di più associazioni, ma all'inizio le cose possono diventare molto complicate e difficili da decidere l'opzione migliore. In tali situazioni, consiglio di leggere questi due tutorial Envato Tuts + molto utili per ottenere una migliore comprensione dei join e poter decidere l'approccio meno costoso in termini di prestazioni:

  • Uno sguardo più approfondito alle query di selezione avanzate 
  • Lavorare con MySQL e INNER JOIN

Ultimo ma non meno importante, può essere complicato scoprire le aree nella tua applicazione pre-compilazione in cui dovresti migliorare le prestazioni in generale o trovare la N + 1 i problemi. In quei casi consiglio una bella gemma chiamata proiettile. Può avvisarti quando dovresti aggiungere il caricamento desideroso N + 1 query e quando si utilizza il caricamento indesiderato inutilmente.