sql - regla - ¿Cómo puedo ejecutar actualizaciones en lotes en Rails 3/4?
no hay caracteristicas seleccionadas para actualizarlas sql (6)
Aún no he tenido la oportunidad de probar esto, pero es posible que puedas usar ARel y una consulta secundaria.
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
Necesito actualizar en masa muchos miles de registros, y me gustaría procesar las actualizaciones en lotes. Primero, intenté:
Foo.where(bar: ''bar'').find_in_batches.update_all(bar: ''baz'')
... que esperaba que generara SQL como:
"UPDATE foo SET bar = ''baz'' where bar=''bar'' AND id > (whatever id is passed in by find_in_batches)"
Eso no funciona porque find_in_batches devuelve una matriz, mientras que update_all necesita una relación ActiveRecord.
Esto es lo que intenté a continuación:
Foo.where(bar: ''bar'').select(''id'').find_in_batches do |foos|
ids = foos.map(&:id)
Foo.where(id: ids).update_all(bar: ''baz'')
end
Eso funciona, pero obviamente ejecuta una selección seguida por la actualización, en lugar de una única actualización basada en mis condiciones ''donde''. ¿Hay alguna forma de limpiar esto, para que la selección y la actualización no tengan que ser consultas separadas?
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'')
Consulte la documentation para más detalles.
Esto es 2 años tarde, pero las respuestas aquí son a) muy lentas para grandes conjuntos de datos yb) ignoran las capacidades de los rieles integrados ( http://api.rubyonrails.org/classes/ActiveRecord/Batches.html ).
A medida que aumenta el valor de compensación, dependiendo de su servidor de base de datos, realizará una exploración de secuencia hasta que llegue a su bloque, y luego buscará los datos para procesarlos. A medida que su compensación llegue a millones, esto será extremadamente lento.
use el método iterador "find_each":
Foo.where(a: b).find_each do |bar|
bar.x = y
bar.save
end
Esto tiene la ventaja adicional de ejecutar las devoluciones de llamada del modelo con cada guardado. Si no le interesan las devoluciones de llamada, 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
He escrito un pequeño método para invocar update_all en lotes:
https://gist.github.com/VarunNatraaj/420c638d544be59eef85
Espero que sea útil! :)
La respuesta de pdobb está en el camino correcto, pero no me funcionó en Rails 3.2.21 debido a este problema de ActiveRecord que no analiza OFFSET con las llamadas UPDATE:
github.com/rails/rails/issues/10849
Modifiqué el código en consecuencia y funcionó bien para establecer simultáneamente el valor predeterminado en mi tabla de Postgres:
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
También me sorprende que no haya una forma más fácil de hacer esto ... pero se me ocurrió 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 será:
- Cree una matriz de compensaciones entre
0
yFoo.count
paso a paso porbatch_size
cada vez. Por ejemplo, siFoo.count == 10500
obtendrías:[0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000]
- Recorra estos números y utilícelos como DESPLAZAMIENTO en la consulta SQL, asegurándose de ordenar por
id
y limitando elbatch_size
delbatch_size
. - Actualice en la mayoría de los registros
batch_size
cuyo "índice" sea mayor que eloffset
.
Esta es básicamente la forma manual de realizar lo que dijo que esperaba en el SQL generado. Lástima que no se pueda hacer de esta manera con un método de biblioteca estándar ... aunque estoy seguro de que podría crear uno propio.