SQL Serverで「SQLが遅い」「インデックスを作ったのに速くならない」「なぜか tempdb が増える」といった問題を調べるとき、まず見たいのが実行計画です。実行計画を見ると、SQL Serverがどのテーブルやインデックスを読み、どの順序で結合し、どこでソートや集計をしているかを確認できます。
この記事では、SQL Serverの実行計画の基本、推定実行計画と実際の実行計画の違い、Table Scan、Index Seek、Key Lookup、Missing Index、Sort、Hash Match の見方、そして遅いSQLを調査するときの実務的な順番を整理します。
遅いSQLを見るときは、まず「実際の実行計画」を取得し、推定行数と実際の行数のズレ、全件スキャン、繰り返しの
Key Lookup、Sort や Hash Match の警告、Missing Index の提案を確認します。ただし実行計画のコストや Missing Index はヒントです。SET STATISTICS IO と SET STATISTICS TIME で読み取り量と時間も見て、本当に改善しているかを確認します。実行計画で最初に見る7項目
細かいプロパティを読む前に、まず次の7項目を見ると原因を絞り込みやすいです。
- 対象ステートメントのコストが高い箇所
- 太い矢印で大量行が流れている箇所
- 推定行数と実際の行数のズレ
Table Scan/Index Scan/Index Seekの違いKey Lookupが大量に繰り返されていないかSort、Hash Match、Spoolの警告STATISTICS IOの logical reads と実行時間
この7項目で問題候補を見つけてから、インデックス、統計情報、SQLの書き方、ロック待ちのどれを深掘りするか決めます。
SQL Serverの実行計画とは
実行計画は、SQL Serverのクエリオプティマイザーが選んだ実行手順です。同じ SELECT でも、テーブル全体を読むのか、インデックスで絞り込むのか、どのテーブルから結合するのか、途中でソートするのかによって処理時間は大きく変わります。
実行計画を見る目的は、単に「重そうなアイコン」を探すことではありません。SQL Serverがなぜその読み方を選んだのかを推測し、WHERE句、JOIN条件、インデックス、統計情報、取得列、並び替え条件のどこを直すべきかを判断することです。
| 見るもの | わかること | 改善につながる観点 |
|---|---|---|
| アクセス方法 | Table Scan、Index Scan、Index Seek など |
全件読みになっていないか、検索条件に合うインデックスがあるか |
| 行数 | 推定行数と実際の行数 | 統計情報が古い、条件の選択性を見誤っている、パラメータの偏りがある |
| 結合方法 | Nested Loops、Hash Match、Merge Join |
結合順序、JOIN条件、インデックス、入力行数が妥当か |
| 警告 | Spill、暗黙変換、Missing Index など | tempdb使用、型不一致、インデックス不足の候補を確認する |
| 読み取り量 | STATISTICS IO の logical reads |
実行計画上の見た目だけでなく、実際の読み取り量で判断する |
推定実行計画と実際の実行計画の違い
SQL Serverの実行計画には、主に推定実行計画と実際の実行計画があります。遅いSQLの調査では、可能であれば実際の実行計画を見ます。推定実行計画はSQLを実行せずに見られるため安全ですが、実行時の行数や警告が不足します。
| 種類 | 特徴 | 使いどころ |
|---|---|---|
| 推定実行計画 | SQLを実行せず、オプティマイザーが選びそうな計画を表示する | 更新系SQLや重いSQLを実行前に確認したいとき |
| 実際の実行計画 | SQLを実行したあと、実行時の行数や警告も含めて表示する | 遅いSQLの原因調査、推定と実測のズレ確認 |
| Live Query Statistics | 実行中のクエリの進み具合を表示する | 長時間実行中のSQLがどこで詰まっているかを見るとき |
本番環境で実際の実行計画を取る場合は、対象SQLが実行される点に注意します。更新系SQLや負荷の高いSQLは、検証環境で再現するか、実行時間帯を選んで確認します。
また、実行計画を表示するには、対象オブジェクトへの実行権限に加えて、参照先データベースに対する SHOWPLAN 権限が必要になる場合があります。権限が足りない環境では、DBAや管理者に確認してから調査します。
SSMSで実行計画を表示する方法
SQL Server Management Studioでは、クエリウィンドウ上部のボタンやメニューから実行計画を表示できます。実務では、まず実際の実行計画を有効にしてSQLを実行し、結果タブの横に出る「実行プラン」タブを確認する流れが多いです。
| 操作 | 内容 | 注意点 |
|---|---|---|
| 推定実行計画の表示 | SQLを実行せずに計画を表示する | 実際の行数や実行時警告は見られない |
| 実際の実行計画を含める | SQL実行後に実行計画を表示する | 対象SQLは実際に実行される |
| 計画XMLを表示 | 実行計画をXMLで確認する | Missing Index や詳細属性を検索しやすい |
| 実際の実行計画の分析 | SSMSの分析機能で問題候補を見る | 出た指摘をそのまま採用せず、SQLとデータ分布を確認する |
T-SQLで実行計画と実行情報を確認する
SSMSのGUIだけでなく、T-SQLでも実行計画や実行時情報を確認できます。検証時は STATISTICS IO と STATISTICS TIME を一緒に使うと、改善前後の読み取り量と時間を比較しやすくなります。
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
SET STATISTICS XML ON;
SELECT
o.OrderID,
o.OrderDate,
c.CustomerName
FROM dbo.Orders AS o
JOIN dbo.Customers AS c
ON c.CustomerID = o.CustomerID
WHERE o.OrderDate >= '2026-01-01'
AND o.OrderDate < '2026-02-01';
SET STATISTICS XML OFF;
SET STATISTICS TIME OFF;
SET STATISTICS IO OFF;
SET SHOWPLAN_XML ON はSQLを実行せずに推定実行計画を返します。一方、SET STATISTICS XML ON はSQLを実行して、実際の実行情報を含むXMLを返します。用途を取り違えないようにします。
SET SHOWPLAN_XML ON; GO SELECT * FROM dbo.Orders WHERE CustomerID = 100; GO SET SHOWPLAN_XML OFF; GO
実行計画を詳しく見る順番
実行計画は情報量が多いので、最初から細かいプロパティを全部読むと迷いやすいです。まずは次の順番で見ると、原因候補を絞り込みやすくなります。
| 順番 | 見る場所 | 判断すること |
|---|---|---|
| 1 | 対象ステートメント | 複数SQLのうち、どのSQLが高コストか |
| 2 | 太い矢印 | 大量行が流れている箇所はどこか |
| 3 | 推定行数と実際の行数 | 見積もりが大きく外れていないか |
| 4 | Scan / Seek | 必要以上に広い範囲を読んでいないか |
| 5 | Key Lookup | 大量に繰り返されていないか |
| 6 | Sort / Hash Match / Spool | メモリ不足や tempdb 使用の原因になっていないか |
| 7 | 警告と Missing Index | 暗黙変換、Spill、インデックス候補が出ていないか |
実行計画のコストは、同じバッチ内の相対的な見積もりです。実際の経過時間やCPU時間そのものではありません。最終判断では、STATISTICS IO の logical reads、実行時間、実行回数、業務上の影響を合わせて見ます。
主要なオペレーターの見方
SQL Serverの実行計画には多くのオペレーターがありますが、遅いSQLの初期調査では次のものを優先して見れば十分です。
| オペレーター | 意味 | 確認すること |
|---|---|---|
Table Scan |
ヒープテーブル全体を読む | WHERE句で絞れるのに全件読みになっていないか |
Clustered Index Scan |
クラスタ化インデックス全体を読む | 対象行が多いなら妥当。少数行なら条件やインデックスを確認する |
Index Scan |
非クラスタ化インデックスを広く読む | Seekできない条件、取得列不足、並び替え目的かを見る |
Index Seek |
インデックスで範囲を絞って読む | Seek Predicate に検索条件が入っているかを見る |
Key Lookup |
非クラスタ化インデックスにない列をクラスタ化インデックスから取りに行く | 行数が多い場合は INCLUDE 列や取得列の見直しを検討する |
Nested Loops |
片方の結果を使ってもう片方を繰り返し探す | 外側の行数が多すぎないか、Lookupが大量発生していないか |
Hash Match |
ハッシュ表を作って結合・集計する | 入力行数、メモリ、Spill警告、結合条件を確認する |
Sort |
並び替えを行う | ORDER BYやGROUP BYに合うインデックスがないか、Spillしていないか |
Spool |
中間結果を一時保存する | 相関サブクエリ、更新処理、再利用のために重くなっていないか |
Table ScanやIndex Scanが悪いとは限らない
Table Scan や Index Scan が出たからといって、必ず悪い実行計画とは限りません。テーブルの大半を取得するSQLなら、インデックスで何度も探すよりスキャンのほうが効率的なことがあります。
問題になりやすいのは、「少数行を取りたいのに広く読んでいる」ケースです。たとえば、検索列に関数をかけている、型が合っていない、前方一致ではないLIKEを使っている、複合インデックスの先頭列を条件に使っていない、といった場合です。
-- 悪い例: OrderDate列に関数をかけると、インデックスを効率よく使いにくい SELECT OrderID, OrderDate, CustomerID FROM dbo.Orders WHERE CONVERT(date, OrderDate) = '2026-01-15'; -- 改善例: 範囲条件にする SELECT OrderID, OrderDate, CustomerID FROM dbo.Orders WHERE OrderDate >= '2026-01-15' AND OrderDate < '2026-01-16';
このように、列側を加工せずに検索できる形にすると、Index Seek になりやすくなります。日付条件の書き方は、日付のみで比較するSQLの記事も参考になります。
Index Seekでも安心しすぎない
Index Seek は良い兆候ですが、それだけで高速とは限りません。Seekしたあとに大量行を返している場合や、Key Lookup が大量に繰り返されている場合は、全体として重くなることがあります。
-- CustomerIDでSeekできても、取得列が多いとKey Lookupが大量発生することがある
SELECT
OrderID,
CustomerID,
OrderDate,
TotalAmount,
ShippingAddress
FROM dbo.Orders
WHERE CustomerID = 100;
このSQLで CustomerID の非クラスタ化インデックスだけがあり、TotalAmount や ShippingAddress が含まれていない場合、不足列を取りに行くために Key Lookup が発生します。対象行が少なければ問題ありませんが、数千回、数万回繰り返されるなら重くなります。
-- 例: よく使う取得列をINCLUDEしてLookupを減らす -- 実際には既存インデックス、更新負荷、重複を確認してから作成する CREATE INDEX IX_Orders_CustomerID_OrderDate ON dbo.Orders (CustomerID, OrderDate) INCLUDE (TotalAmount, ShippingAddress);
ただし、取得列を何でも INCLUDE すればよいわけではありません。インデックスは更新時の負荷と容量も増やします。実行回数が多いSQL、業務上重要なSQL、読み取り量が大きいSQLから優先して検討します。
Missing Indexの提案はそのまま作らない
実行計画に Missing Index が表示されることがあります。これはSQL Serverが「このようなインデックスがあるとよさそう」と判断した候補です。便利ですが、そのまま本番で作成するのは危険です。
| 確認点 | 理由 |
|---|---|
| 既存インデックスと重複していないか | 似たインデックスが増えると更新負荷と容量が増える |
| 列順が検索条件に合っているか | 等価条件、範囲条件、ORDER BYの順に効き方が変わる |
| INCLUDE列が多すぎないか | 読み取りは速くなるが、書き込みやメンテナンスが重くなる |
| そのSQLだけの都合ではないか | 他の重要SQLの計画を悪化させる可能性がある |
| 実測で改善するか | logical reads、CPU時間、実行時間で比較する |
Missing Index の一覧は DMV から確認できます。ただしSQL Server再起動などで情報が消えるため、恒久的な履歴ではなく、調査時点の候補として扱います。
SELECT TOP (30)
DB_NAME(mid.database_id) AS database_name,
OBJECT_NAME(mid.object_id, mid.database_id) AS table_name,
migs.user_seeks,
migs.user_scans,
migs.avg_total_user_cost,
migs.avg_user_impact,
mid.equality_columns,
mid.inequality_columns,
mid.included_columns
FROM sys.dm_db_missing_index_group_stats AS migs
JOIN sys.dm_db_missing_index_groups AS mig
ON mig.index_group_handle = migs.group_handle
JOIN sys.dm_db_missing_index_details AS mid
ON mid.index_handle = mig.index_handle
WHERE mid.database_id = DB_ID()
ORDER BY migs.avg_total_user_cost * migs.avg_user_impact * (migs.user_seeks + migs.user_scans) DESC;
推定行数と実際の行数のズレを見る
実行計画で特に重要なのが、推定行数と実際の行数のズレです。SQL Serverは統計情報を使って行数を見積もり、その見積もりを元に結合順序や結合方法を選びます。見積もりが大きく外れると、少数行向けの計画で大量行を処理したり、逆に大量行向けの計画で少数行を処理したりします。
| ズレの例 | 起きやすい原因 | 確認すること |
|---|---|---|
| 推定1行、実際10万行 | 統計情報が古い、条件の偏り、パラメータスニッフィング | 統計情報の更新、実際のデータ分布、パラメータ値 |
| 推定10万行、実際1行 | 選択性を低く見積もっている | 検索条件、複合統計、インデックス列順 |
| JOIN後の行数が大きく外れる | JOIN条件の相関、データ偏り、不要な重複 | JOINキーの一意性、重複データ、集計前後の行数 |
統計情報が古い場合は、統計情報の更新が改善につながることがあります。ただし、統計情報を更新すれば必ず速くなるわけではありません。更新後に実行計画が変わるため、重要SQLでは検証環境で確認してから反映します。
-- 対象テーブルの統計情報を更新する例 UPDATE STATISTICS dbo.Orders; -- サンプリングではなく全件で更新する例。大きいテーブルでは負荷に注意 UPDATE STATISTICS dbo.Orders WITH FULLSCAN;
SortやHash Matchの警告を見る
Sort や Hash Match は、並び替え、集計、結合でよく出ます。これら自体が悪いわけではありませんが、入力行数が多い、メモリ見積もりが外れている、統計情報が古い、適切なインデックスがない場合に重くなりやすいです。
実行計画に Spill 警告が出ている場合は、メモリに収まらず tempdb にこぼれた可能性があります。tempdb が肥大化する調査では、実行計画の Sort、Hash Match、Spool も確認します。
SQL Serverでtempdbが肥大化する原因と確認方法
| 警告・状態 | 疑うこと | 見直し候補 |
|---|---|---|
| SortのSpill | 並び替え対象が多い、メモリ不足、統計情報のズレ | ORDER BYに合うインデックス、取得行数削減、統計情報更新 |
| Hash MatchのSpill | ハッシュ結合や集計の入力が多い | JOIN条件、事前絞り込み、インデックス、統計情報 |
| Spoolが大きい | 中間結果の再利用や相関処理が重い | サブクエリ、更新SQL、JOINへの書き換え |
| 暗黙変換 | 列とパラメータの型が合っていない | 型を合わせる、列側変換を避ける |
ロック待ちやデッドロックと切り分ける
SQLが遅い原因は、実行計画だけとは限りません。実行計画は妥当でも、他のセッションのロック待ちで止まっていることがあります。実行中に遅い場合は、sys.dm_exec_requests の wait_type やblocking_session_id も確認します。
SELECT
r.session_id,
r.status,
r.command,
r.wait_type,
r.wait_time,
r.blocking_session_id,
r.cpu_time,
r.logical_reads,
t.text AS running_sql
FROM sys.dm_exec_requests AS r
OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) AS t
WHERE r.session_id <> @@SPID
ORDER BY r.cpu_time DESC;
ロック待ちが原因なら、実行計画の改善よりもブロッキング元の特定が先です。テーブルロックやブロッキングの確認は、次の記事で整理しています。
また、処理同士が互いにロックを取り合ってエラー1205になる場合はデッドロックです。その場合はデッドロックグラフを確認し、処理順序やトランザクション範囲を見直します。
キャッシュされた重いSQLから実行計画を探す
特定のSQLがわかっていない場合は、プランキャッシュから読み取り量やCPU時間が大きいSQLを探す方法もあります。キャッシュは再起動やメモリ圧迫で消えるため、恒久的な監査ログではありませんが、直近の重いSQLを探す入口として便利です。
SELECT TOP (30)
qs.execution_count,
qs.total_logical_reads,
qs.total_logical_reads / NULLIF(qs.execution_count, 0) AS avg_logical_reads,
qs.total_worker_time,
qs.total_elapsed_time,
SUBSTRING(
st.text,
(qs.statement_start_offset / 2) + 1,
((CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(st.text)
ELSE qs.statement_end_offset
END - qs.statement_start_offset) / 2) + 1
) AS statement_text,
qp.query_plan
FROM sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) AS qp
ORDER BY qs.total_logical_reads DESC;
ここで出てくる query_plan を開くと実行計画を確認できます。ただし、キャッシュされた計画は現在のパラメータやデータ分布に完全一致しないことがあります。重要なSQLは、実際のパラメータで再現して確認します。
実行計画から改善する流れ
実行計画を見たあと、いきなりインデックスを増やすのではなく、SQL、データ量、インデックス、統計情報、ロック状況を順番に確認します。
| 手順 | やること | 見る指標 |
|---|---|---|
| 1 | 遅いSQLを特定する | 実行時間、CPU、logical reads、実行回数 |
| 2 | 実際の実行計画を取る | 推定行数と実際の行数、警告、Scan/Seek |
| 3 | SQLの条件を見直す | 列側関数、暗黙変換、不要列、不要なORDER BY |
| 4 | 既存インデックスを見る | 検索条件、JOIN条件、ORDER BY、取得列との対応 |
| 5 | 統計情報を確認する | 行数見積もりのズレ、更新日時、データ偏り |
| 6 | 改善案を検証する | logical reads、CPU時間、経過時間、実行計画の変化 |
| 7 | 副作用を確認する | 更新負荷、容量、他SQLの計画、ロック時間 |
トランザクションログの肥大化や更新処理の遅延が絡む場合は、実行計画だけでなく、トランザクションの範囲、バッチサイズ、ログ使用量も見ます。
SQL Serverでトランザクションログがいっぱいになる原因と対処法
よくある失敗パターン
実行計画を使ったSQLチューニングでは、次の失敗がよくあります。どれも現場で起きやすいので、改善前後の実測を残しながら進めるのが大事です。
| 失敗 | なぜ危ないか | 代わりにやること |
|---|---|---|
| Missing Indexを全部作る | 重複インデックスが増え、更新負荷と容量が増える | 既存インデックスと統合し、重要SQLから検証する |
| コスト割合だけで判断する | 推定コストであり、実時間や読み取り量とはズレることがある | STATISTICS IO/TIMEと実行回数も見る |
| Index Seekなら問題なしと考える | Seek後の行数やKey Lookupが多いと重い | 実際の行数、Lookup回数、取得列を見る |
| Scanをすべて悪者にする | 大量取得ではScanが妥当なこともある | 取得割合とlogical readsで判断する |
| 検証せず本番にインデックスを追加する | 他SQLの計画や更新性能に影響する | 検証環境で比較し、ロールバック手順も用意する |
よくある質問
推定実行計画だけ見れば十分ですか?
実行前の確認には役立ちますが、遅いSQLの原因調査では不十分なことが多いです。実際の行数、実行時警告、Spillなどを見るため、可能なら実際の実行計画を取得します。
Table Scanが出たら必ずインデックスを作るべきですか?
必ずではありません。テーブルの大部分を読むSQLならScanが妥当なこともあります。少数行を取得する想定なのにScanになっている場合は、WHERE句、型、関数、インデックスを確認します。
Missing Indexの提案は信頼できますか?
候補としては有用ですが、そのまま作るものではありません。既存インデックスとの重複、列順、INCLUDE列、更新負荷、他SQLへの影響を確認します。
Key Lookupは悪いものですか?
少数回なら問題になりにくいです。大量行に対して繰り返されている場合は、取得列の削減やカバリングインデックスを検討します。
実行計画が毎回変わることはありますか?
あります。統計情報、パラメータ値、データ量、キャッシュ状態、SQL Serverの設定によって変わることがあります。特定パラメータでだけ遅い場合は、パラメータスニッフィングやデータ偏りも疑います。
実行計画とロック待ちはどう切り分けますか?
実行中の wait_type と blocking_session_id を確認します。ロック待ちなら、実行計画改善より先にブロッキング元とトランザクション範囲を確認します。
まとめ
SQL Serverの実行計画は、遅いSQLの原因を探すための地図です。まず実際の実行計画を取得し、推定行数と実際の行数のズレ、Table Scan、Index Scan、Index Seek、Key Lookup、Sort、Hash Match、警告や Missing Index を確認します。
ただし、実行計画だけで結論を出さないことが重要です。STATISTICS IO、STATISTICS TIME、実行回数、ロック待ち、tempdb やトランザクションログへの影響も合わせて見ます。SQLの書き方、既存インデックス、統計情報、取得列を順番に確認し、改善前後の読み取り量と時間を比較しながらチューニングしましょう。

