![Manejando grandes cantidades de datos en Ruby on Rails Manejando grandes cantidades de datos en Ruby on Rails](https://gerardomiranda.dev/blog/wp-content/uploads/2021/11/Ruby_On_Rails_Logo-1024x387.png)
Manejando grandes cantidades de datos en Ruby on Rails
El otro día tenía una tarea para la que necesitaba generar 2,000,000 de registros en la base de datos antes de poder trabajar en ella. Quien había creado la tarea tuvo la amabilidad de dejarme un script para generar los datos. No le presté demasiada atención y sólo lo dejé corriendo mientras completaba otras tareas. Mis otras tareas me llevaron todo el día para completar y cuando fui a revisar el progreso del script me di cuenta que no iba ni por la mitad.
Evidentemente el script no era eficiente en lo absoluto, asi que lo detuve y lo revisé. Lo primero que noté es que estaba generando las instancias una por una, que dada la naturaleza de los objetos que estaba creando era inevitable. La verdadera causa de la ineficiencia era cómo estaba guardando los registros en la base de datos, directamente con ActiveRecord.save.
Sé de primera mano que el método para guardar de ActiveRecord es muy poco eficiente para grandes cantidades de datos, pero no tenía idea de cuánto. Así que después de cambiar el script para utilizar la librería activerecord-import, tuve curiosidad de qué tan grande era la diferencia rendimiento con el ActiveRecord.save.
Preparando las pruebas
Para eso primero necesitamos un modelo, lo mantendremos bastante simple porque no es necesario complicarnos demás.
create_table :dummy_models do |t|
t.string :dummy_text1
t.string :dummy_text2
t.string :dummy_text3
t.timestamps
end
Lenguaje del código: Ruby (ruby)
Lo siguiente será el método para generar los registros. Para generar el texto utilizaremos la gema Faker. Para respetar el problema original en todos los casos vamos a generar los registros uno por uno.
def self.generate_record
DummyModel.new(dummy_text1: Faker::Lorem.word, dummy_text2: Faker::Lorem.word, dummy_text3: Faker::Lorem.word)
end
Lenguaje del código: Ruby (ruby)
Ahora vamos con los métodos a comparar, primero utilizando ActiveRecord.save
def self.with_active_record_create(size)
(1..size).each do |i|
record = generate_record
record.save!
end
end
Lenguaje del código: Ruby (ruby)
Seguimos con utilzando activerecord-import
def self.with_active_record_import(size)
records = []
(1..size).each do |i|
records << generate_record
end
DummyModel.import(records)
end
Lenguaje del código: Ruby (ruby)
Yendo un poco mas lejos, utilizaremos un par de opciones que nos permite la librería, primero para saltarnos las validaciones y adicionalmente para pasar las columnas en lugar de un objeto ActiveRecord. Para esto último primero generaremos el ActiveRecord correspondiente y extraeremos sus valores, para ver si la diferencia en rendimiento lo justifica.
def self.with_active_record_import_without_validations(size)
records = []
(1..size).each do |i|
records << generate_record
end
DummyModel.import(records, validate: false)
end
def self.with_active_record_import_columns_without_validations(size)
values = []
columns = ['dummy_text1', 'dummy_text2', 'dummy_text3']
(1..size).each do |i|
record = generate_record
values << [record.dummy_text1, record.dummy_text2, record.dummy_text3]
end
DummyModel.import(columns, values, validate: false)
end
Lenguaje del código: Ruby (ruby)
Comparando los tiempos
Listo, lo único que queda es utilizar benchmarking para ver las diferencias. Comenzaremos pasando solo 1,000 registros.
With ActiveRecord save
2.444752 0.297301 2.742053 ( 5.692380)
With ActiveRecord import
0.260530 0.028553 0.289083 ( 0.325896)
With ActiveRecord import without validations
0.250810 0.024476 0.275286 ( 0.289907)
With ActiveRecord using columns without validations
0.223519 0.028434 0.251953 ( 0.267757)
Lenguaje del código: CSS (css)
Ya con esta cantidad de datos, la diferencia es abismal entre ActiveRecord.save y los métodos de activerecord-import, con mas de 5 segundos de diferencia. Aún así creo que con cantidades de esta magnitud todavía se puede utilizar ActiveRecord.save sin mucha preocupación.
Entre los diferentes métodos de activerecord-import casi no se puede apreciar diferencia. Vamos a intentar subiendo un poco la vara con 10,000 registros.
With ActiveRecord save
19.409276 1.963945 21.373221 ( 47.560994)
With ActiveRecord import
2.865762 0.203885 3.069647 ( 3.313518)
With ActiveRecord import without validations
2.657060 0.195326 2.852386 ( 2.983243)
With ActiveRecord using columns without validations
2.423606 0.217493 2.641099 ( 2.779699)
Lenguaje del código: CSS (css)
Bueno con estos tiempos creo que ActiveRecord.save quedaría descartado para cantidades alrededor de los 10,000 registros. Pero los diferentes métodos de activerecord-import aún se mantienen a poca distancia unos de otros a menos de 1 segundo.
Hagamos trabajar un poco a nuestra PC y generemos 100,000 registros a ver qué sucede.
With ActiveRecord save
207.721426 20.519692 228.241118 (503.432937)
With ActiveRecord import
27.889573 2.115005 30.004578 ( 32.025789)
With ActiveRecord import without validations
27.754645 2.084204 29.838849 ( 31.793397)
With ActiveRecord using columns without validations
24.807959 1.963377 26.771336 ( 28.741473)
Lenguaje del código: CSS (css)
Definitivamente ActiveRecord.save queda descartado si quieres guardar 100,000+ registros, ahora entiendo por qué ese script no terminaba de ejecutar. En cuanto a los demás se puede ver que el método para guardar directamente las columnas sin pasar un objeto ActiveRecord fue mas rápido que sus contrapartes por al menos 2 segundos. Esto no es nada del otro mundo pero hay que tomar en cuenta que primero creamos los objetos ActiveRecord y luego extrajimos los datos. Asi que en caso de que manejes millones de registros y si puedes no crear un objeto ActiveRecord antes, puede valer la pena optar por esta última opción.
Estas comparaciones me muestran que definitivamente cuando me encuentre desarrollando en Ruby on Rails, tengo que tener muy en cuenta la cantidad de datos con la que voy a trabajar. No sea que uno de estos días uno de esos scripts se vaya a producción y la aplicación se muera.
El código está disponible para que puedas verlo aquí.