RailsでDBからCSVエクスポートする時に高速かつ低負荷な処理方法とは?

こんにちは。横部です。
これは TECHSCORE Advent Calendar 2017 の4日目の記事です。

今回は「RailsでDBからCSVエクスポートする時に高速かつ低負荷な処理方法」についてお話しさせていただきます。

使用したバージョンは以下の通りです。

Ruby : 2.2.4
Rails : 5.1.4

CSV.generate と each でCSV生成

RailsでCSVエクスポート(ダウンロード)機能を実装する際は、以下のような処理を書くことが多いかと思います。

この処理方法は全件をメモリに展開して処理を行うため、数百件単位の小規模なものであれば問題ありませんが、DBのデータ件数が膨大になるとメモリ不足でエラーになる可能性が潜んでいるコードです。
これを防ぐためには、find_each を使います。

CSV.generate と find_each でCSV生成

find_each を使うことで分割してレコードを取得することが可能になるので、メモリ不足でエラーになる問題が解消されます。

http://railsdoc.com/references/find_each

ただし実行速度の観点からみると、find_each は each よりも多少の遅れを生じさせるようです。
実際に以下のような処理を作成して実行時間の集計を行いました。

すると以下のような結果になりました。
各結果は10回実行した場合の平均値です。

このように find_each は each より若干の遅れが見受けられます。
また、each と find_each は実行の度にActiveRecordオブジェクトを作成しているため、DBのカラムや件数が増えればより負荷が高まり、実行時間は長くなっていきます。
そのため、CSV.generate を使ったエクスポートを行う際は、pluck の利用を推奨します。

CSV.generate と pluck でCSV生成

pluck は実行の度にActiveRecordオブジェクトを生成しないので、より高速に処理を行うことができます。
ただし、pluck 自体は each と同じように全データをメモリに展開して処理してしまうので、limit と offset を活用して負荷がかからないように工夫してます。

http://railsdoc.com/references/pluck

実際に計測すると以下の結果になりました。

each や find_each に比べて倍近くの高速化に成功しており、比較することでActiveRecordオブジェクト生成に大きな負荷がかかっていることがわかります。
ここまでで、CSV.generate を使ったエクスポートの場合は、pluck を使うことで高速化できることを示しました。
これよりもさらに低負荷かつ高速化できる方法が存在します。

SQLでCSV生成

それはSQLで直接CSVを生成することです。

SQLで直接生成した場合は、pluck を使った場合の約10分の1以下の時間で完了しており、ダントツで早い処理であることが示されました。

まとめ

想定される件数が少なく、速度よりもメンテナンス性を重視するのであれば、CSV.generate でCSVを生成する方法が望ましいですが、
件数が数万件単位を超え、メンテナンス性よりも速度を重視する場合は、SQLでCSVを直接生成することが良いと思われます。

Comments are closed, but you can leave a trackback: Trackback URL.