Resolviendo el problema de n+1 en la cuenta de datos.

En este tutorial vamos a aprender un poco de eager loading en rails, y como evitar los problemas de n+1 cuando se trata de contar elementos hijos o padres y cuál es la diferencia entre los métodos count, size y length a la hora de contar datos.

Setup

Para el ejemplo vamos a ocupar un mini twitter, en este hay tweets y cada tweet pertenece un usuario, o sea dos modelos:

creamos el modelo de usuario

creamos el modelo de tweets

y luego migramos.

Dentro del modelo de usuarios vamos a agregar la relación con los tweets

Para probar vamos a necesitar datos, para eso vamos a agregar la gema faker al gemfile y vamos a utilizar el siguiente archivo seed.

luego corremos el seed.

Además vamos a necesitar el controller de usuarios para mostrar un index

y finalmente hacemos correr el servidor

Contando elementos de la forma iterativa

La forma más fácil de mostrar a todos los usuarios con la cuenta de sus respectiva es en el controller seleccionar a todos, y luego dentro de la vista iterar mostrando los resultados.

o sea nuestro controller users sería:

luego creamos una sencilla vista de usuarios que muestre a los usuarios con la cuenta de tweets.

ahora si observamos nuestro terminal en el tab donde corre el servidor, veremos lo siguiente:

Fig1: Select con count

Lo que sucedió fue que un query que consistía en mostrar a todos los usuarios se terminó convirtiendo en N+1 Query. En mi base de datos el primer usuario es el 2, y el último el 11, o sea hay 10 usuarios y 11 queries y no es coincidencia, es realmente un problema de n+1

Solución: Utilizar includes

Al igual que todos los problemas de n+1 este lo podemos resolver ocupando includes.

Includes permite precargar los datos de la tabla de tweets en memoria, eso lo podemos ver en la siguiente imagen, donde se gatilla una consulta nueva y se seleccionan todos los tweets de los usuarios involucrados

Includes más count

Sin embargo, si revisamos debajo de la precarga de tweets, veremos que todavía tenemos el mismo problema, y esto se debe al count que tenemos en la vista siempre gatilla una consulta count SQL nueva, en lugar de count deberíamos ocupar length, o size.

En cambio si ocupamos size o length, obtendremos lo siguiente:

includes con size

¿Cuál es la diferencia entre count, length y size en Rails?

Vamos a partir explicando que no es lo mismo hablar de esto en rails que en ruby, en rails count realiza siempre un query sql para contar los elementos, length en cambio sólo cuenta si ya la tabla está precargada y si no trae la información a memoria, y size determina de forma automática cual ocupar.

Length parece una muy buena alternativa pero debemos tener cuidado de siempre ocuparlo en conjunto con includes de lo contrario en bases de datos muy grandes podríamos terminar trayendo centenas de miles de datos que pueden ocupar gigas de memoria, y por regla general SQL es más eficiente que rails para contar datos y de esa forma traer a memoria sólo lo que necesitamos.

length sin includes

Para ejemplificar el caso anterior, este screenshot muestra una vista ocupando length, sin embargo en el controller no se especifica el includes, y como resultado generamos un query para traer los tweets del usuario a memoria para contarlos dentro de ruby, se imaginan lo ineficiente que sería esto en un Twitter real, con 5000 tweets por usuario?

Si las consultas están paginadas, o los resultados son pocos entonces no hay problema alguno y podemos ocupar length, pero por regla es mejor ocupar size, la eficiencia que se logra ocupando length en lugar de size es bastante baja, pero siempre corresponde medir.

¿Entonces que sucede si ocupamos include sin size?

size sin includes

Obtenemos lo mismo que ocupando count, puesto que size determina de forma inteligente que hacer.

Resumen:

Count Length Size
Sin includes Gatilla siempre una consulta SQL count, buena opción de ser necesario Trae todos los tweets a memoria, mala opción Utiliza Count
Con includes Gatilla siempre una consulta SQL count, mala opción, ya los tenemos en memoria Ocupa los tweets que ya tenemos en memoria para contar Utiliza Length

Otra opción:

Utilizar counter cache, para contar los tweets, es una buena solución en general con excepción de que la tasa de ingresos de elementos de la base de datos sea muy alta, en especial si es mucho más alta que la de consulta. Además este método no permite resolver otro tipo de operaciones especiales, claro resuelve la cuenta pero en algunos casos nosotros queremos el promedio o la suma.

En caso de ocupar Counter Cache deberíamos ocupar el método size ya que este automáticamente ocupa la cuenta si existe.

la documentación oficial explica como implementar esta solución http://guides.rubyonrails.org/association_basics.html

Si te gustó el artículo, puedes ver la segunda parte, que utilizaremos la aprendido para crear gráficos con ruby on rails.

Share Button

Director de DesafíoLatam. Ingeniero Civil Informático de la Universidad Federico Santa María. Emprendedor lean, dedicado al desarrollo de una mejor web con ruby on rails. Fanático de los números y las métricas, la música y la fotografía.