2014-04-23 2 views
17

수천 개의 레코드를 대량 업데이트해야하며 일괄 적으로 업데이트를 처리하고 싶습니다. 첫째, 시도 : 나는 SQL을 생성하는 것입니다 기대했다Rails 3/4에서 업데이트를 어떻게 일괄 적으로 실행할 수 있습니까?

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

... 같은 : update_all는 액티브 관계를 필요로하면서 find_in_batches는, 배열을 반환하기 때문에 작동하지 않습니다

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

.

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

작동하지만, 분명히 업데이트로 다음에 선택이 아닌 내 '어디에'조건에 따라 하나의 업데이 트를 실행합니다

이 내가 다음에 시도하는 것이다. 이 방법을 선택하여 업데이트가 별도의 쿼리 일 필요가 없도록 정리할 수 있습니까?

+0

하지만 일괄 적으로 업데이트해야합니까? where 절이 몇 행을 산출합니까? –

+0

where 절은 수십만 개의 레코드를 검색하므로 find_in_batches를 사용하여 한 번에 1000 개씩 일괄 적으로 업데이트를 처리합니다. – MothOnMars

+0

마리안과 같은 질문이지만, 나는 당신의 이성을 얻지 못합니다. Foo.where(). update_all을 수행하면 Rails에 레코드가로드되지 않고 db update 쿼리 만 수행됩니다. –

답변

12

나는이 작업을 수행 할 수있는 쉬운 방법이 아니라는 것을, 너무 놀랐어요 ... 그러나 나는이 방법을 마련 않았다 : 기본적으로

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 

이됩니다

  1. 0Foo.count 사이의 오프셋 배열을 매번 batch_size 스테핑으로 만듭니다. 예를 들어, Foo.count == 10500이면 다음을 얻을 수 있습니다. [0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000]
  2. 이러한 숫자를 반복하고 SQL 쿼리에서 OFFSET으로 사용하고 id으로 주문하고 batch_size으로 제한하십시오.
  3. "색인"이 offset보다 큰 최대 batch_size 개의 레코드를 업데이트하십시오.

이것은 기본적으로 생성 된 SQL에서 원하는 바를 수동으로 수행하는 방법입니다. 너무 나쁘다는 것은 표준 라이브러리 방법으로 이미이 방법으로 수행 할 수 없다는 것입니다. 물론 자신 만의 라이브러리를 만들 수는 있습니다.

+0

나를 위해,이 정확히 지정한대로 작동하지 않았다 (첫 번째 실행에서 업데이트 된 모든 레코드를 얻지 못 했으므로) 완료 될 때까지 처리 한 while 문을 래핑했습니다 :'query = -> {Foo.where (conditions) .count}; while (count = query.call)> 0; #run above; end' –

5

pdobb의 대답은 올바른 궤도에 있지만 때문에 UPDATE와 OFFSET 구문 분석하지 액티브의이 문제의 레일 3.2.21에서 나를 위해 작동하지 않았다 호출

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

나는 코드를 수정 내가 일괄 update_all 호출하는 작은 방법을 서면으로 작성했습니다

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 
+1

시퀀스 ID를 사용하는 경우에만 작동합니다. – shem

4

이것은 2 년 늦었지 만 여기에 대한 답변은 a) 큰 데이터 세트의 경우 매우 느리고 b) 내장 레일 기능 (http://api.rubyonrails.org/classes/ActiveRecord/Batches.html)을 무시하십시오.

오프셋 값이 증가함에 따라 DB 서버에 따라 블록에 도달 할 때까지 시퀀스 스캔을 수행 한 다음 처리 할 데이터를 가져옵니다.오프셋이 수백만 개가되면 이 매우이됩니다.

사용 "find_each"반복자 방법 :

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

이 각각의 저장과 모델 콜백을 실행의 추가 혜택을했다. 당신이 콜백을 상관하지 않는 경우, 시도 :

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 
+0

find_each는 내부적으로 오프셋을 사용하지 않습니까? – Naremy

+1

@Naremy no. find_each는 질의를 실행하고'id> X order by id asc limit 1000'을 추가합니다. 일괄 처리를 반복하면서 ID를 최신 ID로 업데이트 한 다음 새 호출을 발행합니다. 이렇게하면 오프셋을 사용하지 않습니다 (오프셋 작업이 완료되기 전에 모든 데이터를로드해야하므로 점진적으로 느려집니다) – Faisal

26

레일 5에서이 문제를 해결하기 위해 새로운 편리한 방법 ActiveRecord::Relation#in_batches있다 : 자세한 내용은

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

확인 documentation합니다.

0

아직 테스트 할 기회가 없지만 ARel 및 하위 쿼리를 사용할 수 있습니다.

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 
관련 문제