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:
Implementar counter caches, implicaría primero agregar una nueva columna a la tabla
users
, la llamaré 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.