arrow_back Volver
Inicio keyboard_arrow_right Artículos keyboard_arrow_right Artículo

Acelera tus conteos con counter caches en Ruby on Rails

Uriel Hernández

CTO de Código Facilito

av_timer 4 Min. de lectura

remove_red_eye 5915 visitas

calendar_today 09 Mayo 2019

Al trabajar en una plataforma con información en una base de datos, una de las consultas más populares es la que nos permite contar registros, el viejo COUNT:

SELECT COUNT(*) as users_count from users;

Además, cuando trabajas con asociaciones y relaciones entre tus tablas, puede también ser conveniente contar los registros a los que un elemento está asociado. Por ejemplo, supongamos que tenemos una tabla users y una tabla articles y queremos saber cuántos artículos ha escrito un usuario:

SELECT COUNT(*) as articles_count from articles WHERE articles.user_id = 1;

Esta clase de consultas son extremadamente populares y a primera vista pueden parecer inofensivas, una consulta como la anterior en una base de datos real como la de CódigoFacilito tarda 3ms en completarse, por lo que podemos sentirnos confiados de realizar una consulta así, sin embargo ¿qué pasa cuando intentamos contar de una tabla con cientos de miles o millones de registros?

Contando muchos registros

Una consulta de conteo en una de las tablas más grandes de CódigoFacilito +2 millones de registros toma 949ms, es decir, cerca de 1 segundo por una consulta de conteo como las anteriores. Una consulta de 1 segundo es inaceptable, considerando que muchos estudios recomiendan un tiempo de carga promedio de entre 2 y 3 segundos; ocupar la mitad de este tiempo en una sola consulta no es definitivamente una opción.

Estas problemáticas pueden exponenciarse cuando estamos contando los registros asociados a un elemento, considera la tabla que acabo de mencionar, donde un usuario como el mío posee 2 mil de esos 2 millones de registros, por lo que un ejercicio para mostrar el conteo asociado a cada usuario, puede tardar muchísimo más de 1 segundo, ya que por cada usuario tienes que ir a la tabla mencionada y contar los miles de registros asociados a dicho usuario.

Contar, entonces, se vuelve una operación costosa que, además, continúa empeorando conforme los datos que almacenamos continúan creciendo.

Cómo acelerar nuestros conteos

Una de las maneras más prácticas de acelerar los conteos es usar counter caches, el término en inglés que traducido significa cachear los contadores, es decir guardar el conteo en lugar de calcularlo.

Lo que este proceso requiere es que en algún campo de la tabla, se vaya guardando el conteo de elementos asociados, por ejemplo, considerando de nuevo una tabla users que está asociada con otra articles, deberíamos guardar una columna en users que lleve el conteo de cuantos articles están asociados a cada usuario.

Este es un diagrama simplificado de la relación sobre la que trabajaríamos:

Diagrama con dos tablas asociadas 1 a muchos

Implementar counter caches, implicaría primero agregar una nueva columna a la tabla users, la llamaré articles_count

Diagrama con dos tablas asociadas 1 a muchos y un nuevo campo articles_count

Esta columna deberá guardar cuántos artículos le pertenecen a este usario, sumará 1 cada que se cree un nuevo artículo asociado a este usario y restará uno cuando se elimine el registro asociado o se modifique el usuario al que le pertenece.

Esto significa que ahora si quiero conocer cuántos artículos le pertenecen al usuario, en lugar de hacer el cálculo, solo necesito leer dicho campo:

SELECT COUNT(*) as articles_count from articles WHERE articles.user_id = 1; /* ANTES */
SELECT articles_count from users WHERE users.id = 1; /* DESPUÉS */

Implementación en Ruby on Rails

Escribí este artículo desde la perspectiva de Ruby on Rails, aunque la solución puede aplicarse a cualquier framework, con la diferencia de que ésta estrategia viene incluída en Rails y no en otros frameworks donde tendrás que implementarla manualmente o buscar una librería que lo haga.

Considerando nuestro ejemplo anterior, deberíamos tener dos modelos como los siguientes:

class Article < ActiveRecord::Base
  belongs_to :user
end
class User < ActiveRecord::Base
  has_many :articles
end

Mantener actualizado el campo articles_count con el conteo de artículos que le pertenecen a un usuario es tan sencillo como agregar la opción counter_cache en la tabla con el belongs_to. Hay que dejar esto bien claro porque aunque el campo articles_count debería agregarse al modelo con el has_many, en este caso la tabla users, el counter_cache se configura en el otro modelo, en este caso el de Article.

class Article < ActiveRecord::Base
  belongs_to :user, counter_cache: true
end

Ahora, para saber cuátnos artículos le pertenecen al primer usuario podríamos hacer lo siguiente:

User.first.articles_count

Alternativamente, puedes usar el método size en la asociación para leer el campo con el conteo:

User.first.articles.size

Muy importante, mandar a llamar count siempre realiza la calculación del conteo e ignora el counter cache:

User.first.articles.count #Ignora el campo y realiza un COUNT

Resultados y conclusiones

Hay que recalcar, que este enfoque no viene sin sus desventajas, considera que ahora cada que guardes, crees, actualices o elimines un artículo, se haría otra consulta adicional para mantener el contador actualizado, este intercambio suele valer la pena considerando el beneficio de rendimiento en lectura.

La diferencia en rendimiento entre usar un counter cache y hacer el cálculo del conteo son más significativos cuando las tablas asociadas tienen cientos de miles o millones de registros, por lo que no deberías simplemente usarlo en cualquier asociación que tengas, aprovéchalo únicamente cuando es requerido, en este caso cuando la tabla ha crecido mucho o cuando tienes que hacer el cálculo del conteo muchas veces.

En CódigoFacilito, por ejemplo, guardamos un counter cache para saber cuántos alumnos están registrados a un curso, esto significa que, por ejemplo, si intentamos hacer el cálculo de conteo de un curso popular como el de JAVA gratuito, usar el counter cache nos ahorra una consulta de cálculo de 10ms. Por supuesto los beneficios pueden variar dependiendo del tamaño, tus indexes, entre otros factores en los que no quiero ahondar.

Si gustas, puedes enriquecer este artículo dejando en los comentarios tu experiencia implementando esto en tu plataforma.