チューニング

クエリ高速化チェックリスト

計測 → 実行計画 → SQL → インデックス → 設計 → アプリ層の順でたどる、遅いクエリ改善チェックリスト。

このチェックリストの使い方

「クエリが遅い」と言われたら、勘で CREATE INDEX を足すのではなく、上から順にチェックしてください。より安く・安全に効果が大きい順に並べてあります。各項目は詳細ページへのリンク付きで、深掘りしたいところだけ辿れる構成です。

  1. STEP 1: 測定 — そもそも「どこが遅いか」を知る
  2. STEP 2: 実行計画 — オプティマイザが何をしているかを読む
  3. STEP 3: SQL の書き方 — クエリ自体で直せるものを直す
  4. STEP 4: インデックス — 不足・過剰・列順・カバリングを点検
  5. STEP 5: スキーマ設計 — 型・正規化・主キー選定
  6. STEP 6: 接続・アプリ層 — N+1 / プール / トランザクション
  7. STEP 7: 物理的な手段 — パーティション・レプリカ・リライト
  8. 最終チェック — 本番投入前に確認する項目

「STEP 4 から始めたい」気持ちは分かりますが、測定せずにインデックスを足すと直ってないのに直った気になるのが一番事故です。STEP 1 を飛ばさないでください。

STEP 1: 測定 — 何がどれだけ遅いのか

先にやるべきはどのクエリが重いかの特定です。感覚で選ばず、合計時間(スループット)と 1 回あたり時間(レイテンシ)の両方で上位を出します。

チェック項目詳細
PostgreSQL で pg_stat_statements を有効化し、total_exec_time / mean_exec_time / calls 降順で Top 10 を確認スロークエリ特定
MySQL でスロークエリログを有効化し、pt-query-digest で集計スロークエリ特定
APM(Datadog / New Relic / Sentry / OpenTelemetry)で「1 リクエスト中の DB 時間」を可視化アプリ層の詰まりも同時に見る
再現環境を用意し、プロダクションと同等のデータ量で計測(1 万行と 1 億行では計画が変わる)小さい DB で速くても意味なし
改善前後で比較できるよう、現状値をスナップショットとして残す後の STEP 8 で答え合わせ

見るべき 2 種類: (1) 1 回が遅いクエリ(レイテンシ観点)。(2) 1 回は速いが合計時間が大きいクエリ(N+1 や頻発クエリの疑い)。両方チェックしてください。

STEP 2: 実行計画を読む — 推測ではなく事実で判断

重いクエリが特定できたら、必ず EXPLAIN で計画を見ます。頭の中でなんとなく「インデックスが使われているはず」は外れます。

チェック項目詳細
PostgreSQL: EXPLAIN (ANALYZE, BUFFERS) を実行。推定行数と実測行数の乖離を見る(10 倍以上ずれていれば統計情報の再取得)PostgreSQL EXPLAIN
MySQL: EXPLAIN ANALYZE または EXPLAIN FORMAT=TREEtype=ALL(フルスキャン)や Using temporary; Using filesort に注目MySQL EXPLAIN
SQL Server: SSMS の「実際の実行プラン」。Key Lookup の多発・Hash Match の Spill・推定行数の警告(黄色三角)SQL Server 実行計画
Oracle: DBMS_XPLAN.DISPLAY_CURSOR で A-Rows(実測)と E-Rows(推定)を比較Oracle 実行計画
Seq Scan / ALL が悪とは限らない。小テーブルや 90% 以上を読むクエリではむしろ最速実行計画の基本
JOIN が Nested Loop / Hash / Merge のどれで処理されているか、選択が自然か不自然なら統計情報かインデックス不足のサイン

STEP 3: SQL の書き方を見直す — インデックスを足す前に

インデックスの前に、SQL そのものの書き方で直せる問題を潰します。多くの場合、ここで 5 割以上改善します。

チェック項目詳細
SELECT * をやめる。必要な列だけ取得(カバリング成立・ネットワーク量削減)SELECT
WHERE col = ? の左辺に関数や演算を使わない(LOWER(col), col + 1 はインデックスが使われない → 関数索引か書き換え)SARGable なクエリ
暗黙の型キャストをなくす(varchar 列に数値を比較するとインデックス不使用)暗黙の型キャスト
OR より UNION ALL / IN (...) / EXISTS が速いことがある(特に異なる列の OR)OR の書き換え
相関サブクエリJOIN や CTE に書き換えられないかJOIN
DISTINCT / GROUP BY を重複原因で使っていないか(JOIN 多重でデータが増えているだけ)根本の JOIN を見直す
LIMIT + 大きな OFFSET はページが進むほど遅い → キーセット・ページネーションページング設計
ウィンドウ関数・CTE で中間行数を大幅に減らせないかウィンドウ関数
NOT IN (...) は NULL で想定外の挙動。NOT EXISTS に置き換えNULL 安全

STEP 4: インデックスを見直す

SQL を整えてもまだ遅いならインデックス。足すだけでなく、消す・順序を直す・カバリングにする視点を忘れずに。

