出力データが大量すぎて時間がかかる
クライアントからログ情報を分析したいとのことでログ情報をDBから出力するような機能を作ったが
実際に実運用が始まってからログを出力した結果、1, 2日くらいのログであれば時間はかかるがcsv出力できた。しかし、全期間になると時間がかかりすぎて使い物にならない。というような事態に陥った。実際に試してみると15分たっても出力されず、「確かにこれは使えないなぁ。。。」という状態だったので、直接SQLを叩いてログ出力をするようにした。
それまでに使っていた方法
RailsでDBのデータをCSV出力しよう|已むに已まれぬ雑記帳|note
このような感じでよくあるcsv出力をCSVライブラリを使用して出力していた。(これかな。。
library csv (Ruby 2.7.0 リファレンスマニュアル)
)
今回、生SQLでクエリを発行してログ出力するのもCSVライブラリを使っているがちょっとだけ使い方が違う。
to_sql
メソッドを使ってActiveRecordで発行されるSQLを生SQLに変換して、それを出力している。
app/services/large_csv_exporter_service.rb
class LargeCsvExporterService
require 'csv'
attr_accessor :table, :column, :host, :username, :password, :database
def initialize(table, column, host, username, password, database)
@table = table
@column = column
@host = host
@username = username
@password = password
@database = database
end
def set_file_name(file_name)
@csv_file_name = file_name
end
def set_query(query)
results = @client.query(query)
results
end
def write_csv(results)
# File.write(@csv_file_name, encoding: Encoding::SJIS) unless File.exist? @csv_file_name
CSV.open("./tmp/csv_export/#{@csv_file_name}", "w:sjis") do |csv|
csv << results.fields
results.each do |row|
csv << row.values
end
end
end
def export_csv_file
filepath = "./tmp/csv_export/#{@csv_file_name}"
stat = File::stat(filepath)
return filepath, stat, @csv_file_name
end
def delete_created_csv_file
if File.exist?("./tmp/csv_export/#{@csv_file_name}")
File.delete("./tmp/csv_export/#{@csv_file_name}")
end
end
def set_client
# cache_rows: falseでメモリを使わないようにしている
@client = Mysql2::Client.new(
host: host,
username: username,
password: password,
database: database,
stream: true,
cache_rows: false,
)
end
end
serviceをcontrollerのなかで使っている。真ん中あたりのcsv_export = LargeCsvExporterService.new("access_logs", "*", ENV["DB_D_HOST"], ENV["DB_D_USERNAME"], "", ENV["DB_D_NAME"])
らへんから。ENV["DB_D_HOST"]
とかで、.env(dotenv)のDB名とかユーザー名とかを参照している。
綺麗なコードは書けないので、大目に見てください。
app/controllers/admins/access_logs_controller.rb
def export_csv(params_q)
@q = AccessLog.order(id: :asc).ransack(params_q)
user_type_param = params[:q][:user_type].empty? ? ["user", "admin", "supplier"] : params[:q][:user_type].split(':')[1]
methoda_type_param = params[:q][:method_type] == "ALL_TYPE" ? AccessLog.distinct.pluck(:method_type).compact : params[:q][:method_type]
access_dates = AccessLog.order(access_date: :asc).to_a
from_time = params[:q][:created_at_gteq].empty? ? access_dates.first.access_date : params[:q][:created_at_gteq].to_time
to_time = params[:q][:created_at_lt].empty? ? access_dates.last.access_date : params[:q][:created_at_lt].to_time
sql_query = AccessLog.where(user_type: user_type_param).where(method_type: methoda_type_param).where(created_at: [from_time..to_time]).to_sql
csv_export = LargeCsvExporterService.new("access_logs", "*", ENV["DB_D_HOST"], ENV["DB_D_USERNAME"], "", ENV["DB_D_NAME"])
csv_export.set_file_name("AccessLog_#{Time.zone.now.strftime("%Y%m%d%S")}.csv")
csv_export.set_client
# results = csv_export.set_query("SELECT * FROM access_logs WHERE created_at BETWEEN '#{from_time}' AND '#{to_time}' AND method_type = '#{methoda_type_param}' AND user_type = '#{user_type_param}'" )
results = csv_export.set_query(sql_query)
csv_export.write_csv(results)
filepath, stat, csv_file_name = csv_export.export_csv_file
send_result = send_file(filepath, :filename => csv_file_name, :length => stat.size)
# csv_export.delete_created_csv_file
end
sqlクエリはこんな感じのが1行で出力される
sql
SELECT `access_logs`.*
FROM `access_logs`
WHERE `access_logs`.`user_type` = 'user'
AND `access_logs`.`method_type` IN ( 'GET', 'POST' )
AND `access_logs`.`created_at` BETWEEN
'2020-08-25 00:00:00' AND '2020-09-27 23:55:35'
まぁ、こんな感じで、実際に時間は計測していないが10万件くらいのファイルでも10秒くらいで出力できるようになった。