ENECHANGE Developer Blog

ENECHANGE開発者ブログ

AIエージェントでデータベースクエリ最適化を実現した体験談

Webアプリケーションの開発において、データベースのパフォーマンス問題は避けて通れない課題です。サービスが成長し、ユーザー数やデータ量が増加するにつれて、SQLクエリの最適化はますます重要になります。

現在私は、Claude Codeなどの生成AIツールを用いて開発を行っており、AIエージェントによりSQL最適化を効率的に改善する取り組みを行いました。本記事では、実際のプロジェクトで得られた体験談を共有します。

AIツール活用の背景

私が担当しているWebアプリケーションでは、パフォーマンス監視ツールを活用して大きなボトルネックになっている箇所は継続的に改善していました。しかし、優先度が低く後回しになりがちな箇所への対応が課題となっていました。

また、新機能開発時には、パフォーマンスを考慮した実装の検討に時間がかかることが多く、コードレビュー時においても、潜在的なパフォーマンス問題までレビューしようとすると時間がかかってしまう問題がありました。

そこで、AIツールを普段の開発フローに取り入れることで、これまで見落としていた改善点を効率的に発見し、開発スピードを向上させることを目指しました。特に日常的な開発作業やコードレビューでAIツールを活用することで、より質の高いコードを書けるようになりました。

具体的な活用例

以下では、実際にAIツールを活用してSQL最適化を行った具体例を紹介します。

N+1問題の検出と解決

N+1問題は良くないものとして認識していましたが、パフォーマンスに大きく関わる箇所は日々改善していた一方、データ件数が少ない箇所や使用頻度の低い機能では後回しになっていました。

AIツールを使ったN+1問題の検出プロセスは、これまで見過ごしていた箇所を効率的に発見するのに役立ちました。問題の有りそうなコードを見つけてClaudeに投げると、即座に「N+1問題が発生しています」という指摘と共に、具体的な改善案が提示されます。

複雑なSQLのパフォーマンス改善

複雑な集計クエリなどの検討をした際、AIツールに既存または検討中のSQLを分析させると、サブクエリの最適化やJOINの順序変更、ウィンドウ関数の活用など、複数の改善案を提示してくれました。下記に実際に改善を行ったサンプルコードを示します。

CASE文を活用した集計の最適化

例えば、注文データを分析する際、ステータス別の集計を別々のクエリで実行していました

# Before: 3回のデータベースアクセスが発生
completed_count = Order.where('created_at >= ?', '2025-09-01')
                       .where(status: 'completed').count
completed_amount = Order.where('created_at >= ?', '2025-09-01')
                        .where(status: 'completed').sum(:total_amount)
cancelled_count = Order.where('created_at >= ?', '2025-09-01')
                       .where(status: 'cancelled').count

この実装をAIに最適化してもらうため、以下のようなシンプルなプロンプトを使いました:

「このActive Recordのクエリが3回実行されています。パフォーマンスを改善するために、1回のクエリで同じ結果を取得する方法を教えてください。CASE文やその他のSQL機能を使った最適化を提案してください。」

AIは以下の改善案を提示してくれました:

# After: 1回のクエリで全ての集計を完了
stats = Order.where('created_at >= ?', '2025-09-01')
             .select(
               "COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed_count",
               "SUM(CASE WHEN status = 'completed' THEN total_amount END) as completed_amount",
               "COUNT(CASE WHEN status = 'cancelled' THEN 1 END) as cancelled_count"
             ).take

# 結果の使用
puts "完了注文数: #{stats.completed_count}"
puts "完了注文の合計金額: #{stats.completed_amount}"
puts "キャンセル注文数: #{stats.cancelled_count}"

ウィンドウ関数による時系列データ分析

各ユーザーの最新注文と前回注文を効率的に取得する処理でN+1問題が発生していました

# Before: 前回注文を個別に検索(N+1問題)
users_with_orders = Order.distinct.pluck(:user_id).sort

users_with_orders.map do |user_id|
latest_order = Order.where(user_id: user_id).order(created_at: :desc).first
previous_order = Order.where(user_id: user_id)
                      .where('created_at < ?', latest_order.created_at)
                      .order(created_at: :desc)
                      .first

if previous_order
  days_diff = (latest_order.created_at.to_date - previous_order.created_at.to_date).to_i
  "ユーザー#{user_id}: #{days_diff}日ぶりの注文"
else
  "ユーザー#{user_id}: 初回注文"
end

この実装をAIに最適化してもらうため、以下のようなシンプルなプロンプトを使いました:

「各ユーザーの最新注文と前回注文を取得するN+1問題が発生しています。ウィンドウ関数などを使って効率化して下さい。ただし元のコードのソート順などは同じになるように考慮するようにして下さい。」

AIが提案した最適化:

