スキップしてメイン コンテンツに移動

N+1解消でRuby高速化

N+1解消でRuby高速化

N+1問題の概要と対策

Ruby on Rails で頻繁に発生する N+1問題は、親レコードを取得した後に子レコードを個別にクエリすることで、クエリ数が N+1 に増える現象です。これによりデータベースへの負荷が急増し、パフォーマンス改善が遅延します。まずは、ActiveRecord の includes を使って関連テーブルを一括でロードし、N+1問題を可視化することが重要です。

可視化には bulletrack-mini-profiler を導入し、実際に発生しているクエリ数を確認します。N+1問題が判明したら、次に eager_loadpreload の違いを理解し、適切な手法を選択します。

eager_load vs preload vs includes

includes はデフォルトで preloadeager_load を自動で切り替えます。preload は別々の SELECT 文を発行し、Ruby 側で結合します。一方、eager_load は SQL の LEFT OUTER JOIN を使用し、1 回のクエリで全データを取得します。

パフォーマンス改善の観点からは、テーブルが大きく結合が複雑な場合は eager_load が有効ですが、JOIN で返る行数が増えるとメモリ使用量が増えるリスクがあります。逆に、関連テーブルが小さく、結合が不要な場合は preload が高速です。実際のケースでは、includes を使い、preloadeager_load の両方を試し、EXPLAIN ANALYZE で実行計画を比較します。

User.includes(:posts).where(active: true).to_sql
# => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id" WHERE "users"."active" = 't'

キャッシュとインデックスでパフォーマンス改善

クエリ数を減らすだけでなく、キャッシュを活用することでレスポンス時間を短縮できます。Rails の Rails.cache を使い、頻繁に参照されるデータをメモリに保持します。例えば、人気記事一覧をキャッシュし、更新時に expire_fragment で無効化します。

また、インデックスはデータベースの検索速度を大幅に向上させます。WHERE 句で頻繁に使用されるカラムにインデックスを作成し、EXPLAIN でスキャンタイプを確認します。インデックスが無いとフルテーブルスキャンが発生し、ボトルネック特定の際に重要なポイントとなります。

add_index :users, :email, unique: true
# インデックス作成後の実行計画
EXPLAIN SELECT * FROM users WHERE email = 'example@example.com';

ボトルネック特定の実践手順

パフォーマンス改善を行う前に、まずボトルネック特定を徹底します。New RelicDatadog などの APM ツールでアプリケーション全体のリクエスト時間を可視化し、遅延が発生している箇所を特定します。

次に、SQL レベルで EXPLAIN ANALYZE を実行し、クエリの実行計画を確認します。インデックスが効いていない、JOIN が多すぎる、または N+1問題が残っている場合は、コードをリファクタリングします。最後に、キャッシュ戦略とインデックス設計を組み合わせ、実際のレスポンス時間を測定して改善効果を検証します。

このサイクルを継続的に回すことで、Ruby応用のスキルを磨きつつ、アプリケーションのパフォーマンスを安定的に向上させることができます。

この記事はAIによって作成されました。

コメント