在 Rails 中使用 SQL statement 進行批次塞值(Bulk Insert)到 table

寫在開始之前

最近碰到專案需要重新設計 database 的架構,當中會遇到需要把原本 table 中的 data migrate 到另一個新的 table 的情況。如果是幾十、幾百筆的話當然是小 case,但如果是上萬筆甚至是幾十萬筆的話就比較刺激惹~

舉個例子吧

舉個例子好了,假設現在有 Book & Info 兩個 model 以及 table,他們的 association 是

book has_many infos
info belongs_to book

也就是一本書裡面會有很多基本資料,所以 info 裡面可能就會有 published_date, author 等等的 column。

原本的方法

而現在在更新 Book 的過程中會需要去把 Info 裡面的資料全部放到新的 Meta table 裡面去,原本採用的方法是:

# 更新其中一本 book 時
Info.where(book_id: book.id).find_each(batch_size: 200) do |info|
  Meta.create(published_date: info.published_date, author: info.author)
end

這裡解釋一下幾個東西:

如果方法有需要重複用的話可能就需要考慮 Meta 裡面是否已經有這一筆資料,如 first_or_intitialize 之類的方法。

以這樣的方法如果我們有超多本書,而且每本書有超多筆 info,那麼雙重迴圈下來的時間會非常可觀。以我這一次的經驗而言,在本機跑這樣的指令需要一分鐘,然後上到 staging 跑需要 10 分鐘!!!!!!我是覺得這樣真的跑 hen 久 QQ

SQL Bulk 語法?

因此後來就想說可以使用 SQL 的 bulk insert 來做這件事,於是就開始 google….,最後得到的 SQL 語法是

INSERT INTO "target_table" (col1, col2)
  VALUES (val1_1, val1_2), (val2_1, val2_2), (val3_1, val3_2), ...

這個語法看起來應該不難懂,target_table 就是我們要塞資料進去的 table,也就是 metas;col1, col2 就是要賦值的欄位;而 values 就是每一筆資料的值。

怎麼處理資料?

所以這裡的想法是我們先把要用的值全部抓出來,然後整理成 VALUES 後面的格式,再配合整個 SQL 語句我們就可以大量的新增資料惹 😊

但是原本的 find_each 這個方法傳到 do block 裡面的還是單一筆資料,我們需要的是很多筆資料,因此這邊我們換成另一個語法,如下

Info.where(book_id: book.id).find_in_batches(batch_size: 200) do |info_group|
  ...
end

上面的寫法有看到傳到 do block 裡面的是 info_group 嗎?沒錯!他就是一個 200 筆資料的 array。接下來我們就可以對這個 array 進行操作囉!

Info.where(book_id: book.id).find_in_batches(batch_size: 200) do |info_group|
  infos = info_group.map { |info| "(#{info.published_date}, '#{info.author}')" }.join(",")
end

這一段做了什麼呢?我們將原本的 info_group array 裡面的每一筆資料用 map 抓出來,然後整理成 (val1, val2) 的形式,最後再用 join 將整個處理過的資料合併成一條 string,所以資料會如下:

"(Sun, 22 Apr 2018, 'alan yeh'),(Sun, 22 Apr 2017, 'helen chuang')"

最後的最後,總要來個大集合

到這裡我們就完成 VALUES 的部分了,接下來讓我們繼續完成 SQL 語句的部分

Info.where(book_id: book.id).find_in_batches(batch_size: 200) do |info_group|
  infos = info_group.map { |info| "(#{info.published_date}, '#{info.author}')" }.join(",")

  sql = <<-SQL
    INSERT INTO metas (published_date, author)
    VALUES #{infos};
  SQL
  ActiveRecord::Base.connection.execute(sql)
end

好加在 Rails 提供了我們很方便的方法寫原生的 SQL 語句,然後最後執行就完成啦!是不是很簡單呢~~~~

以我這次的經驗,在本機上跑從一分鐘降成不到 10 秒呢 😊 整個屌打原本的寫法~

這些很重要的雷,一定要注意一下!

要注意的就是:

感謝大家收看囉 😊

Comments