Rails es un gran framework, pero la magia y la ágilidad que te proporciona tiene un precio y no es otro que la velocidad y esto se deriva en problemas de escalabilidad. Para una aplicación con poco tráfico o incluso con un tráfico medio la manera de realizar las consultas en Rails nos puede ser suficiente, pero cuando nos encontramos con una aplicación que soporta un gran volumen de visitas o que maneja una gran cantidad de datos, las consultas “out of the box” a la base de datos se hacen cada vez más lentas hasta llegar a límites insostenibles.
Aquí tenéis algunos consejos para optimizar las consultas a la base de datos y conseguir aumentar considerablemente el redimiento de vuestra aplicación.
Haz pruebas en un entorno real
Las pruebas en local y con una base de datos vacía no sirven de nada. Llena tu base de datos con miles de registros y súbelo al servidor. Haz las pruebas en staging simulando el entorno en el que se ejecutará la aplicación.
Mira la consola y los logs
No cierres la consola donde lances el servidor, verás continuamente que está pasando, las consultas que ejecuta y el tiempo que tardan en ejecutarse. Pega la nariz a la pantalla y revisa linea por linea que está haciendo la aplicación en cada momento.
Reduce el tamaño de los datos que estás accediendo
Extrae de la tabla únicamente aquellos campos que necesitas, normalmente no necesitamos todos los campos de un modelo
User.find(:all) # No User.find(:all, :select => "user.id, user.name, user.email")
Reduce el número de consultas a las base de datos
Vamos a mostrar diez posts y sus comentarios, la manera “normal” sería
@posts = Post.find(:all, :limit => 10) # 1 consulta
@posts.comments.each do |c| ... end # 10 consultas
Esto se puede mejorar bastante incluyendo los comentarios en la consulta donde sacamos los posts
@posts = Post.find(:all, :limit => 10, :include => [:comment]) # 2 consultas
De esta manera nos hemos ahorrado 9 consultas y no es necesario cambiar ni úna línea de código en las vistas para mostrar los comentarios.
Reduce aún más
Combinado los dos ejemplo anteriores podriamos tener algo como
Post.find(:all, :limit => 10, :select => "post.id, post.title, post.excerpt", :include => [:comment])
Esto está muy bien, hemos reducido considerablemente el numero de accesos a la base de datos, sobretodo si el parámetro :limit es muy grande, pero aún se están cargando todos los campos del modelo asociado, en este caso los somentarios. Podemos utilizar el parámetro :joins para especificar qué campos del modelo asociado queremos
@posts = Post.find(:all, :limit => 10, :select => "post.id, post.title, post.excerpt, comment.body AS comment_body', :joins => [:comment])
Utiliza find_by_sql
Algunas consultas son difíciles de optimizar, pero siempre podemos recurrir al método find_by_sql que nos permitirá realizar las consultas exactamente como las queremos.
User.find_by_name("Pepe") # No
User.find_by_sql(SELECT user.* WHERE user.name = 'Pepe')
No utilices consultas complejas, mejor varias consultas simples
No os volváis unos talibanes de la optimización y pretendáis hacerlo todo con una consulta enorme, a veces es mejor realizar más consultas pequeñas, ya que aunque se realicen varios accesos a la base de datos, la suma de los tiempos de latencia puede ser menor que el tiempo que tarda en ejecutarse una mega-consulta.
stuff = Stuff.find_by_sql("SELECT everything.* FROM everything JOIN (box_1, box_2) ON (everything.id = box_1.everything_id box_2.box1_id = box_1.id") # Consulta lenta
# Alternativa
boxes = Boxes.find_by_sql("SELECT box_1.everything_id FROM box_1 JOIN box_2 ON box_2.box1_id = box_1.id")
boxes.each do |box_1|
stuff = Stuff.find_by_sql("SELECT everything.* FROM everything WHERE id = #{box_1.everything_id}
end
Usa tablas MyISAM en lugar de InnoDB
A no ser que necesites transacciones, bloqueos o el nivel de escrituras en tu aplicación sea muy elevado, usa MyISAM, notarás una mejora considerable del rendimiento. Cuando se realiza una migración en Rails, por defecto se crean tablas InnoDB, esto se puede cambiar en las migraciones de la siguiente manera
def self.up
create_table :users, :options => 'engine=MyISAM' do |t|
t.string :name
t.string :surname
t.string :email
t.timestamps
end
end
Puedes crear una migración que pase todas tus tablas InnoDB a MyISAM
class ConvertToMyIsam < ActiveRecord::Migration
def self.up
execute 'ALTER TABLE users ENGINE = MyISAM'
execute 'ALTER TABLE posts ENGINE = MyISAM'
...
end
def self.down
execute 'ALTER TABLE users ENGINE = InnoDB'
execute 'ALTER TABLE posts ENGINE = InnoDB'
...
end
end
Utiliza cache de modelos
Rails 2.1 proporciona más opciones de cache que pueden ser muy útiles para reducir el número de consultas. Supongamos que nuestra aplicación muestra siempre los diez usuarios registrados. Continuamente estaremo realizando una consulta como esta
@users = User.find(:all, :limit => 10, :order => "created_at desc")
Utilizando cahé podemos ahorrarnos esta consulta mientras no haya nuevos usuarios registrados. En el modelo User definimos
def self.recent_cached
Rails.cache.fetch('User.recent_cached') { User.recent }
end
def self.recent
User.find(:all, :limit => 10, :order => 'created_at desc')
end
Utiliza índices
En todos aquellos campos que no sean índices primarios sobre los cuales estés realizando búsquedas, crea un índice que te permita encontrarlos más rápidamente. Por ejemplo si continuamente buscas usuarios por su nickname
@user = User.find_by_sql(SELECT user.* WHERE user.nickname = 'PepeGrillo')
Crea un índice sobre el campo nickname y verás reducido el tiempo de la consulta considerablemente, sobretodo si tienes 100.000 usuarios. Crea una migración
./script/generate migration add_index_to_user
y añade el índice de esta manera
class AddIndexToUser < ActiveRecord::Migration
def self.up
add_index :users, :nickname
end
De momento es todo, espero que estos trucos te sirvan para poder aumentar el rendimento de tu aplicación, pero aún hay más técnicas que puedes utilizar. Las explicaré en un próximo post.



Justo estaba yo pegándome con la optimización de consultas en el proyecto en el que estoy trabajando ahora y este post me viene de perlas.
Gracias Emili
Super bien resumido… hemos aprendido bastante con este último megaproyecto, eh?
Sobre el tema de llenar con muchos datos nuestra base de pruebas, he publicado este post:
http://www.jaimeiniesta.com/2008/09/13/como-generar-contenido-ficticio-para-tus-aplicaciones/
Con respecto al uso de InnoDB y MyISAM he encontrado este post donde dice que no hay tanta diferencia en la velocidad de consulta entre las tablas de un tipo y la de otro.
Está claro que la experiencia empírica es la que manda :)
http://www.forosdelweb.com/f86/faqs-mysql-489891/#post2024134
Un saludo.
Muy buen post si señor!
Gracias!
Buen post, sólo dos apuntes:
- respecto a InnoDB y MyISAM el rendimiento no se nota tanto, y según en qué casos InnoDB es mucho más rápido. Lo interesante es tener un sistema maestro - esclavo y que el maestro sea InnoDB para asegurar transacciones y demás y el esclavo MyISAM para acelerar lecturas.
- hay que tener mucho cuidado con los :include en una relación has_many, ya que, tomando prestado tu ejemplo, si el post tiene 1000 comentarios, estás haciendo una query que devuelve mil filas, y eso no es nada eficiente. El :include hay que utilizarlo en sentido contrario de la relación (cuando seleccionas los comments haces un include del post al que pertence) o en relaciones 1 - 1.
Un saludo!
El tema MyISAM vs InnoDB siempre lleva asociada la polémica :) Tengo pendiente hacer unas pruebas para comparar el rendimiento entre MyISAM e InnoDB en varios escenarios diferentes, ya publicaré los resultados.
En cuanto a lo que comentas , Fernando, de los includes, tienes razón, tal vez debería haberlo detallado mejor. En el ejemplo que puse se hace un limit => 10, en este caso el include no supone una carga excesiva, pero sí es verdad que para un número elevado de registros es mejor hacer el include a la inversa como bien dices.
Gracias por vuestros comentarios.