SQLは速いのにAPIが遅い
データベースのクエリは高速です。キャッシュも機能しています。バックグラウンドジョブも問題ありません。
それなのに、APIは依然として遅い。CPU使用率は高いままです。
問題はデータベースではありません。問題はRubyレイヤーにあります。
ボトルネックは、データがデータベースを離れてアプリケーションに入った後に発生することがよくあります。これには主に3つの原因があります。
- 肥大化したシリアライズ
- 過剰なオブジェクト割り当て
- 重複する計算
解決策は以下の通りです。
1. 肥大化したシリアライズを止める
多くの開発者が、モデル全体をJSONに変換してしまいます。
render json: @shipments
もし出荷(shipment)に40個のカラムがあっても、フロントエンドが必要なのが5個だけであれば、CPUサイクルを無駄に消費しています。また、APIキーやコストなどの機密データが漏洩するリスクもあります。
解決策:必要なフィールドのみを返します。
render json: @shipments.as_json(only: [:id, :tracking_no, :status])
さらに高速化するには、pluck を使用してデータを配列として取得します。これにより、重い ActiveRecord オブジェクトの構築を完全に回避できます。
2. オブジェクト割り当てを減らす
Rubyが作成するすべてのオブジェクトはメモリを消費します。単一のリクエスト中に数千ものオブジェクトを作成すると、ガベージコレクタ(GC)の負荷が高まります。これがシステム全体の速度低下を招きます。
ループ内で新しいハッシュや文字列を作成するのは避けましょう。
悪い例:
@shipments.map do |s|
{ label: "#{s.tracking_no} - #{s.status.upcase}" }
end
良い例: 静的なデータをループの外に出します。Rubyではなく、データベース側でより多くの処理を行うようにします。
3. 重複する計算を避ける
1回のリクエスト内で同じメソッドを何度も呼び出すと、時間を無駄にします。
例:
def total_weight
shipments.sum(&:weight)
end
ビュー、ヘルパー、シリアライザーのすべてがこれを呼び出すと、合計値が3回計算されることになります。
解決策:メモ化(memoization)を使用します。
def total_weight
@total_weight ||= shipments.sum(&:weight)
end
これにより、計算がリクエストごとに一度だけ行われるようになります。
まとめ
| 問題 | 解決策 |
|---|---|
| 肥大化したシリアライズ | 必要なフィールドのみを返すか、pluck を使用する |
| 過剰な割り当て | ループ内でのオブジェクト作成を減らす |
| 重複する計算 | メモ化を使用して結果を再利用する |
データベースの最適化とは、要求するデータを減らすことです。アプリケーションの最適化とは、データ取得後に発生する無駄な作業を減らすことです。
Source: https://dev.to/danewu/your-sql-is-fast-but-the-api-is-slow-its-the-ruby-layer-2fno
