Necesito masa-actualización de muchos miles de registros, y me gustaría para procesar las actualizaciones por lotes. En primer lugar, he intentado:

Foo.where(bar: 'bar').find_in_batches.update_all(bar: 'baz')

…que tenía la esperanza de que iba a generar SQL tales como:

"UPDATE foo SET bar = 'baz' where bar='bar' AND id > (whatever id is passed in by find_in_batches)"

Que no funciona porque find_in_batches devuelve una matriz, mientras que update_all necesidades de un ActiveRecord relación.

Esto es lo que he intentado siguiente:

Foo.where(bar: 'bar').select('id').find_in_batches do |foos|
  ids = foos.map(&:id)
  Foo.where(id: ids).update_all(bar: 'baz')
end

Que funciona, pero es obvio que se ejecuta un select seguido por la actualización, en lugar de una sola actualización basado en mi ‘donde’ condiciones. Hay alguna forma de limpiar esto, por lo que el select y update no tiene que ser independiente de las consultas?

  • pero y qué tienes que hacer que actualizar en lotes? cuántas filas hace su cláusula where rendimiento?
  • La cláusula where se recuperar cientos de miles de registros, que es la razón por la que estoy usando find_in_batches para procesar las actualizaciones en lotes de 1000 a la vez.
  • Misma pregunta como Mariano, no entiendo tu razonamiento. Si usted Foo.donde().update_all no va a cargar los registros para los Rieles, acaba de realizar una actualización de db consulta.
  • Estoy realizando la actualización por lotes para evitar el bloqueo de mi mesa, mientras que la actualización de cientos de miles de registros.
InformationsquelleAutor MothOnMars | 2014-04-23

6 Comentarios

  1. 53

    En Rails 5, hay un nuevo método práctico ActiveRecord::Relation#in_batches para resolver este problema:

    Foo.in_batches.update_all(bar: 'baz')

    De verificación documentación para más detalles.

  2. 11

    Me sorprende, también, que no hay una manera más fácil de hacer esto… pero me vino con este enfoque:

    batch_size = 1000
    0.step(Foo.count, batch_size).each do |offset|
      Foo.where(bar: 'bar').order(:id)
                           .offset(offset)
                           .limit(batch_size)
                           .update_all(bar: 'baz')
    end

    Básicamente esto:

    1. Crear una matriz de desplazamientos entre 0 y Foo.count pisar por batch_size cada momento. Por ejemplo, si Foo.count == 10500 se obtendría: [0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000]
    2. Bucle a través de estos números y utilizarlos como un DESPLAZAMIENTO en la consulta SQL, asegurándose de orden por id, y la limitación a la batch_size.
    3. Actualización en la mayoría de los batch_size registros cuyo «índice» es mayor que offset.

    Esta es básicamente la manera manual para realizar lo que dijo que estaban esperando en el SQL generado. Lástima que no se puede hacer de esta manera ya por una norma del método de la biblioteca… aunque estoy seguro de que podría crear uno propio.

    • para mí, esto no funciona exactamente como se especifica (no obtener todos los registros actualizados en la primera ejecución) así que me envuelto en una mientras que la declaración que hizo el proceso hasta que esté hecho: query = -> { Foo.where(conditions).count } ; while (count = query.call) > 0 ; #run above ; end
    • Lamentablemente, esto no va a funcionar porque Rails no apoyo offset con update_all, ver aquí: github.com/rails/rails/issues/10849
    • Esto sólo funciona si usted está iterando sobre toda la tabla. Esto no funcionará si usted está tratando de actualizar un centenar de millones de filas de una tabla que tiene la mitad de mil millones de registros.
  3. 6

    Este es de 2 años de retraso, pero las respuestas aquí son) muy lento para grandes conjuntos de datos y b) ignorar el grupo builtin rieles de capacidades (http://api.rubyonrails.org/classes/ActiveRecord/Batches.html).

    Como el valor de desplazamiento aumenta, dependiendo de su servidor de DB, se va a hacer una secuencia de escaneo hasta que llega a su bloque y, a continuación, obtiene los datos para su procesamiento. Como su desplazamiento se mete en la de millones de personas, esto será extremadamente lento.

    el uso de la «find_each» iterador método:

    Foo.where(a: b).find_each do |bar|
       bar.x = y
       bar.save
    end

    Esto tiene el beneficio añadido de la ejecución del modelo de devoluciones de llamada con cada una de guardar. Si usted no se preocupan por las devoluciones de llamada, a continuación, intente:

    Foo.where(a: b).find_in_batches do |array_of_foo|
      ids = array_of_foo.collect &:id
      Foo.where(id: ids).update_all(x: y)
    end
    • El find_each no utilice desplazamiento internaly?
    • no. find_each iba a ejecutar una consulta y añade where id > X order by id asc limit 1000. Como se repite sobre el lote, se mantiene la actualización del dni a la última id y, a continuación, emite una nueva llamada. De esta manera nunca se utiliza offset (que se hace cada vez lento ya que tiene que cargar todos los datos antes de cualquier desplazamiento de una operación realizada)
    • No hay necesidad de ids = array_of_foo.collect &:id. Se puede pasar un array de objetos en una where cláusula así: Foo.where(id: array_of_foo).update_all(x: y)
  4. 3

    pdobb la respuesta está en el camino correcto, pero no funciona para mí en Rails 3.2.21 debido a este problema de ActiveRecord no el análisis de la compensación de ACTUALIZACIÓN de llamadas:

    https://github.com/rails/rails/issues/10849

    He modificado el código en consecuencia y funcionó bien simultáneamente para establecer el valor predeterminado en mi Postgres tabla:

    batch_size = 1000
    0.step(Foo.count, batch_size).each do |offset|
      Foo.where('id > ? AND id <= ?', offset, offset + batch_size).
          order(:id).
          update_all(foo: 'bar')
    end
    • Esto sólo funcionará si usted está utilizando el id de secuencia
  5. 0

    No han tenido la oportunidad de probar este todavía, pero usted puede ser capaz de utilizar ARel y una sub consulta.

    Foo.where(bar: 'bar').select('id').find_in_batches do |foos|
      Foo.where( Foo.arel_table[ :id ].in( foos.to_arel ) ).update_all(bar: 'baz')
    end

Dejar respuesta

Please enter your comment!
Please enter your name here