Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Ruby on Rails データベースパフォーマンス完全ガイド:テストと最適化の実践戦略

この記事では、Railsアプリケーションにおけるデータベースのパフォーマンスをテストする方法と、よくあるデータベース性能問題への具体的な解決策を解説します。

Railsアプリケーションを開発する際、データベース操作はデフォルトでActiveRecordが担当します。ActiveRecordは.where.save.create.updateといった直感的なコマンドで、データの取得や登録を手軽に行える強力なインターフェースです。RailsがこれらのコマンドをSQLクエリへ自動変換してくれるのは大きな利点ですが、その裏側では意図しないパフォーマンス問題が発生することもあります。開発者としては、典型的な問題点と最適化手法をきちんと理解しておくことが重要です。

Ruby on RailsにおけるActiveRecordとは

RailsのActiveRecordは、MVC(Model-View-Controller)アーキテクチャにおけるModel層の中核であり、データベースをビジネスオブジェクトとして扱うための仕組みです。ORM(Object-Relational Mapping)という技法を採用し、アプリケーションのオブジェクトとリレーショナルデータベースのテーブルをシームレスに結び付けます。

それでは、本題に入りましょう。

Railsでデータベース性能問題を特定・テストする3つの方法

1. ActiveRecordクエリにEXPLAINを実行する

EXPLAIN文は、SQLクエリの実行計画(どのようにクエリが処理されるか)を表示してくれます。具体的には、スキャンされる行数、使用されるインデックス、テーブル同士の結合方法などの情報が得られます。

実行計画を確認することで、以下のような観点からクエリのボトルネックを突き止められます。

  • クエリのパフォーマンス向上のために、どのインデックスを追加すべきか。
  • テーブルの結合順序が最適かどうか。必要に応じてSTRAIGHT_JOINを使って結合順序を強制し、性能を改善できます。

なお、EXPLAINはSELECTだけでなく、DELETE、INSERT、REPLACE、UPDATE文に対しても利用可能です。

ActiveRecordでの使い方は非常にシンプルで、クエリの末尾に.explainを付けるだけです。


.explainを追加すると、ActiveRecordのコマンドに対するクエリプランが出力されます。上記の例は主キーであるidを使ったごく単純なクエリですが、EXPLAINの結果を見るとpkey(主キーインデックス)が使用されていることがわかります。この場合、ステートメントは最適かつ高速であると判断できます。

遅いと感じるクエリに.explainを付けて実行すれば、実行順序や使用されているインデックスを把握できます。クエリプランにSeq Scan(シーケンシャルスキャン=全行スキャン)が表示された場合は、インデックスが使われていないサインなので、クエリの修正または新しいインデックスの追加が必要です。

次にJOINを含む例を見てみましょう。


この例ではusersテーブルとcollaborationsテーブルが結合されています。クエリプランを確認すると、collaborationsテーブルはseq scanで先に実行され、一方usersテーブルはpkeyインデックスを使って後から実行されているのがわかります。このような場合は、collaborationsテーブルのuser_idカラムにインデックスを追加することでクエリを最適化できます。EXPLAINを活用すれば、クエリを分解して分析し、どこを最適化すべきか明確にできるのです。

2. 重要なデータベースメトリクスを測定する

クエリの実行時間だけがパフォーマンスの指標ではありません。以下のような他のデータベースメトリクスも併せて監視しましょう。

  1. CPU使用率
  2. メモリ使用量
  3. I/O待ちのディスクキュー
  4. ネットワーク帯域幅(送受信トラフィック)
  5. 空きディスク容量
  6. スループット

これらの指標が一定の閾値を超えると、クエリが遅延することがあります。パフォーマンス問題を正しく理解するには、時間軸に沿ったデータポイントを継続的に観察することが大切です。

どのデータポイントを測定すべきかは、次のようなさまざまな要因によって変わります。

  • データベースの種類:

    • リレーショナルDB
    • インメモリDB
    • NoSQL
    • データウェアハウス
  • サーバーのホスティング形態:

    • オンプレミス
    • クラウド

データベースメトリクスの監視方法に唯一の正解はなく、環境や要件に応じて最適なアプローチを選ぶことになります。

3. AppSignalでRailsアプリのパフォーマンスを計測する

すべてのクエリを一元的に可視化できる場所がないと、パフォーマンスメトリクスの管理は難しくなります。個々のコードブロックにパフォーマンス計測コードを埋め込んでいくのは、手間がかかるうえに管理も煩雑になりがちです。

AppSignalのようなツールを使えば、パフォーマンス計測機能をアプリケーションに簡単に統合できます。AppSignalはRailsを標準サポートしており、「AppSignal for Ruby」の公式ドキュメントに従えば、シンプルな手順でインストール可能です。

