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