Aprendizaje
Manejando grandes cantidades de datos en Ruby on Rails

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
 endLenguaje 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)
endLenguaje 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 
endLenguaje 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)
endLenguaje 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)
endLenguaje 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í.

Etiquetas :

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *