Mejora el rendimiento de su aplicación Rails con Eager Loading

A los usuarios les gustan las aplicaciones rápidas, y luego se enamoran de ellas y las hacen parte de su vida. Las aplicaciones lentas, por otro lado, solo molestan a los usuarios y pierden ingresos. En este tutorial, nos aseguraremos de no perder más dinero o usuarios, y entenderemos las diferentes formas de mejorar el rendimiento..

Active Records y ORM son herramientas muy poderosas en Ruby on Rails, pero solo si sabemos cómo liberar y usar ese poder. Al principio, encontrarás muchas maneras de realizar una tarea similar en RoR,pero solo cuando profundiza un poco más llega a conocer los costos de usar uno sobre otro. 

Es la misma historia en el caso de ORM y Asociaciones en Rails. Seguro que hacen nuestra vida mucho más fácil, pero en algunas situaciones también pueden actuar como una exageración..

Tomemos un ejemplo

Pero antes de eso, vamos a generar rápidamente una aplicación ficticia para jugar con.

Paso 1 

Inicia tu terminal y escribe estos comandos para crear una nueva aplicación:

rieles nuevo blog cd blog

Paso 2

Genera tu aplicación:

rieles g andamio Nombre del autor: rieles de cadenas g andamio Título del post: cuerpo de la cadena: autor del texto: referencias

Paso 3

Implementarlo en su servidor local:

rastrillo db: migrar carriles s

¡Y eso fue todo! Ahora deberías tener una aplicación ficticia en ejecución.

Así es como deben verse nuestros dos modelos (Autor y Publicación). Tenemos publicaciones que pertenecen al autor, y tenemos autores que pueden tener muchas publicaciones. Esta es la asociación / relación muy básica entre estos dos modelos con los que vamos a jugar..

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

Eche un vistazo a su "Controlador de publicaciones", así debería verse. Nuestro enfoque principal será únicamente en su método de índice..

# Controlador de clase PostsController < ApplicationController def index @posts = Post.order(created_at: :desc) end end

Y por último, pero no menos importante, nuestra vista de índice de publicaciones. Puede parecer que el tuyo tiene algunas líneas adicionales, pero estas son las que quiero que te centres, especialmente la línea con post.author.name.

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

Solo creamos algunos datos ficticios antes de comenzar. Ve a la consola de tus rieles y agrega las siguientes líneas. O simplemente puedes ir a http: // localhost: 3000 / posts / new y  http: // localhost: 3000 / autores / nuevo para agregar algunos datos manualmente. 

