Update masivo de registros

24 views
Skip to first unread message

Cesar Martinez

unread,
Jun 6, 2017, 12:51:07 PM6/6/17
to Grupo RubySur
Buenas! les cuento mi problematica, tengo un boton cambiar precios en una vista, se carga la cotizacion del dia, y al darle click me ejecuta un metodo en mi controller 

  def cambiar_precios
    @cotizacion = Cotizacion.find(params[:id])
    @productos = Producto.joins(:categoria).where("categorias.nombre = ?", "ALUMINIO")
    @linea_precios = PrecioColor.all
   
    @linea_precios.each do |li_pre|
      @productos.each do |producto|
        if li_pre.linea_id == producto.linea_id && li_pre.color_id == producto.color_id
          producto.precio = li_pre.precio * producto.peso * producto.largo.medida
        end
        producto.full_nombre = "#{producto.nombre}"+" "+"#{producto.descipcion}"+" precio: $#{producto.precio.round(2)}"+" en stock: #{producto.cantidad_stock}"
        producto.save
      end
    end

    flash[:notice] = "Precios nuevos asignados satisfactoriamente con esta cotizacion #{@cotizacion.monto} Gs.!"
    redirect_to cotizaciones_url
end

mi metodo desde consola funciona 100%, pero al ejecutarlo en la vista, en modo produccion, unicorn mata el proceso, y ni si aumento el timeout, me completa el update masivo
Les agradeceria cualquier guia o sugerencia, gracias de antemano

Gestor de BD postgresql, ngnix, unicorn, asi como lo indica el tutorial http://lobotuerto.com/blog/2014/10/10/levantar-aplicacion-rails-para-produccion-en-ubuntu-con-unicorn-y-nginx/

Pd: mi proceso aproximadamente demora 2 min en consola

Mariano Ayesa

unread,
Jun 6, 2017, 12:59:10 PM6/6/17
to rub...@googlegroups.com
Buenas,

Mirá la gema sidekiq, delayed_job o similares para poder delegar esto
a un worker.

Saludos,
> --
> Has recibido este mensaje porque estás suscrito al grupo "rubysur" de Grupos
> de Google.
> Para cancelar la suscripción a este grupo y dejar de recibir sus mensajes,
> envía un correo electrónico a rubysur+u...@googlegroups.com.
> Para acceder a más opciones, visita https://groups.google.com/d/optout.

Joaquín Vicente

unread,
Jun 6, 2017, 1:38:52 PM6/6/17
to rub...@googlegroups.com
Posiblemente la mejor opción es, como dijo Mariano, pasar ese proceso a un worker que corra en background​ y liberar al controller de dicha carga.
Sin embargo estuve mirando el código y noto un par de cuellos de botella que podrían evitarse cambiando la forma en que se ejecutan las queries:

Tenés dos loops anidados (@linea_precios y @productos) y en cada iteración actualizás un producto. Quizás podrías realizar una sola query que haga un update y eso sería mucho más rápido para la BD (aunque el código seguramente no quede tan claro!)

Por lo visto, si no se cumple que:

  li_pre.linea_id == producto.linea_id && li_pre.color_id == producto.color_id
no vas a modificar el precio, y al no modificar el precio, no haría falta cambiar la descripción (full_nombre)

Esto quizás lo podés solucionar haciendo un JOIN entre Productos y PreciosColor y definiendo la relación entre los IDs de las tablas. Con esto te podrías evitar tener un loop anidado

Te tiro un ejemplo:
  UPDATE productos p JOIN precio_colores pc
  ON (pc.linea_id = p.linea_id AND pc.color_id = p.color_id)
  SET p.precio = ... , p.full_nombre = ...;

Otra cosa que me llamó la atención es que estás cambiando los precios según la cotización, pero en ningún lado hacés uso ese valor...

Saludos!


> envía un correo electrónico a rubysur+unsubscribe@googlegroups.com.