# After: ウィンドウ関数で1回のクエリに最適化
sql = <<-SQL
SELECT user_id, created_at, previous_order_date, order_rank
FROM (
  SELECT 
    user_id,
    created_at,
    LAG(created_at) OVER (PARTITION BY user_id ORDER BY created_at, id) as previous_order_date,
    ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC, id DESC) as order_rank
  FROM orders
) ranked_orders
WHERE order_rank = 1
ORDER BY user_id
SQL

latest_orders_with_history = Order.find_by_sql(sql)

# 使用例
latest_orders_with_history.map do |order|
  if order.previous_order_date
    days_diff = (order.created_at.to_date - order.previous_order_date.to_date).to_i
    "ユーザー#{order.user_id}: #{days_diff}日ぶりの注文"
  else
    "ユーザー#{order.user_id}: 初回注文"
  end
end

これにより、ユーザー数×2回のクエリ実行を1回に集約でき、処理時間が大幅に短縮されました。

インデックス最適化

既存のコードベースを定期的に見直し、パフォーマンスが改善できそうな箇所の洗い出しにAIツールを活用しました。特に、実行頻度の高いクエリや、データ量の増加が予想されるテーブルに関連するコードをAIに分析させることで、インデックスが不足している箇所を効率的に発見できました。

AIは単純な単一カラムのインデックスだけでなく、複合インデックスの必要性も指摘してくれます。例えば、頻繁に実行される範囲検索や、カーディナリティを考慮したインデックス設計を提案してくれるため、より効果的なインデックス戦略を立てることができました。

コードレビューでの活用

コードレビュー時にパフォーマンス周りの評価をAIツールに行わせることで、N+1問題やインデックスの最適化、クエリの最適化などをリリース前に検証できるようになりました。

新機能追加時のプルリクエストでは、AIにコード差分を分析させることで、潜在的なパフォーマンス問題を早期に発見できます。インデックスの付け忘れや、非効率なクエリの書き方など、人間のレビューでは見落としがちな問題も検出可能になりました。

個人的には「レビューの質が向上し、学習効果も高い」と実感しています。AIの指摘を通じて、なぜそのインデックスが必要なのか、どのような問題が発生する可能性があるのかを理解できるため、単なる指摘以上の教育効果がありました。

既存コードとは異なるアプローチの発見

既存のコードを流用して開発することが多い中、AIツールにゼロベースでコードを書かせることで、より効率的な実装方法を知ることができました。

AIは既存コードの慣習に縛られずに、パフォーマンスと可読性を両立した実装を提案してくれるため、チーム全体のコード品質向上につながりました。なぜその方法が効率的なのかについても説明してくれるため、単なるコピペではなく、理解を深めながら新しい知識を習得できました。

SQLからORマッパーへの変換支援

複雑なSQLクエリをORマッパーに変換することで、保守性や開発効率を向上させたいケースがありました。出力内容やソート、クエリ条件などを変更する際に、生のSQLよりもORマッパーの方が修正しやすいためです。

SQLからORマッパーへの変換作業は地味に時間がかかっていましたが、AIは比較的正確なコードを提案してくれました。ただし、AIの提案が間違っていることもあるため、動作確認は必須です。

実践での学びと成果

AIツール活用の効果

AIツールの導入により、着実にクエリ数を減らし、システム全体のパフォーマンスを底上げすることができました。個々の改善は小さなものでも、積み重ねることで問題の発見から解決まで大幅に効率化できました。

それだけでなく、SQLを精度高く作成できるようになり、開発スピードが飛躍的に向上しました。複雑なクエリの構築時にも、AIの提案を活用することで、考え込む時間を削減し、コード品質と生産性を同時に高めることができました。

注意点と限界

もちろん、AIツールには注意すべき点もあります。実環境のデータ量やアクセスパターンを完璧に把握しているわけではないため、提案内容が必ずしもベストソリューションとは言えません。

さらに、AIが提案する最適化を実装する時は、本番環境でのパフォーマンス検証が不可欠です。テスト環境での効果が、必ずしも本番の大量データで再現されるとは限らないためです。

まとめと今後の展望

AIツールは、SQL最適化において強力な補助ツールとして機能します。N+1問題の検出、複雑なSQLの改善、インデックスの最適化、コードレビューでの活用、そして既存とは異なる効率的な実装方法の発見など、様々な場面で開発効率と品質の向上に貢献しました。

大切なのは、AIツールの提案をそのまま使うのではなく、自分たちの状況に合わせて調整することです。AIの提案を参考にしながら、実際の環境に最適な方法を選ぶことが重要です。

これからもAIツールをうまく活用しながら、より効率的で高品質なアプリケーション開発を目指していきます。AIと人間の知識を効果的に組み合わせることで、パフォーマンスの高い価値のあるWebサービスを提供し続けていきたいと思います。