CSVを分割して読み込むことでメモリの消費を抑える
【Rails】in_batches、find_in_batches、find_eachの違いと使いどころで紹介したように、each
はデータをすべてメモリにのせてからループ処理を実行します。
そのためCSV.read
とeach
を利用してCSVの読み込みをした場合、CSVのサイズが大きいとメモリを圧迫させる可能性があります。
### データが多いとメモリを大量に消費するダメな実装
CSV.read('example.csv').each do |row|
# 処理
endn
サイズの大きいCSVを扱う場合は、1行ずつCSVを処理するCSV.foreachを利用することでメモリ消費が抑えられます。
### 1行ずつ読み込んで処理するのでメモリ消費を抑えられる
CSV.foreach('example.csv') do |row|
# 処理
end
CSV.foreach
とeach_slice
を組み合わせると複数行単位でCSVを処理できます。
### 10行単位でCSVを読み込んで処理をする例
CSV.foreach('example.csv').each_slice(10) do |rows|
rows.each do |row|
# 処理
end
end
CSVのデータをデータベースに保存する場合はバルクインサートを活用する
CSVのデータをデータベースに保存する場合、1件ずつ処理をすると頻繁にデータベースへアクセスすることになるため時間がかかります。
### 1件ずつsaveが実行されるので時間がかかる
CSV.foreach('example.csv') do |row|
title = row[0]
price = row[1]
book = Book.new(title: title, price: price)
book.save
end
一度に複数のデータをインサートするバルクインサートを活用することでデータベースへのアクセスが減り、処理時間を短縮できます。
バルクインサートはactiverecord-import gemや、Rails 6以上であればinsert_allメソッドを利用することで実装できます。
insert_allの詳細解説はRails 6のinsert_allで大量のダミーデータを短時間で作成するで紹介しています。
insert_allを利用したバルクインサートの例は以下の通りです。
CSV.foreach('example.csv').each_slice(10) do |rows|
books = []
rows.each do |row|
title = row[0]
price = row[1]
time = Time.current
books <<
{
title: title,
price: price,
created_at: time,
updated_at: time
}
end
Book.insert_all books
end
Active Recordのバリデーションを利用したい場合はactiverecord-importを利用するとよいです。
ただし、時間がかかったとしても1件ずつデータの整合性を確認しながら処理をしたいケースもあるので、要件に応じて適した方法を選択してください。
まとめ
- メモリの消費を抑えるためにCSVファイルは分割して読み込む
- CSV.readではなくCSV.foreachやeach_sliceを利用してCSVを読み込む
- 処理時間を短縮させるためにレコード作成はバルクインサートを活用する
- データ作成はsaveではなくinsert_allやactiverecord-importを活用する
Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!