> Para acceder a más opciones, visita https://groups.google.com/d/optout.

--
Has recibido este mensaje porque estás suscrito al grupo "rubysur" de Grupos de Google.
Para cancelar la suscripción a este grupo y dejar de recibir sus mensajes, envía un correo electrónico a rubysur+unsubscribe@googlegroups.com.
Para obtener más opciones, visita https://groups.google.com/d/optout.

Matias Toselli

unread,
Jun 6, 2017, 2:14:00 PM6/6/17
to rub...@googlegroups.com
Es posible que each te este llenando la memoria. Lo que eso hace es traerte todos los registros de la DB a memoria y cuando estan todos cargados recien empieza a iterar sobre ellos. Alternativamente podes hacer find_each, que lo que hace es iterar en batches de a 1000, o tambien podes pasarle otro numero, y ejecuta tu bloque sobre cada uno, se usa igual que each pero hace diferentes queries para operar sobre los registros. Es mas economico en terminos de memoria para iterar sobre gran cantidad de registros.

Podes hacer dmesg | grep unicorn para verificar que el sistema te mata el proceso por falta de memoria. O bien podes inspeccionar todo dmesg para ver si hay otra cosa andando. Adicionalmente podes inspeccionar otros logs en /var/log, pero en mi experiencia dmesg siempre me tiro que procesos se sacrifican y por que. Depende un poco de como es server, si tenes swap o no, si es virtualizado o fisico, etc.

Como otros te comentaron podes hacer el proceso en otro lado en background con sidekiq o delayedjob, pero si se te muere el proceso por falta de memoria, vas a tener el mismo problema, nomas que el worker se te va a morir por la misma razon.

Verifica cuantos registros estas manejando en el proceso, nunca es bueno cargar todo en memoria si no se necesita todo al mismo tiempo.

Otra cosa puede ser el join si tenes muchos productos. Verifica que tengas y uses los indices. Segun veo PostgreSQL tambien usa EXPLAIN para analizar queries, eso te deberia mostrar como se planifica la query en la DB y ver que indices usa. Realmente necesitas categorias para updatear los precios? Se podria separar las dos cosas. Imagino que usas categorias para mostrar en la vista, lo cual estaria bien, pero no para actualizar si no las usas. Me llama la atencion que tengas el join con un string tambien, usualmente se usan con claves foraneas lo que termina siendo un has_many/belongs_to o algo similar, no siempre es bueno usar strings como ids, suelen cambiar mas que un id numerico pero depende mucho del contexto y la necesidad.

Ni hablar que iteras los productos por cada linea_precios. Quizas falta una relacion ahi a la cual podes adicionar un indice y poder facilitar traerte registros relacionados sin necesidad de comparar si los id de clave foranea son iguales o no. Con esto podes hacer @linea_precio.productos y cosas similar, aunque con mucho volumen se torna complicado luego.

Adicional: no hace falta usar + para concatenar strings podes hacer "#{variable1} tiene #{variable2} productos" a menos que tengas alguna razon en particular para hacerlo asi. full_nombre daria para ser otro metodo adicional que te retorne ese armado para evitarte tener que guardar un string computado asi en DB. Eso te ahorraria tener que actualizar el nombre de esa manera y te evitas tocar esos datos ya que el string se armaria con los datos del objeto al vuelo, y ojo que no te adiciona queries si los valores estan todos en el objeto producto. Esto es opinion mia mas que nada.

Consejo: solo trae de la DB lo que realmente necesitas mostrar/editar, los update deberian ser lo mas puntuales posibles, si hay registros que no se van a updatear o ver no tiene mucho sentido iterarlos. Por eso es importante tener buenos indices y relaciones para reducir el set que tenes que updatear y targetear lo que si vas a cambiar. Finalmente .all no se deberia usar casi nunca. Podes usar find_each o algun where para limiar, sino luego dependes del volumen de datos y las cosas no te van a andar luego de que tengas cierto volumen, superando inicialmente los timeouts de webserver si son acciones desde una web y luego matando procesos como te esta pasando.

