2013-03-03 51 views
47

我試圖運行的使用ActiveRecord的find_each方法大約50,000記錄的查詢相結合,但它似乎被忽略了我的其他參數,像這樣:ActiveRecord的find_each有限制,爲了

Thing.active.order("created_at DESC").limit(50000).find_each {|t| puts t.id } 

而不是在停止50000我想和created_at排序,下面是越過整個數據集執行結果查詢:

Thing Load (198.8ms) SELECT "things".* FROM "things" WHERE "things"."active" = 't' AND ("things"."id" > 373343) ORDER BY "things"."id" ASC LIMIT 1000 

有沒有辦法讓類似的行爲find_each但總的最大限制和尊重我的排序標準?

+0

任何細節你有沒有接受任何答案的理由? – 2013-11-19 21:26:58

+0

對不起,我忘了: - \ – Avishai 2013-12-12 20:59:19

+1

在像find_each這樣的批處理操作中,find_in_batches的作用域順序和限制被忽略,它被強制爲批處理順序和批處理大小 – 2014-11-10 15:21:13

回答

49

The documentation說find_each和find_in_batches不保留排序順序和限制,因爲:

  • 對PK上的ASC進行排序用於使批量排序工作成功。
  • 限制用於控制批量大小。

你可以像@rorra那樣編寫你自己的這個函數的版本。但是在變異對象時可能會遇到麻煩。例如,如果按照created_at排序並保存該對象,則可能會在下一批中再次出現該對象。同樣,您可能會跳過對象,因爲執行查詢以獲取下一批時,結果順序已更改。只能將該解決方案與只讀對象一起使用。

現在我主要擔心的是我不想一次加載30000多個對象到內存中。我關心的不是查詢本身的執行時間。因此,我使用了一個解決方案來執行原始查詢,但只緩存ID。然後它將ID數組劃分爲塊並查詢/創建每個塊的對象。這樣,您可以安全地變更對象,因爲排序順序保存在內存中。

這裏是相似,我做了一個小例子:

batch_size = 512 
ids = Thing.order('created_at DESC').pluck(:id) # Replace .order(:created_at) with your own scope 
ids.each_slice(batch_size) do |chunk| 
    Thing.find(chunk, :order => "field(id, #{chunk.join(',')})").each do |thing| 
     # Do things with thing 
    end 
end 

權衡這種解決方案是:

  • 執行完整的查詢來獲取ID的
  • 的所有ID的數組都保存在內存中
  • 使用MySQL特定的FIELD()函數

希望這有助於!

20

find_each使用find_in_batches在引擎蓋下。

無法選擇記錄的順序,如find_in_batches中所述,會自動設置爲在主鍵(「id ASC」)上升序以使批次排序工作。

然而,標準應用,你可以做的是:

Thing.active.find_each(batch_size: 50000) { |t| puts t.id } 

關於限制,它沒有實現:https://github.com/rails/rails/pull/5696


回答你的第二個問題,你可以自己創建邏輯:

total_records = 50000 
batch = 1000 
(0..(total_records - batch)).step(batch) do |i| 
    puts Thing.active.order("created_at DESC").offset(i).limit(batch).to_sql 
end 
+0

有沒有不同的方法來實現這個? – Avishai 2013-03-03 23:47:19

+0

@ jan-hettich,我寫了* find_in_batches *在我的原始答案中不支持* limit *選項,我還指出實現該選項的pull請求,但它從未被接受/合併。 – rorra 2013-06-27 04:31:45

+1

如果您在處理批次時突變對象,此解決方案將使您陷入困境。如果突變對數據庫中的排序順序有影響,您可以跳過一些或者加倍。 – 2013-11-06 17:31:00

2

我正在尋找相同的行爲,並想出了這個解決方案。這不是由created_at命令,但我想我會反正。這種方法的

max_records_to_retrieve = 50000 
last_index = Thing.count 
start_index = [(last_index - max_records_to_retrieve), 0].max 
Thing.active.find_each(:start => start_index) do |u| 
    # do stuff 
end 

缺點: - 你需要2個查詢(第一個應該是快) - 這保證了50K記錄的最大值,但如果IDS被跳過,你會得到較少。

+0

由於我在尋找skip + find_each的時候發現了這個,所以在這裏值得一提的是::start選項可以用作skip()的等價物,否則您可能會使用它。 – Yourpalal 2016-03-09 16:36:31

11

檢索ids第一和處理in_groups_of

ordered_photo_ids = Photo.order(likes_count: :desc).pluck(:id) 

ordered_photo_ids.in_groups_of(1000).each do |photo_ids| 
    photos = Photo.order(likes_count: :desc).where(id: photo_ids) 

    # ... 
end 

也將ORDER BY查詢添加到內心的召喚是非常重要的。

+1

與公認的答案不同,這在PostgreSQL中起作用。此外,保持答案的簡潔明瞭。 – kdt 2016-01-06 03:06:16

+0

這將需要在一個查詢中採集表中的所有ID,並且我不知道對於較大的表(這是您將使用find_in_batches的地方)建議這樣做。 – Ibrahim 2017-12-13 22:23:16

+0

雖然我猜這樣的事情,你可能不得不求助於獲取所有的ID,如果你需要按任意列排序。 – Ibrahim 2017-12-13 22:29:34

2

一種選擇是把你的特定型號量身定製的實現到模型本身(說到這,id通常是訂購的記錄是更好的選擇,created_at可能有重複):

class Thing < ActiveRecord::Base 
    def self.find_each_desc limit 
    batch_size = 1000 
    i = 1 
    records = self.order(created_at: :desc).limit(batch_size) 
    while records.any? 
     records.each do |task| 
     yield task, i 
     i += 1 
     return if i > limit 
     end 
     records = self.order(created_at: :desc).where('id < ?', records.last.id).limit(batch_size) 
    end 
    end 
end 

要不然你可以概括的事情了一下,使其成爲所有車型的工作:

lib/active_record_extensions.rb

ActiveRecord::Batches.module_eval do 
    def find_each_desc limit 
    batch_size = 1000 
    i = 1 
    records = self.order(id: :desc).limit(batch_size) 
    while records.any? 
     records.each do |task| 
     yield task, i 
     i += 1 
     return if i > limit 
     end 
     records = self.order(id: :desc).where('id < ?', records.last.id).limit(batch_size) 
    end 
    end 
end 

ActiveRecord::Querying.module_eval do 
    delegate :find_each_desc, :to => :all 
end 

config/initializers/extensions.rb

require "active_record_extensions" 

P.S.我按照this answer將代碼放入文件中。

3

您可以通過標準的寶石迭代器向後遍歷:

Thing.last.id.step(0,-1000) do |i| 
    Thing.where(id: (i-1000+1)..i).order('id DESC').each do |thing| 
    #... 
    end 
end 

注:+1是因爲BETWEEN,這將是在查詢包括邊界,但我們需要包括唯一的一個。

當然,用這種方法可以批量取得少於1000條記錄,因爲它們中的一些已經被刪除了,但在我的情況下這是可以的。

2

您可以試試ar-as-batches寶石。

從他們documentation你可以做這樣的事情

Users.where(country_id: 44).order(:joined_at).offset(200).as_batches do |user| 
    user.party_all_night! 
end 
+0

看起來不像它可以從ruby寶石中獲得,但要求它關閉github很棒 – 2016-11-11 09:37:08

0

做在一個查詢和避免迭代:

User.offset(2).order('name DESC').last(3)

意志產品這樣

SELECT "users".* FROM "users" ORDER BY name ASC LIMIT $1 OFFSET $2 [["LIMIT", 3], ["OFFSET", 2]

查詢