管理画面の CSV ダウンロードは、データが増えるにつれて遅くなり、やがてタイムアウトする代表的な機能です。非同期処理に移す前に、まずは同期のまま改善できる余地がないかを確認します。
遅くなる主な原因
- 関連テーブルを 1 行ずつ読む N+1 クエリ
- 全件を一度にメモリへ読み込み、ActiveRecord オブジェクトを大量に生成している
- CSV 全体を文字列として組み立ててから返している
- 絞り込み条件に使っている列にインデックスがない
実装例
def export
headers["Content-Type"] = "text/csv; charset=utf-8"
headers["Content-Disposition"] = %(attachment; filename="orders.csv")
headers["X-Accel-Buffering"] = "no"
self.response_body = Enumerator.new do |out|
out << "\uFEFF" # excel で文字化けしないよう BOM を付ける
out << CSV.generate_line(%w[注文番号 顧客名 金額 注文日])
Order.where(created_at: range).in_batches(of: 2_000) do |batch|
batch.joins(:customer)
.pluck(:number, "customers.name", :total, :created_at)
.each { |row| out << CSV.generate_line(row) }
end
end
endin_batches で一定件数ずつ読み込み、pluck で必要な列だけを配列として取得することで、オブジェクト生成のコストを大きく減らせます。Enumerator をレスポンスにすると生成した行から順に送信されるため、最初のバイトが届くまでの時間も短くなります。
効果を確かめる
- ログの SQL 実行時間とビューの描画時間を分けて比較する
- EXPLAIN で絞り込み条件にインデックスが使われているか確認する
- それでも数十秒を超える件数なら、非同期生成とメール通知に切り替える