Espero te ayude un poco.

Saludos
--
Has recibido este mensaje porque estás suscrito al grupo "rubysur" de Grupos de Google.
Para cancelar la suscripción a este grupo y dejar de recibir sus mensajes, envía un correo electrónico a rubysur+u...@googlegroups.com.

Emanuel Friedrich

unread,
Jun 6, 2017, 2:26:18 PM6/6/17
to rub...@googlegroups.com
además de lo que dijo vicente, podrías ver hacer un update solo en vez de las

Producto.joins(:categoria).where("categorias.nombre = ?", "ALUMINIO").count * PrecioColor.all.count

veces que hacés producto.save

POdrías almacenar en un dos arrays por un lado los ids y por otro un array de hashes con los valores de full_nombre y precio, correspondiendo el id del producto del elemento i del primer array con el sus valores actualizados en el segundo array elemento i
así:

Producto.update([2,3,4], [{full_nombre: 'asdfasdf', precio: 2212.22}, {full_nombre: 'asdfasdf', precio: 2212.22}, {full_nombre: 'asdfasdf', precio: 2212.22}])

Para cancelar la suscripción a este grupo y dejar de recibir sus mensajes, envía un correo electrónico a rubysur+unsubscribe@googlegroups.com.

Para acceder a más opciones, visita https://groups.google.com/d/optout.

--
Has recibido este mensaje porque estás suscrito al grupo "rubysur" de Grupos de Google.
Para cancelar la suscripción a este grupo y dejar de recibir sus mensajes, envía un correo electrónico a rubysur+unsubscribe@googlegroups.com.

Para acceder a más opciones, visita https://groups.google.com/d/optout.



--
Emanuel Friedrich 

Cel: 3754-442896

Jorge Vargas

unread,
Jun 6, 2017, 2:30:55 PM6/6/17
to rub...@googlegroups.com
Hola!

+ 1 que el each está haciendo algo feo, pero creo que el motivo es diferente (y corrijanme si me equivoco).

El tema es que cuando haces iteraciones dentro de un objeto anidado entras en el mundo de los llamados problemas N + 1 (http://guides.rubyonrails.org/active_record_querying.html#eager-loading-multiple-associations). Basicamente significa que AR hace un select por cada vuelta del each. Para solucionar eso a mi me gusta includes, aunque también existe eager_load.

BTW: esto detecta los n+1 https://github.com/flyerhzm/bullet

Además, podrías hacer un update masivo como sale acá https://cbabhusal.wordpress.com/2015/01/03/updating-multiple-records-at-the-same-time-rails-activerecord/ aunque no lo he probado.

Saludos

Cesar Martinez

unread,
Jun 6, 2017, 2:59:53 PM6/6/17
to Grupo RubySur
Muchas Gracias a todos por sus respuestas y sugerencias, tomare cada una y mejorare mi codigo, emplee la gem 'sucker_punch' y sin problemas me realiza los update, muchas gracias grupo rubysur, les comparto luego la solucion optimizada, esta groso el tema de los jobs, muy poderoso!

Para cancelar la suscripción a este grupo y dejar de recibir sus mensajes, envía un correo electrónico a rubysur+unsubscribe@googlegroups.com.

Para acceder a más opciones, visita https://groups.google.com/d/optout.


--
Has recibido este mensaje porque estás suscrito al grupo "rubysur" de Grupos de Google.
Para cancelar la suscripción a este grupo y dejar de recibir sus mensajes, envía un correo electrónico a rubysur+unsubscribe@googlegroups.com.

Para acceder a más opciones, visita https://groups.google.com/d/optout.

--
Has recibido este mensaje porque estás suscrito al grupo "rubysur" de Grupos de Google.
Para cancelar la suscripción a este grupo y dejar de recibir sus mensajes, envía un correo electrónico a rubysur+unsubscribe@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages