Tải CSV ở màn hình quản trị là chức năng điển hình chậm dần khi dữ liệu tăng và cuối cùng bị timeout. Trước khi chuyển sang xử lý bất đồng bộ, nên kiểm tra xem còn cải thiện được khi vẫn xử lý đồng bộ hay không.
Nguyên nhân thường gặp
- Truy vấn N+1 khi đọc bảng liên quan cho từng dòng
- Đọc toàn bộ dữ liệu vào bộ nhớ, tạo ra quá nhiều đối tượng ActiveRecord
- Ghép toàn bộ CSV thành chuỗi rồi mới trả về
- Cột dùng để lọc chưa có index
Ví dụ triển khai
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" # thêm BOM để excel không lỗi font
out << CSV.generate_line(%w[order_no customer total ordered_at])
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
endĐọc theo từng lô bằng in_batches và chỉ lấy các cột cần thiết dưới dạng mảng bằng pluck giúp giảm mạnh chi phí tạo đối tượng. Dùng Enumerator làm response thì dòng nào tạo xong sẽ được gửi ngay, rút ngắn thời gian đến byte đầu tiên.
Kiểm chứng hiệu quả
- So sánh riêng thời gian chạy SQL và thời gian render trong log
- Dùng EXPLAIN để kiểm tra điều kiện lọc có dùng index không
- Nếu số bản ghi vẫn khiến thời gian vượt vài chục giây, chuyển sang tạo file bất đồng bộ và gửi email thông báo