Rails 2.1.0 - Migliorate le performance di Active Record con l’uso della condizione IN di SQL
Con l’uscita di Rails 2.1.0 sono stati introdotti sostanziali miglioramenti all’active record riguardanti le performance nell’uso dell’eager loading, adesso funzionante anche con associazioni polimorfiche.
I miglioramenti
Fino alla penultima versione di rails (2.0.2) non era possibile includere un’associazione polimorfica:
1 MyModel.find(:all, :include => :my_polymorphic_association, :order => "created_at DESC")
restituiva come errore:
1 ActiveRecord::EagerLoadPolymorphicError: Can not eagerly load the polymorphic association :my_polymorphic_association
Questo problema non sussiste più! Inoltre il comportamento dell’include in termini di query SQL è cambiato in meglio, infatti anzichè fare una join vengono eseguite due query più leggere, ottenendo un sostanziale miglioramento in termini di performance.
Una query come questa:
1 Person.find(:all, :include => [:friends])
passa da:
1 SELECT `people`.`id` AS t0_r0, `people`.`name` AS t0_r1, 2 `people`.`surname` AS t0_r2, `friends`.`id` AS t1_r0, 3 `friends`.`nickname` AS t1_r1 FROM `people` 4 LEFT OUTER JOIN `friends` ON friends.person_id = friends.id
a queste due query più veloci:
1 SELECT `people`.`id`, `people`.`name`, `people`.`surname` FROM `people` 2 SELECT `friends`.`id`, `friends`.`nickname` FROM `friends` WHERE (`friends`.person_id IN (5,8,27,42))
E per le vecchie versioni di Rails?
Chiaramente sarebbe utile migrare al rails 2.1.0, ma non sempre il passaggio è veloce. Nei nostri vecchi progetti abbiamo adottato la stessa soluzione con una piccola variante che risolveva il problema degli ordinamenti. Magari con un esempio vi spiego meglio!!
Supponiamo di fare una query e di voler recuperare gli oggetti associati, per esempio:
1 Table.find(:all, :include => [:avatar], :conditions => "created_at DESC")
Questa query mi restituisce i record della tabella “tables” e in eager load gli Avatar associati nell’ordine in cui li desideravo, ma è dispendiosa poichè afflitta dal problema 1+N di Active Record.

Sarebbe interessante riuscire a prendere in un colpo solo tutti gli Avatar associati magari con una query in più (come fa l’active record 2.1):
1 ids = Table.find(:all, :conditions => "created_at DESC").map{|item| item.myrlable_id} 2 Avatar.find(:all, :conditions => ["id IN(?)",ids])
Così facendo si ottengono gli Avatar associati ma anzichè essere restituiti nell’ordine voluto vengono restitituiti come trovati sul db. E’ Chiaro che questo non va bene perchè deve essere mantenuto il primo ordinamento richiesto; per ovviare al problema potete aggiungere l’ordinamento “FIELD(id,#{ids.join(”,”)})” nella seconda query:
1 ids = Table.find(:all, :conditions => "created_at DESC").map{|item| item.myrlable_id} 2 Avatar.find(:all, :conditions => ["id IN(?)",ids], :order => "FIELD(id,)")
In questo modo gli oggetti Avatar vengono restituiti nell’ordine corretto.


Ma non dovrebbe esssere più veloce mysql nel gestire il join piuttosto (visto che è stato concepito per quello) piuttosto che fare 2 query e poi creare il risultato facendolo parsare allo script server side?
Ciao Lorenzo,
quando nelle tabelle interessate cominciano ad esserci centinaia di migliaia di record ti assicuro che la seconda soluzione, quella che è stata ottimizzata in rails 2.1, è veramente più veloce!!
Ciao Duccio,
appena ho un po’ di tempo lo proverò ma ho come l’impressione che si corra il rischio che il numero di elementi ritornati dalla prima query, da mettere poi come parametro nella clausola in della seconda query, possano essere troppi (mi sembra infatti che ci siano dei limiti sul numero di elementi assegnabili come parametro nella clausola in)… cosa ne pensi ? (es. prima tabella 20K record e seconda tabella con 2M record, se il filtro sulla prima tabella mi ritorna ad esempio 5000 id diversi ce la fa sql a passarli nella clausola in senza scoppiare ?)
Dividere un join in query più piccole è notoriamente migliore sotto l’aspetto prestazionale, soprattutto con MySql