【Rails】サイズの大きいCSVファイルを扱う時の注意ポイント

Ruby

CSVを分割して読み込むことでメモリの消費を抑える

【Rails】in_batches、find_in_batches、find_eachの違いと使いどころで紹介したように、eachはデータをすべてメモリにのせてからループ処理を実行します。

そのためCSV.readeachを利用してCSVの読み込みをした場合、CSVのサイズが大きいとメモリを圧迫させる可能性があります。

### データが多いとメモリを大量に消費するダメな実装
CSV.read('example.csv').each do |row|
  # 処理
endn

サイズの大きいCSVを扱う場合は、1行ずつCSVを処理するCSV.foreachを利用することでメモリ消費が抑えられます。

### 1行ずつ読み込んで処理するのでメモリ消費を抑えられる
CSV.foreach('example.csv') do |row|
  # 処理
end

CSV.foreacheach_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
insert_allは直接SQLを実行するメソッドのためバリデーションやコールバックはスキップされます。
Active Recordのバリデーションを利用したい場合はactiverecord-importを利用するとよいです。

ただし、時間がかかったとしても1件ずつデータの整合性を確認しながら処理をしたいケースもあるので、要件に応じて適した方法を選択してください。

まとめ

サイズの大きいCSVを扱うときのチェックポイント
  • メモリの消費を抑えるためにCSVファイルは分割して読み込む
  • CSV.readではなくCSV.foreachやeach_sliceを利用してCSVを読み込む
  • 処理時間を短縮させるためにレコード作成はバルクインサートを活用する
  • データ作成はsaveではなくinsert_allやactiverecord-importを活用する

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考