DEV Community

Kaziu
Kaziu

Posted on

SQL複数カラムの計算結果をインデックスにまとめる設計手法と、書き込みオーバーヘッドの限界

データベースで複数のカラムを掛け合わせた検索(例:(単価 × 数量 - 割引) × 税率)を実行すると、通常は全件スキャン(Table Scan)が発生して大幅な遅延を引き起こします。

この記事では、複数カラムの計算結果を単一または複合インデックスへ集約する方法と、気になる書き込み時のオーバーヘッド(遅延リスク)のレベル感・警戒時の対策について解説します。


1. 複数カラムの計算結果をインデックスにまとめる2つの手法

例えば、以下の 5 カラムを持つ orders テーブルを考えます。

  • price(単価)
  • quantity(数量)
  • discount(値引き)
  • tax_rate(税率)
  • status(注文ステータス)

「合計請求金額が 10,000 円以上の完了済み(completed)注文」を高速検索したい場合、アプローチは 2 通り存在します。

アプローチA:式インデックス(Expression Index)を直接貼る

計算式そのものをインデックスに定義し、status カラムと組み合わせた複合インデックスを作成します。

SQL (PostgreSQL)

CREATE INDEX idx_orders_total_amount_and_status ON orders (
  ((price * quantity - discount) * (1 + tax_rate)), 
  status
);
Enter fullscreen mode Exit fullscreen mode

Rails migration

class AddCalculatedIndexToOrders < ActiveRecord::Migration[7.0]
  def change
    add_index :orders, 
              ["((price * quantity - discount) * (1 + tax_rate))", :status],
              name: "idx_orders_total_amount_and_status"
  end
end
Enter fullscreen mode Exit fullscreen mode

アプローチB:生成列(Generated Column) + 複合インデックス【おすすめ】

式が複雑な場合、テーブルに「計算結果カラム」を定義してそこにインデックスを貼る方が、SQLの可読性とメンテナンス性が格段に向上します。

Rails (Migration)

class AddTotalAmountToOrders < ActiveRecord::Migration[7.0]
  def change
    change_table :orders do |t|
      # 1. 4つのカラムを使った計算列を定義(DB側で自動計算&保存される)
      t.virtual :total_amount, 
                type: :decimal, 
                precision: 10, 
                scale: 2, 
                as: "(price * quantity - discount) * (1 + tax_rate)", 
                stored: true

      # 2. 生成列と status の複合インデックスを作成
      t.index [:total_amount, :status], name: "idx_orders_total_amount_and_status"
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

2. 書き込み(INSERT / UPDATE)オーバーヘッドのレベル感

計算インデックスや生成列を使用すると、対象カラムのいずれか 1 つでも INSERT または UPDATE されるたびに「再計算 + B-Treeインデックスの再配置」が発生します。

パフォーマンスへの影響度は「データ件数」よりも「1秒あたりの書き込み・更新回数(TPS)」に依存します。

警戒レベル 1秒あたりの書き込み/更新数 (TPS) アプリへの影響・状況判定・推奨アクション
レベル 1:安全 〜 50 回/秒 一般的なWebシステムやECサイト。ミリ秒以下の処理で完結する。
対策不要(積極的に導入して良い)
レベル 2:注意 100 〜 500 回/秒 スパイク時のアクセスやSNSアプリ。書き込みのレイテンシがわずかに上昇。
モニタリング継続(Slow Queryログを監視)
レベル 3:警戒 1,000 回/秒 以上 IoTデータ受信、金融取引、大量バッチ処理。CPU使用率急増・ロック詰まりが発生。
要対策(アーキテクチャの見直しが必要)

どのくらいの「データ量」で影響が大きくなる?

データ量が増えると、「インデックス自体のサイズがメモリ(RAM)に入り切らなくなったタイミング」でパフォーマンスが急激に落ちます。

  • メモリに収まるサイズ(〜数百万件):
    インデックスの更新処理がすべてメモリ(RAM)上で完結するため、計算オーバーヘッドがあってもミリ秒以下(数ミリ秒)で終わります。

  • メモリに入り切らないサイズ(数千万件〜億単位):
    インデックスを更新するたびにディスク(SSD/HDD)への読み書きが発生します。こうなると、1回の UPDATE の遅延が数ミリ秒から数ミリ秒〜数秒へと跳ね上がります。

特に遅くなりやすい「ワーストケース」のパターン

同じ書き込みでも、以下のパターンは特にオーバーヘッドが大きくなります。

  • 更新が超頻繁なカラム(例:updated_at や status)が含まれている
    price などの金額は一度決まると滅多に変わりませんが、status や updated_at が計算式に含まれていると、ステータスが変わるたびに計算&インデックス再配置が発生します。

  • 一括更新(UPDATE ... WHERE や BULK INSERT)を行う
    1件ずつの更新なら数ミリ秒の遅延で済みますが、UPDATE orders SET price = price * 1.1 のように10万件を一括更新する場合、10万回の再計算とインデックス更新が同時に走り、DBが数秒〜数分フリーズします。

Top comments (0)