SQL最適化による性能改善

2026.04.28
SQL最適化による性能改善

SQL最適化による性能改善

症状の見極めと測定設計

体感の「遅い」を捌くには、まず遅さの種類を分解します。レイテンシ(p95/p99)、スループット(QPS)、コスト(スキャンバイト/行スキャン)、キャッシュの影響(コールド/ウォーム)を切り分けると手順が決まります。特に「rows examined / rows returned」の比率が高い場合は、インデックスやフィルタの押し込みが疑わしいです。

観測ポイントの優先順位

  • プラン: EXPLAIN/EXPLAIN ANALYZE(PostgreSQL)、EXPLAIN FORMAT=JSON(MySQL)、クエリプロファイラ(BigQuery/Snowflake/Redshiftのステージ詳細)。
  • ログ: スロークエリログのしきい値をp95目標の2倍程度に設定し、まず上位10本から。
  • メトリクス: CPU使用率、I/O待ち、ネットワーク転送量、バッファヒット率。行スキャンが多くI/Oがボトルなら索引・分割、CPUが高止まりなら重い関数やソートを疑います。
  • 実データでの再現: コールド/ウォームの両方で計測し、キャッシュに依らない改善か確認します。

計測は「変更前/後の同条件比較」が基本です。プランの差分、スキャン行数、スキャンバイト、p95を揃え、改善幅がKPI(例えばp95 500ms以下・スキャンバイト30%削減)に届いたら採用します。

クエリ書き換えの実践レシピ

  • SELECT * をやめる: 必要列だけ取得し、カバリングインデックスを狙います。返却列が多いほどI/Oとネットワークが増えます。
  • 関数は右辺に: WHERE created_at >= ‘2026-02-01’ のように列に関数を掛けない(date(created_at)は全表走査を招きがち)。
  • LIKEの先頭ワイルドカード回避: ‘%abc’は索引が効きません。前方一致に寄せるか、トライグラム/全文検索系へ委譲します。
  • ORは分割してUNION ALL: 選択度が違う条件は別クエリにしてUNION ALLすると、それぞれ最適なプランが選ばれることがあります。
  • INとEXISTSの使い分け: 重複排除不要で存在確認ならEXISTS、サブクエリが小さくメモリに乗るならINでも可。プランを見て判断します。
  • ORDER BY + LIMITはキーセットに: OFFSETは深いページほど遅くなります。WHERE (key < 前のkey) ORDER BY key DESC LIMIT N の「キーセットページネーション」に変更します。
  • CTEは注意: PostgreSQL 12+は多くがインライン化されますが、意図せずマテリアライズされる場合は一時テーブルやマテリアライズドビューへ明示的に逃がします。
  • JOINは選択度の高いテーブルから: 早く行数を絞ると後段の結合・ソートコストが落ちます。結合条件はONに、フィルタは可能な限り元テーブル側へ押し込みます。
  • DISTINCT/COUNT(DISTINCT)の乱用回避: 本当に必要かを見直し、代替として前処理で重複排除、近似関数、マート化を検討します。

プランでは「フィルタのプッシュダウン」「索引使用」「結合方式(Nested Loop/Hash/ Merge)」「ソートの発生」を確認します。目安として、rows examined / returned が10倍を超えるなら、さらなる書き換えや索引設計の余地が大きいです。

インデックスとデータ配置の意思決定

複合インデックスの順序設計

基本は「高選択度の列 → 並び替え列 → 取得頻度の高い列(INCLUDE)」です。例: ユーザー別の最新注文取得なら (user_id, created_at DESC) を主キーにし、よく参照する status, amount をINCLUDE(PostgreSQL/SQL Server)します。MySQLはINCLUDE構文はありませんが、対象列をインデックスに含めるとカバリングしやすくなります。

パーティションとクラスタリング

イベントやログ系は日付でパーティションし、直近Nパーティションだけをスキャンさせます。列指向DWH(BigQuery/Snowflake/Redshift)はパーティション+クラスタリングキー(例: created_at, user_id)でスキャンバイトを大きく削減できます。OLTPならクラスタ化インデックスやCLUSTERで局所性を上げ、レンジアクセスを高速化します。

統計とメンテナンス

プランナーは統計が命です。ANALYZE/VACUUM(PostgreSQL)、ANALYZE TABLE(MySQL)を定期運転し、ヒストグラムを有効化します。急増・偏ったデータ投入の直後は手動ANALYZEも検討します。実行ヒントは最後の手段で、まず統計とスキーマを正します。

身近な企業活用例:宅食D2Cのやり直し

月間注文は約100万件、イベントログは2億行。運用はアプリのOLTPにPostgreSQL、分析にBigQuery。成長とともにダッシュボードは30秒超、夜間ETLは7時間に伸び朝の集計が間に合わなくなりました。

失敗の芽は明確でした。APIは SELECT * でOFFSETページング、日付抽出に date(created_at)、CTEが多段で再計算、注文テーブルは非パーティション、索引は user_id 単独。BigQuery側もクラスタリングなしで全表スキャンが常態化していました。

対応は次の通りです。OLTPでは (user_id, created_at DESC) の複合索引を作成し、INCLUDE相当でよく使う列をカバー。ページングはキーセット方式へ変更し、日付条件は created_at BETWEEN … へ修正。重いCTEは一時テーブル化。分析側は月次パーティション+クラスタリング(created_at, store_id)を設定し、週次の売上マートを追加しました。

結果は、注文詳細APIのp95は2.3秒から180ms、BigQueryのスキャンバイトは68%削減、ETLは7時間から2.1時間、月間クエリコストは35%減に。クエリの下書きやプラン解釈のたたき台にChatGPTとClaudeを活用し、探索的分析のプロトタイピングはGemini、IDEの補完や誤用パターン検出はCopilotが役立ちました。最終判断は必ず実データでのEXPLAIN ANALYZEと負荷試験で行い、AI提案は「比較対象の一つ」として扱っています。

運用に効く型とチェックリスト

  • 性能予算を宣言: p95 500ms/スキャンバイト上限/メモリ使用量のSLOを定義し、逸脱時にアラート。
  • レビューの型: PRにEXPLAIN前後の差分を必須化、rows examined/returned、結合方式、ソート発生の有無を確認。
  • クエリ規約をリンター化: SELECT * 禁止、OFFSET禁止、列左辺の関数禁止、COUNT(DISTINCT)の審査制。
  • 代表データでAB計測: コールド/ウォームの両モード、実サイズでのベンチを自動化。
  • 高コスト集計は前処理: マート化、マテリアライズドビュー、結果キャッシュでオンライン負荷を削減。

SQL最適化は単発の「神リライト」ではなく、計測→仮説→実装→再計測の反復です。これをチームの作法として仕組みに落とすと、データ基盤の運用コストと意思決定のリードタイムが一緒に下がります。データ解析プラットフォーム事業では、同じハードをより多くの仮説検証に回せることが競争力になります。速く・安く・安全に回るSQLは、プロダクトの学習速度そのものを底上げします。