チェック項目詳細
WHERE / JOIN / ORDER BY / GROUP BY で使われる列にインデックスがあるかインデックス入門
複合インデックスの列順: 等値 → 範囲 → ソートの順。左端一致ルール複合インデックス
カバリングに持ち込めるか(PostgreSQL / SQL Server の INCLUDE)。EXPLAIN で Index Only Scan / Using index を確認カバリング
使われていないインデックスを削除(PostgreSQL: pg_stat_user_indexes、MySQL: sys.schema_unused_indexes)。書き込みコストだけ払っている状態を解消インデックスの罠
重複インデックスの整理: (a, b) があれば (a) 単独は不要ストレージ・書き込み節約
カーディナリティが低すぎる列(boolean 等)に単独インデックスを貼っていないか部分インデックスを検討
部分インデックス(PostgreSQL WHERE ...、SQL Server フィルタ付き)で狙い撃ち部分インデックス
外部キー列にインデックスがあるか(多くの RDBMS で自動生成されない)FK とインデックス
1 テーブルあたり 5〜7 本を目安に。書き込み重視のテーブルは更に少なく貼りすぎは書き込み劣化
本番投入は CREATE INDEX CONCURRENTLY(PostgreSQL)/ オンライン DDL(MySQL / SQL Server)でロック回避

STEP 5: スキーマ設計を見直す

ここまで来てまだ遅い、あるいは慢性的に遅いテーブルには設計レベルの問題があることが多いです。後から直すほど高コストなので、思い切って俎上に載せます。

チェック項目詳細
主キーの型: UUID v4 を使っていないか(B-Tree 断片化)。UUID v7 / BIGINT への移行を検討主キーの選び方
列の型が大きすぎないか(TEXT で済むものを VARCHAR(65535)INT で十分なものを BIGINT型選択
過剰な正規化で JOIN が増えすぎていないか(読み取り中心なら非正規化が勝つこともある)正規化
JSON / 配列列を検索条件に使うなら、生成列 + インデックス、または GIN / FTS を検討ネイティブ型 + 専用インデックス
履歴データを同じテーブルに混ぜていないか(履歴テーブル分離で現行テーブルを小さく保つ)履歴テーブル
集計結果を都度計算するクエリが重いなら、マテリアライズドビューや集計テーブル読み取り最適化
統計情報が古くないか(PostgreSQL: ANALYZE、MySQL: ANALYZE TABLE、SQL Server: UPDATE STATISTICS計画が歪む原因

STEP 6: 接続・アプリ層・トランザクション

DB が悪いとは限りません。アプリと DB の間で詰まっているケースは想像以上に多いです。

チェック項目詳細
N+1 クエリがないか(ORM のループ内でリレーションアクセス)。eager loading / DataLoader / IN バッチで解消N+1 問題
コネクションプールのサイズは適切か((cores * 2) + spindles が目安)コネクションプール
サーバーレス / 多 worker 構成では PgBouncer / RDS Proxy を挟めているか外部プーラ
statement_timeout / idle_in_transaction_session_timeout を設定しているか(長時間ロック防止)プール枯渇を回避
トランザクション境界が広すぎないか(読み取りだけの処理を巨大 TX で囲わない)トランザクション
ロック粒度 / 分離レベルが適切か(SERIALIZABLE を無意識に使っていないか、READ COMMITTED で十分か)分離レベル
アプリが大量取得 → メモリでフィルタをしていないか(WHERE を DB に任せる)ネットワーク転送量の削減
ネットワーク RTT が大きいなら、バッチ化・prepared statement・キャッシュ層1 リクエスト内の往復回数を減らす

STEP 7: 物理的な手段 — パーティション・レプリカ・キャッシュ

ここまで来て「テーブルが数億行あって、どうしてもこれ以上縮められない」なら、物理層の武器を投入します。運用コストが上がる施策なので、安易に使わないこと。

チェック項目詳細
日付など単調増加するキーでパーティショニング可能か(古いデータを DROP PARTITION で一瞬削除できる)パーティショニング
読み取り負荷が高いならリードレプリカへオフロード(結果整合の許容範囲を確認)書き込みはプライマリ固定
ホットデータにRedis / memcached 等のキャッシュ層。無効化戦略(TTL / write-through)をセットで設計キャッシュの一貫性問題に注意
全文検索が重いなら Elasticsearch / OpenSearch / pg_trgm / FTSRDBMS 単体で戦わない
集計クエリが恒常的に重いなら、OLAP(BigQuery / ClickHouse / Redshift / DuckDB) に ETLOLTP と OLAP を分離
ハードウェア: SSD / NVMe か、メモリは shared_buffers に十分割り当てているかクラウドならインスタンスクラス見直し

注意: パーティションやレプリカは運用負債を増やします。インデックス・クエリ書き換えで限界を見極めてから採用するのが原則です。

本番投入前の最終チェック

修正をマージする前に、以下を必ず確認してください。「開発環境では速かったのに本番で問題発生」の多くはここを省略したときに起きます。

  • 本番相当のデータ量で EXPLAIN ANALYZE を再実行し、計画が想定通りか確認
  • 追加したインデックスの書き込み影響を測定(INSERT / UPDATE のスループット)
  • DDL のロック時間: CREATE INDEX CONCURRENTLY / オンライン DDL / pt-online-schema-change / gh-ost を検討
  • 他のクエリへの副作用(ある条件で速くなる代わりに別のクエリが遅くなっていないか)
  • ロールバック手順(インデックス削除 / マイグレーション取消)を準備
  • 投入後、pg_stat_statements / slow log で翌日再計測。該当クエリが下がっていることを確認
  • アラート / ダッシュボードで該当クエリの時系列を監視

チェックリストを上から辿っても改善しない場合は、根本原因が RDBMS の外(ネットワーク、アプリ設計、そもそもの要件)にあることが多いです。「SQL の問題である」という前提を一度疑ってみてください。