特に注目すべき重要なメトリクスは以下の通りです。

  • スロークエリ(遅いクエリ)
  • スループットベースのデータベースパフォーマンス
  • N+1クエリの発生状況
  • データベースレイテンシ
  • アクティブな接続数

実際のAppSignalダッシュボードは次のような見た目になります。

Ruby on Rails データベースパフォーマンス完全ガイド:テストと最適化の実践戦略

Railsデータベースパフォーマンスを最適化する7つの方法

1. Eager LoadingでN+1クエリ問題を解消する

N+1クエリは、最も頻繁に発生するデータベースパフォーマンス問題です。userモデルとprojectモデルという2つのモデルを持つケースで例を見てみましょう。


ここで、ユーザー名とプロジェクト名を取得したい場合、次のようなコードを書くかもしれません。


上記のコードはループのたびにデータベースへクエリを発行するため、パフォーマンス問題を引き起こします。実行されるクエリの総数は「ユーザー数+1」となるのです。

この問題の解決は非常に簡単で、関連をeager loading(先行読み込み)するだけです。クエリの末尾に.includes(:projects)を追加しましょう。


こうすると、プロジェクトが事前にまとめて読み込まれるため、ループ実行時に個別のクエリは発生しません。


さらに、Rails 6.1ではstrict loading機能が導入され、関連データにアクセスする前に必ずeager loadingが行われていることを保証できます。有効化するには、モデルに次の1行を追加します。


以降、eager loadingなしでプロジェクトへアクセスしようとすると、RailsはActiveRecord::StrictLoadingViolationError例外をスローして教えてくれるようになります。

Rails 6.1未満のバージョンをお使いの場合は、Bulletなどのgemを活用するとよいでしょう。

2. データベースインデックスを活用する

データベースのインデックスは、データ検索を高速化するための基本機能です。前述のEXPLAINコマンドを使えば、クエリが適切なインデックスを使用しているかどうかを確認できます。

既存のインデックスを使うように遅いクエリを書き換えたり、新しくインデックスを追加したりすることで、パフォーマンスを改善できます。

MySQLには4種類のインデックスがあります。

  1. 主キー(Primary key)— 主キーには自動的にインデックスが作成され、同時に一意性も保証されます。
  2. ユニーク(Unique)— 指定した属性に追加される値が常に一意であることを保証します。
  3. 通常インデックス(Index)— 主キー以外の属性に追加します。
  4. 全文検索インデックス(Full text)— 文字列ベースのデータに対する検索に役立ちます。

インデックスはBツリー構造またはハッシュ形式で保存されます。

インデックスは単一カラムにも複数カラムの組み合わせ(複合インデックス)にも設定できます。複合インデックスは、複数のカラムを含むクエリの最適化に特に有効です。単一カラムのインデックスしかない場合、大量のデータセットをスキャンする必要が生じることがあります。

例えば、次のクエリには2つのフィールドが含まれています。


プロジェクト"abc"には多数のユーザーが所属している可能性があり、countryカラムのスキャンが必要になります。結果セットが大きくなると処理は遅くなりがちです。このようなケースでは、projectカラムとcountryカラムの両方に複合インデックスを作成することで、パフォーマンスを大幅に改善できます。

Railsでは、次のようなActiveRecordマイグレーションでインデックスを追加できます。

単一インデックスの場合:


複合インデックスの場合:


3. LIMITで取得件数を制限する

返却されるレコード数が多くなるほど、パフォーマンスは低下します。大量のデータセットを1回のクエリで取得するよりも、複数回のクエリに分割する方が効率的です。


次の100件を取得するには、offsetを指定します。


これによりパフォーマンスは大幅に向上します。ただし注意点として、offsetの値が大きくなるほどクエリは遅くなる傾向があるため、offsetとlimitは必ずセットで使うようにしましょう。

4. 大量データの読み込みにはfind_eachを使う

レコードを反復処理する際は、Railsのバッチ処理機能を活用するとパフォーマンスが向上します。


このコードは全レコードを一度に取得するため、メモリ消費とデータベース負荷の両面で問題が起こりえます。

find_eachfind_in_batchesを使えば、同じ処理をバッチ単位で実行でき、パフォーマンスを改善できます。


find_eachはデフォルトで1,000件ずつバッチ処理を行います。バッチサイズは引数で自由に変更可能です。


また、実行したい処理に応じてfind_in_batchesを使うこともできます。find_in_batchesfind_eachの違いは、前者が個々のレコードではなくモデルの配列として結果をyieldする点です。

5. Pluckで必要なフィールドだけを選択する