author = Author.create ([name: 'John', name: 'Doe', name: 'Manish']) Post.create (título: 'I love Tuts +', body: ", autor: author.first) Post.create (título: 'Tuts + is Awesome', body: ", author: authors.second) Post.create (title: 'Long Live Tuts +', body:", author: authors.last) 

Ahora que ya está todo configurado, comencemos el servidor con rieles y golpear localhost: 3000 / mensajes.

Verás algunos resultados en tu pantalla como este..

Así que todo parece estar bien: no hay errores, y recupera todos los registros junto con los nombres de los autores asociados. Pero si echa un vistazo a su registro de desarrollo, verá toneladas de consultas ejecutándose como a continuación.

Publicar carga (0.6ms) SELECCIONAR "publicaciones". * DE "publicaciones" ORDENAR POR "publicaciones". "Created_at" DESC Carga de autor (0.5ms) SELECCIONAR "autores". * DE "autores" DÓNDE "autores". "Id" =? LÍMITE 1 [["id", 3]] Carga del autor (0.1ms) SELECCIONE "autores". * DE "autores" DÓNDE "autores". "Id" =? LÍMITE 1 [["id", 2]] Carga del autor (0.1ms) SELECCIONE "autores". * DE "autores" DÓNDE "autores". "Id" =? LÍMITE 1 [["id", 1]]

Bueno, está bien, estoy de acuerdo en que son solo cuatro consultas, pero imagina que tienes 3,000 publicaciones en tu base de datos en lugar de solo tres. En ese caso, nuestra base de datos se inundará con 3,000 + 1 consultas, por lo que este problema se denomina N + 1 problema.

¿Por qué tenemos este problema?

Así que de forma predeterminada en Ruby on Rails, el ORM tiene habilitada la carga diferida, lo que significa que retrasa la carga de datos hasta el punto en que realmente lo necesitamos.

En nuestro caso, primero es el controlador donde se le pide que busque todos los mensajes..

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

La segunda es la vista, donde hacemos un bucle a través de las publicaciones que ha obtenido el controlador y enviamos una consulta para obtener el nombre del autor para cada publicación por separado. Por lo tanto, la N + 1 problema. 

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

¿Cómo resolvemos el problema??

Para rescatarnos de tales situaciones, Rails nos ofrece una función llamada carga ansiosa.

La carga impaciente le permite precargar los datos asociados (autores)por todo el puestos desde la base de datos, mejora el rendimiento general al reducir el número de consultas y le proporciona los datos que desea mostrar en sus vistas, pero el único problema aquí es cuál usar. Gotcha!

Sí, porque tenemos tres de ellos, y todos tienen el mismo propósito, pero según el caso, cualquiera de ellos puede demostrar que está reduciendo o sobrevalorando el rendimiento nuevamente..

preload () eager_load () incluye ()

¿Ahora puedes preguntar cuál usar en este caso? Bueno, vamos a empezar con el primero..

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

Guardalo Presiona la URL de nuevo localhost: 3000 / mensajes.

Por lo tanto, no hay cambios en los resultados: todo se carga exactamente de la misma manera, pero bajo el capó en el registro de desarrollo, esas toneladas de consultas se han cambiado a las siguientes dos.

SELECCIONE "publicaciones". * DE "publicaciones" ORDENAR POR "publicaciones". "Created_at" DESC SELECCIONAR "autores". * DE "autores" DONDE "autores". "Id" IN (3, 2, 1)

La precarga utiliza dos consultas separadas para cargar los datos principales y los datos asociados. En realidad, esto es mucho mejor que tener una consulta separada para cada nombre de autor (el problema N + 1), pero esto no es suficiente para nosotros. Debido a su enfoque de consultas separadas, lanzará una excepción en escenarios como:

  1. Ordenar publicaciones por nombre de autores.
  2. Buscar publicaciones del autor "John" solamente.

Probemos todos los escenarios con eager_load () uno por uno

1. Ordenar publicaciones por nombre de autor

# Ordenar publicaciones por nombre de autores. def index @posts = Post.order ("autores.nombre"). eager_load (: autor) fin

Consulta resultante en los Registros de Desarrollo:

SELECCIONE "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 , "posts". "updated_at" AS t0_r5, "autores". "id" AS t1_r0, "autores". "nombre" AS t1_r1, "autores". created_at "AS t1_r2," autores "." updated_at "AS t1_r3 DE "posts" IZQUIERDA EXTERNA IZQUIERDA "autores" ON "autores". "Id" = "posts". "Author_id" ORDENAR POR autores.name 

2. Buscar publicaciones del autor "John" solamente

# Buscar publicaciones del autor "John" solamente. def index @posts = Post.order (created_at:: desc) .eager_load (: author) .where ("authors.name =?", "Manish") end

Consulta resultante en los Registros de Desarrollo:

SELECCIONE "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 , "posts". "updated_at" AS t0_r5, "autores". "id" AS t1_r0, "autores". "nombre" AS t1_r1, "autores". created_at "AS t1_r2," autores "." updated_at "AS t1_r3 DESDE "posts" IZQUIERDA EXTERIOR IZQUIERDA "autores" ON "autores". "Id" = "posts". "Author_id" WHERE (authors.name = 'Manish') ORDENADO POR "posts". 

3. Escenario N + 1

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

Consulta resultante en los Registros de Desarrollo:

SELECCIONE "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 , "posts". "updated_at" AS t0_r5, "autores". "id" AS t1_r0, "autores". "nombre" AS t1_r1, "autores". created_at "AS t1_r2," autores "." updated_at "AS t1_r3 DE "posts" IZQUIERDA EXTERNA IZQUIERDA "autores" ON "autores". "Id" = "posts". "Author_id" ORDENAR POR "posts". "Created_at" DESC 

Entonces, si nos fijamos en las consultas resultantes de los tres escenarios, hay dos cosas en común. 

primero, eager_load () siempre usa el IZQUIERDA COMBINACIÓN EXTERNA cualquiera sea el caso. En segundo lugar, obtiene todos los datos asociados en una sola consulta, lo que sin duda supera la precarga () Método en situaciones en las que deseamos utilizar los datos asociados para tareas adicionales como ordenar y filtrar. Pero una sola consulta y IZQUIERDA COMBINACIÓN EXTERNA También puede ser muy costoso en escenarios simples como los anteriores, donde todo lo que necesita es filtrar los autores necesarios. Es como usar una bazuca para matar a una pequeña mosca..

Entiendo que estos son solo dos ejemplos simples, y en situaciones reales, puede ser muy difícil decidir cuál es la mejor para su situación. Esa es la razón por la que Rails nos ha dado la incluye () método.

Con incluye (), Active Record se encarga de la decisión difícil. Es mucho más inteligente que los dos. precarga () y eager_load () métodos y decide cuál utilizar por su cuenta.

Probemos todos los escenarios con incluye ()

1. Ordenar publicaciones por nombre de autor

# Ordenar publicaciones por nombre de autores. def index @posts = Post.order ("autores.nombre"). incluye (: autor) fin

Consulta resultante en los Registros de Desarrollo:

SELECCIONE "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 , "posts". "updated_at" AS t0_r5, "autores". "id" AS t1_r0, "autores". "nombre" AS t1_r1, "autores". created_at "AS t1_r2," autores "." updated_at "AS t1_r3 DE "posts" IZQUIERDA EXTERNA IZQUIERDA "autores" ON "autores". "Id" = "posts". "Author_id" ORDENAR POR autores.name

2. Buscar publicaciones del autor "John" solamente

# Buscar publicaciones del autor "John" solamente. def index @posts = Post.order (created_at:: desc) .includes (: author) .where ("authors.name =?", "Manish") # Para rieles 4 No olvide agregar .references (: author ) en el final @posts = Post.order (created_at:: desc) .includes (: author) .where ("authors.name =?", "Manish"). references (: author) end

Consulta resultante en los Registros de Desarrollo:

SELECCIONE "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 , "posts". "updated_at" AS t0_r5, "autores". "id" AS t1_r0, "autores". "nombre" AS t1_r1, "autores". created_at "AS t1_r2," autores "." updated_at "AS t1_r3 DESDE "posts" IZQUIERDA EXTERIOR IZQUIERDA "autores" ON "autores". "Id" = "posts". "Author_id" WHERE (authors.name = 'Manish') ORDENADO POR "posts". "Created_at" DESC 

3. Escenario N + 1

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

Consulta resultante en los Registros de Desarrollo:

SELECCIONE "publicaciones". * DE "publicaciones" ORDENAR POR "publicaciones". "Created_at" DESC SELECCIONAR "autores". * DE "autores" DONDE "autores". "Id" IN (3, 2, 1)

Ahora si comparamos los resultados con el eager_load () En este método, los dos primeros casos tienen resultados similares, pero en el último caso se decidió inteligentemente cambiar a precarga () método para un mejor rendimiento.

Impresionante a la derecha?

No, porque en esta carrera de rendimiento, a veces la carga impaciente puede quedarse corta también. Espero que algunos de ustedes hayan notado que cada vez que usan métodos de carga ansiosos utilizan Se une, solo usan IZQUIERDA COMBINACIÓN EXTERNA. Además, en todos los casos cargan demasiados datos innecesarios en la memoria; seleccionan cada columna de la tabla, mientras que solo necesitamos el nombre del autor.

Bienvenidos a los Joins

A pesar de que Active Record le permite especificar condiciones en las asociaciones cargadas de entusiasmo, como une (), La forma recomendada es utilizar uniones en su lugar. ~ Documentación Rails.

Como se recomienda en la documentación de rieles, el une () El método es un paso adelante en estas situaciones. Se une a la tabla asociada, pero solo carga los datos del modelo requeridos en la memoria como puestos en nuestro caso. Por lo tanto, no estamos cargando datos redundantes en la memoria de forma innecesaria, aunque si queremos, podemos hacerlo también.

Vamos a sumergirnos en algunos ejemplos

1. Ordenar publicaciones por nombre de autor

# Ordenar publicaciones por nombre de autores. def index @posts = Post.order ("authors.name"). joins (: author) end

Consulta resultante en los Registros de Desarrollo:

SELECCIONAR "publicaciones". * DE "publicaciones" INNER JOIN "autores" ON "autores". "Id" = "publicaciones". "Author_id" ORDENAR autores.name SELECCIONAR "autores". * DE "autores" DÓNDE "autores" . "id" =? LÍMITE 1 [["id", 2]] SELECCIONAR "autores". * DE "autores" DÓNDE "autores". "Id" =? LÍMITE 1 [["id", 1]] SELECCIONAR "autores". * DE "autores" DÓNDE "autores". "Id" =? LÍMITE 1 [["id", 3]]

2. Buscar publicaciones del autor "John" solamente

# Buscar publicaciones del autor "John" solamente. def index @posts = Post.order (publish_at:: desc) .joins (: author) .where ("authors.name =?", "John") end

Consulta resultante en los Registros de Desarrollo:

SELECCIONE "posts". * DE "posts" INNER JOIN "autores" ON "autores". "Id" = "posts". "Author_id" DONDE (autores.name = 'Manish') ORDENAR POR "posts". "Created_at" DESC SELECCIONAR "autores". * DE "autores" DÓNDE "autores". "Id" =? LÍMITE 1 [["id", 3]] 

3. Escenario N + 1

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

Consulta resultante en los Registros de Desarrollo:

SELECCIONAR "publicaciones". * DE "publicaciones" INNER JOIN "autores" ON "autores". "Id" = "publicaciones". "Autor_id" ORDENAR POR "publicaciones". "Created_at" DESC SELECT "autores". * DE "autores "DONDE" autores "." Id "=? LÍMITE 1 [["id", 3]] SELECCIONAR "autores". * DE "autores" DÓNDE "autores". "Id" =? LÍMITE 1 [["id", 2]] SELECCIONAR "autores". * DE "autores" DÓNDE "autores". "Id" =? LÍMITE 1 [["id", 1]]

Lo primero que puede notar de los resultados anteriores es que la N + 1 El problema está de vuelta, pero primero concentrémonos en la parte buena.. 

Echemos un vistazo a la primera consulta de todos los resultados. Todos ellos se parecen más o menos así.. 

SELECCIONE "posts". * DE "posts" INNER JOIN "autores" ON "autores". "Id" = "posts". "Author_id" ORDENAR POR autores.name

Obtiene todas las columnas de los mensajes. Combina bien las tablas y clasifica o filtra los registros según la condición, pero sin recuperar ningún dato de la tabla asociada. Que es lo que queríamos en primer lugar..

Pero después de las primeras consultas, ya veremos. 1 o 3 o norte Número de consultas según los datos en su base de datos, como este:

SELECCIONE "autores". * DE "autores" DÓNDE "autores". "Id" =? LÍMITE 1 [["id", 2]] SELECCIONAR "autores". * DE "autores" DÓNDE "autores". "Id" =? LÍMITE 1 [["id", 1]] SELECCIONAR "autores". * DE "autores" DÓNDE "autores". "Id" =? LÍMITE 1 [["id", 3]]

Ahora puedes preguntar: ¿por qué es esto? N + 1 problema de vuelta Es debido a esta línea en nuestra opinión post.author.name.

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

Esta línea activa todas esas consultas. Entonces, en el ejemplo en el que solo tuvimos que ordenar nuestras publicaciones, no necesitamos mostrar el nombre del autor en nuestras vistas. En ese caso podemos solucionar este problema eliminando la línea post.author.name desde la vista.

Pero luego podría preguntar: "Oye MK, ¿qué pasa con los ejemplos en los que queremos mostrar el nombre del autor en la vista?" 

Bueno, en ese caso, el une () El método no lo va a arreglar por sí mismo. Tendremos que decir une () para seleccionar el nombre del autor, o cualquier otra columna de la tabla para el caso. Y podemos hacerlo añadiendo un seleccionar() Declaración al final, como esta:

def index @posts = Post.order (publish_at:: desc) .joins (: author) .select ("posts. *, authors.name as author_name") fin

Creé un alias "author_name" para autores.nombre. Ya veremos por que en sólo un segundo.

Consulta resultante en los Registros de Desarrollo:

SELECCIONE publicaciones. *, Autores.nombre como autor_nombre DE "publicaciones" INNER JOIN "autores" ON "autores". "Id" = "publicaciones". "Author_id" ORDENAR POR "publicaciones". "Created_at" DESC 

Aquí vamos: por fin una consulta SQL limpia y sin N + 1 Problema, sin datos innecesarios, con las cosas que necesitamos. Lo único que queda es usar ese alias en tu vista y cambiar post.author.namepost.author_name. Esto se debe a que author_name ahora es un atributo de nuestro modelo de publicación, y después de este cambio, así es como se ve la página:

Todo exactamente igual, pero bajo el capó se cambiaron muchas cosas. Si pongo todo en pocas palabras, para resolver el N + 1 deberias ir por carga ansiosa, pero a veces, dependiendo de la situación, debe tomar las cosas bajo su control y usarlas. se une para mejores opciones. También puede suministrar consultas de SQL en bruto a la une () método para más personalización.

Las uniones y la carga impaciente también permiten la carga de múltiples asociaciones, pero al principio las cosas pueden ser muy complicadas y difíciles de decidir la mejor opción. En tales situaciones, le recomiendo que lea estos dos tutoriales muy buenos de Envato Tuts + para comprender mejor las uniones y poder decidir el enfoque menos costoso en términos de rendimiento:

  • Una mirada más profunda a las consultas de selección avanzada 
  • Trabajando con MySQL e INNER JOIN

Por último, pero no menos importante, puede ser complicado encontrar áreas en su aplicación de precompilación en las que debería mejorar el rendimiento en general o encontrar el N + 1 problemas. En esos casos recomiendo una bonita joya llamada Bala. Puede notificarle cuándo debe agregar carga ansiosa para N + 1 consultas, y cuando se está utilizando la carga ansiosa innecesariamente.