Pluckコマンドは、クエリ結果をActiveRecordオブジェクトではなく配列として直接取得します。

クエリが大量の結果を返す場合、Pluckを使うことでコードのパフォーマンスが向上します。Pluckはデータベースから必要なフィールドのみを選択します。


結果はメインテーブルではなくインデックスから取得されるため、ソートを伴うクエリでは特に効果を発揮します。

6. バルク操作を活用する

バルク削除
ActiveRecordオブジェクトをループして削除すると、レコードが1件ずつ削除されてしまいます。


各レコードを個別に削除する方式では、データベースへのクエリ発行回数が膨大になります。代わりに、単一のバルクdelete_allクエリを使うのが最適です。


バルク作成
意外と知られていませんが、バルク削除と同様に、ActiveRecordではバルクインサート(一括挿入)も可能です。これにより、n件分のクエリをわずか1回に削減できます。ActiveRecord::Basecreateメソッドは、ハッシュの配列を受け取れます。


7. 必要に応じてインメモリ計算を使う

状況によっては、クエリを重ねるよりもインメモリでの計算の方が適切な場合があります。例えば、ユーザーレコードが存在しない国をデータベースから探したいとしましょう。


上記のクエリは、結果を得るためにN回のクエリが必要です。代わりに、対象の国に属するユーザーを1回のクエリで取得し、残りの判定をメモリ上で行う方法があります。


また、リクエスト・レスポンスサイクルを再利用してデータベース負荷を軽減するために、キャッシュを活用するのも有効です。Railsにはページキャッシュ、アクションキャッシュ、フラグメントキャッシュの3種類があり(フラグメントキャッシュは標準で提供されています)、用途に応じて使い分けられます。

まとめ:ActiveRecordとAppSignalでRailsパフォーマンスを最適化しよう

それでは復習です。この記事では、まずRailsにおけるデータベース性能問題を特定・テストする3つの方法として、以下を紹介しました。

  • ActiveRecordクエリへのEXPLAIN実行
  • 重要なデータベースメトリクスの測定
  • AppSignalによるRailsアプリのパフォーマンス計測

さらに、データベースパフォーマンスを最適化する7つの方法として、以下を解説しました。

  • Eager LoadingによるN+1クエリ対策
  • データベースインデックスの活用
  • LIMITによる取得件数の制限
  • 大量データ読み込みのためのfind_each
  • 必要フィールド選択のためのPluck
  • バルク操作
  • インメモリ計算

Railsを使えば、アプリケーション開発を迅速かつ簡単に進められます。ActiveRecordは、データベースコードの生産性・再利用性・保守性を高めてくれる頼もしい存在です。だからこそ、ActiveRecordのクエリがどのようなSQLに変換され、どう実行されるのかを理解しておくことが重要になります。

しかし、Railsのデータベースパフォーマンスを最適化するうえで最も重要なのは、パフォーマンスデータの可視化です。パフォーマンス問題は誰にでも起こり得ますが、適切に可視化されていれば必ず解決できます。データベースメトリクスを提供してくれる信頼できる監視ツールを導入しましょう。私たちのおすすめはもちろんAppSignalです;)

P.S. Ruby Magicの最新記事をいち早く読みたい方は、Ruby Magicニュースレターをご購読ください。記事を見逃すことはありません!

  1. Kredisを使ってRailsアプリに一時的なUI状態を簡単に保存する方法

    Kredis(Keyed Redis)は、Rails開発者のツールキットに最近追加されたライブラリです。Redis上での構造化データの保存とアクセスを簡素化することを目指しています。 この2部構成シリーズの第1回では、まずKredisの仕組みについて詳しく解説します。その後、独自のRedisキーを使用して一時的なUI状態を保存する実用的なユースケースを紹介します。 それでは始めましょう! RailsにおけるKredisの概要 KredisはRailtieとして提供されており、以下の3つの方法でRedisの利用を効率化する便利なラッパーを備えています。 RubyらしいAPI:例えばKredis

  2. Railsでクラスター間のアソシエーションをマスターする:disable_joinsによるマルチデータベース対応の完全ガイド

    Railsフレームワークの魅力のひとつは、モデル間のアソシエーション(関連付け)を活用できる点です。Active Recordのアソシエーションを使えば、複雑なSQLクエリを意識することなく、美しいRubyの構文でレコードのコレクションにアクセスできます。しかし、この抽象化が機能するのは、すべてのデータが1つのデータベースに存在している場合だけです。テーブルが複数のデータベースクラスタに分散した瞬間、特定の種類のアソシエーションは動作しなくなります。 本記事では、その境界線がどこにあるのか、そしてRailsがその制約の中で何を提供してくれるのかを詳しく解説します。まず問題が発生する理由と影響を