SQL Serverで「昨日から急にSQLが遅い」「実行計画が変わったか確認したい」「パラメータスニッフィングや統計情報更新のあとに性能が落ちたか見たい」とき、Query Storeが役に立ちます。Query Storeは、SQLの実行計画、実行回数、CPU時間、読み取り量、待機統計などをデータベース内に保存し、あとから性能劣化を追跡できる機能です。
この記事では、SQL ServerのQuery Storeの有効化、設定確認、遅いSQLの抽出、実行計画変更の確認、性能劣化したSQLの調査、待機統計、プラン強制、Query Storeヒント、運用時の注意点まで実務向けに整理します。
Query Storeは、遅いSQL調査の履歴装置です。まず
sys.database_query_store_options で有効状態とREAD_WRITE状態を確認し、sys.query_store_query、sys.query_store_plan、sys.query_store_runtime_stats を使って、CPU時間、実行時間、logical readsが大きいSQLを探します。実行計画が変わって遅くなった場合は、過去の良い計画を確認し、必要に応じてプラン強制やQuery Storeヒントを検討します。ただし、計画強制は応急処置です。統計情報、インデックス、SQLの書き方、ロック待ちも合わせて確認します。Query Storeとは
Query Storeは、クエリ、実行計画、実行時統計をデータベース内に保存するSQL Serverの機能です。Microsoft Learnでは、実行計画の選択や性能差を把握し、計画変更による性能劣化を素早く見つけるための機能として説明されています。SQL Server 2016以降で利用でき、SQL Server 2022では新規データベースで既定有効になっています。
| 保存されるもの | 確認できること |
|---|---|
| クエリ本文 | どのSQLが実行されたか |
| 実行計画 | 過去にどの計画が使われたか、計画が変わったか |
| 実行時統計 | 実行回数、平均時間、CPU、logical reads、writesなど |
| 待機統計 | CPU待ち、ロック待ち、IO待ちなどの傾向 |
| 時間帯別集計 | いつから遅くなったか、時間窓ごとの変化 |
実行計画の基本は次の記事で整理しています。Query Storeは、その実行計画を過去分も含めて追えるのが強みです。
Query Storeが向いている調査
Query Storeは、現在実行中の瞬間的な情報を見るだけでなく、過去から現在までの変化を見るときに便利です。特に、計画変更による性能劣化、アップデート後の回帰、パラメータ値による計画差の調査に向いています。
| 調査したいこと | Query Storeで見るもの |
|---|---|
| 急に遅くなったSQL | 直近の平均実行時間、CPU、logical readsの増加 |
| 実行計画が変わったか | 同じquery_idに複数plan_idがあるか |
| 過去の良い計画に戻したい | プラン強制の候補 |
| パラメータスニッフィングを疑う | 同じSQLで複数計画・性能差があるか |
| ロック待ちやIO待ちを見たい | Query Storeのwait stats |
| リリース前後を比較したい | 変更前後の時間帯別統計 |
パラメータスニッフィングの調査では、同じSQLに複数の実行計画があり、ある計画だけ極端に遅くなっていないかを見ると切り分けやすくなります。
Query Storeの状態を確認する
まず、対象データベースでQuery Storeが有効か、実際に書き込み可能な状態かを確認します。設定上はONでも、容量上限に達してREAD_ONLYになっていると新しい情報が集まりません。
SELECT
actual_state_desc,
desired_state_desc,
readonly_reason,
current_storage_size_mb,
max_storage_size_mb,
query_capture_mode_desc,
size_based_cleanup_mode_desc,
wait_stats_capture_mode_desc
FROM sys.database_query_store_options;
| 列 | 見ること |
|---|---|
actual_state_desc |
実際の状態。READ_WRITEなら収集中 |
desired_state_desc |
要求された状態。ONにしたつもりか |
readonly_reason |
READ_ONLYになった理由 |
current_storage_size_mb |
現在の使用量 |
max_storage_size_mb |
容量上限 |
query_capture_mode_desc |
ALL、AUTO、CUSTOMなどの収集モード |
wait_stats_capture_mode_desc |
待機統計を収集するか |
Query StoreがREAD_ONLYの場合は、容量上限、クリーンアップ設定、ディスク容量を確認します。READ_ONLYのままだと、性能劣化が起きた時点のデータが残らないことがあります。
| readonly_reason | 主な意味 | 対応の方向 |
|---|---|---|
65536 |
Query Storeが容量上限に達した | 上限拡張、古いデータ削除、クリーンアップ設定を確認する |
524288 |
データベースのディスク容量上限に達した | DB容量、不要データ、ストレージを確認する |
131072 |
内部メモリ上限に達した | 不要なクエリ削除、収集モード調整、環境リソースを確認する |
262144 |
ディスク永続化待ちのメモリ項目が多い | 一時的か確認し、flush間隔や負荷を確認する |
8 |
セカンダリレプリカなど読み取り専用側 | 可用性グループやレプリカ構成を確認する |
Query Store関連ビューを見るには権限も必要です。SQL Server 2022以降では VIEW DATABASE PERFORMANCE STATE、SQL Server 2016から2019では VIEW DATABASE STATE が必要になる場面があります。権限不足で結果が見えない場合は、DBAや管理者に確認します。
Query Storeを有効化する
Query Storeが無効な場合は、対象データベースで有効化します。SQL Server 2016/2017/2019では新規DBで既定無効の環境が多く、SQL Server 2022では新規DBで既定有効です。既存DBでは必ず状態を確認します。
ALTER DATABASE YourDatabaseName
SET QUERY_STORE = ON (
OPERATION_MODE = READ_WRITE,
QUERY_CAPTURE_MODE = AUTO,
MAX_STORAGE_SIZE_MB = 1024,
CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 30),
SIZE_BASED_CLEANUP_MODE = AUTO,
DATA_FLUSH_INTERVAL_SECONDS = 900,
INTERVAL_LENGTH_MINUTES = 15,
MAX_PLANS_PER_QUERY = 200,
WAIT_STATS_CAPTURE_MODE = ON
);
QUERY_CAPTURE_MODE = AUTO は、重要度の低いクエリを除外しながら、調査に使いやすい情報を集める設定です。SQL Server 2019以降ではAUTOが既定です。検証や短期調査ではALLを使うこともありますが、アドホックSQLが多い環境ではデータ量が増えやすくなります。
遅いSQLを探す
まずは、平均実行時間、CPU時間、logical readsが大きいSQLを探します。Query Storeの統計は時間帯ごとに集計されるため、直近だけでなく期間を決めて見るのが大事です。
SELECT TOP (30)
qsq.query_id,
qsp.plan_id,
SUM(rs.count_executions) AS executions,
AVG(rs.avg_duration) / 1000.0 AS avg_duration_ms,
AVG(rs.avg_cpu_time) / 1000.0 AS avg_cpu_ms,
AVG(rs.avg_logical_io_reads) AS avg_logical_reads,
MAX(rs.last_execution_time) AS last_execution_time,
LEFT(qt.query_sql_text, 4000) AS query_text
FROM sys.query_store_query_text AS qt
JOIN sys.query_store_query AS qsq
ON qsq.query_text_id = qt.query_text_id
JOIN sys.query_store_plan AS qsp
ON qsp.query_id = qsq.query_id
JOIN sys.query_store_runtime_stats AS rs
ON rs.plan_id = qsp.plan_id
GROUP BY qsq.query_id, qsp.plan_id, qt.query_sql_text
ORDER BY avg_duration_ms DESC;
avg_duration や avg_cpu_time はマイクロ秒単位です。ミリ秒で見たい場合は1000で割ります。また、Query Storeのruntime statsは同じ時間窓で複数行になることがあるため、集計して見るのが基本です。
直近で性能劣化したSQLを探す
「昨日から遅い」のような調査では、過去の期間と直近の期間を比較します。次の例は、直近1時間とその前の1時間をざっくり比較する入口です。実際には業務時間帯やバッチ時間帯に合わせて期間を調整します。
WITH stats AS (
SELECT
qsq.query_id,
qsp.plan_id,
qt.query_sql_text,
rsi.start_time,
rs.count_executions,
rs.avg_duration,
rs.avg_cpu_time,
rs.avg_logical_io_reads
FROM sys.query_store_query_text AS qt
JOIN sys.query_store_query AS qsq
ON qsq.query_text_id = qt.query_text_id
JOIN sys.query_store_plan AS qsp
ON qsp.query_id = qsq.query_id
JOIN sys.query_store_runtime_stats AS rs
ON rs.plan_id = qsp.plan_id
JOIN sys.query_store_runtime_stats_interval AS rsi
ON rsi.runtime_stats_interval_id = rs.runtime_stats_interval_id
WHERE rsi.start_time >= DATEADD(hour, -2, SYSDATETIMEOFFSET())
)
SELECT TOP (30)
query_id,
plan_id,
SUM(CASE WHEN start_time >= DATEADD(hour, -1, SYSDATETIMEOFFSET()) THEN count_executions ELSE 0 END) AS recent_execs,
AVG(CASE WHEN start_time >= DATEADD(hour, -1, SYSDATETIMEOFFSET()) THEN avg_duration END) / 1000.0 AS recent_avg_ms,
AVG(CASE WHEN start_time < DATEADD(hour, -1, SYSDATETIMEOFFSET()) THEN avg_duration END) / 1000.0 AS previous_avg_ms,
LEFT(query_sql_text, 1000) AS query_text
FROM stats
GROUP BY query_id, plan_id, query_sql_text
HAVING AVG(CASE WHEN start_time >= DATEADD(hour, -1, SYSDATETIMEOFFSET()) THEN avg_duration END)
> AVG(CASE WHEN start_time < DATEADD(hour, -1, SYSDATETIMEOFFSET()) THEN avg_duration END) * 2
ORDER BY recent_avg_ms DESC;
このSQLは調査の入口です。実行回数が少ないSQLは平均値がぶれやすいので、実行回数も合わせて見ます。
実行計画が変わったSQLを確認する
Query Storeでは、同じquery_idに複数のplan_idが紐づくことがあります。これは、同じSQLに対して複数の実行計画が使われたことを示します。パラメータスニッフィング、統計情報更新、インデックス変更、互換性レベル変更などで発生します。
SELECT TOP (50)
qsq.query_id,
COUNT(DISTINCT qsp.plan_id) AS plan_count,
SUM(rs.count_executions) AS executions,
AVG(rs.avg_duration) / 1000.0 AS avg_duration_ms,
LEFT(qt.query_sql_text, 1000) AS query_text
FROM sys.query_store_query_text AS qt
JOIN sys.query_store_query AS qsq
ON qsq.query_text_id = qt.query_text_id
JOIN sys.query_store_plan AS qsp
ON qsp.query_id = qsq.query_id
JOIN sys.query_store_runtime_stats AS rs
ON rs.plan_id = qsp.plan_id
GROUP BY qsq.query_id, qt.query_sql_text
HAVING COUNT(DISTINCT qsp.plan_id) > 1
ORDER BY plan_count DESC, avg_duration_ms DESC;
候補が見つかったら、plan_idごとの平均時間、CPU、読み取り量を比較します。良い計画と悪い計画がはっきり分かれるなら、プラン強制やQuery Storeヒントの候補になります。
DECLARE @query_id bigint = 123; -- 対象query_idに置き換える
SELECT
qsp.plan_id,
qsp.is_forced_plan,
qsp.force_failure_count,
qsp.last_force_failure_reason_desc,
SUM(rs.count_executions) AS executions,
AVG(rs.avg_duration) / 1000.0 AS avg_duration_ms,
AVG(rs.avg_cpu_time) / 1000.0 AS avg_cpu_ms,
AVG(rs.avg_logical_io_reads) AS avg_logical_reads,
MAX(rs.last_execution_time) AS last_execution_time,
qsp.query_plan
FROM sys.query_store_plan AS qsp
JOIN sys.query_store_runtime_stats AS rs
ON rs.plan_id = qsp.plan_id
WHERE qsp.query_id = @query_id
GROUP BY qsp.plan_id, qsp.is_forced_plan, qsp.force_failure_count,
qsp.last_force_failure_reason_desc, qsp.query_plan
ORDER BY avg_duration_ms DESC;
待機統計から原因を絞る
SQL Server 2017以降では、Query Storeでクエリ単位の待機統計も確認できます。これにより、遅いSQLがCPUで詰まっているのか、ロック待ちなのか、IO待ちなのかを切り分けやすくなります。
SELECT TOP (30)
qsq.query_id,
qsp.plan_id,
ws.wait_category_desc,
SUM(ws.total_query_wait_time_ms) AS total_wait_ms,
AVG(ws.avg_query_wait_time_ms) AS avg_wait_ms,
LEFT(qt.query_sql_text, 1000) AS query_text
FROM sys.query_store_wait_stats AS ws
JOIN sys.query_store_plan AS qsp
ON qsp.plan_id = ws.plan_id
JOIN sys.query_store_query AS qsq
ON qsq.query_id = qsp.query_id
JOIN sys.query_store_query_text AS qt
ON qt.query_text_id = qsq.query_text_id
GROUP BY qsq.query_id, qsp.plan_id, ws.wait_category_desc, qt.query_sql_text
ORDER BY total_wait_ms DESC;
ロック待ちが多い場合は、実行計画より先にブロッキング元を確認します。テーブルロックやデッドロックの調査は次の記事が参考になります。
プラン強制で一時的に戻す
Query Storeでは、過去に使われた良い実行計画を強制できます。急な性能劣化を止める応急処置として便利です。ただし、プラン強制は根本原因の解決ではありません。統計情報、インデックス、SQLの書き方を後で必ず確認します。
-- query_idとplan_idは実環境で確認した値を使う
EXEC sys.sp_query_store_force_plan
@query_id = 123,
@plan_id = 456;
-- 解除する場合
EXEC sys.sp_query_store_unforce_plan
@query_id = 123,
@plan_id = 456;
プラン強制が失敗することもあります。sys.query_store_plan の force_failure_count やlast_force_failure_reason_desc を確認し、オブジェクト変更や互換性の問題がないかを見ます。
| プラン強制が向いている場面 | 注意点 |
|---|---|
| 直近の計画変更で明らかに遅くなった | 良い計画が今のデータにも妥当か確認する |
| 緊急で性能劣化を止めたい | あとで根本原因を調査する |
| リリース後の回帰を一時的に戻したい | 恒久対策として放置しない |
| パラメータスニッフィングで悪い計画が選ばれた | PSP最適化やSQL修正も検討する |
統計情報更新によって良い計画が変わることもあります。統計情報の扱いは次の記事で整理しています。
Query Storeヒントを使う
SQL Server 2022以降やAzure SQL Databaseなどでは、Query Storeヒントを使えます。これは、アプリケーションコードを変更せずに、特定クエリへヒントを適用する仕組みです。たとえば、OPTION (RECOMPILE) や OPTIMIZE FOR UNKNOWN 相当の調整を、Query Store経由で行えます。
-- query_idはQuery Storeで確認した値を使う
EXEC sys.sp_query_store_set_hints
@query_id = 123,
@value = N'OPTION (OPTIMIZE FOR UNKNOWN)';
-- ヒントを解除する場合
EXEC sys.sp_query_store_clear_hints
@query_id = 123;
SELECT
query_hint_id,
query_id,
query_hint_text,
last_query_hint_failure_reason_desc,
query_hint_failure_count,
source_desc
FROM sys.query_store_query_hints
ORDER BY query_id;
Query Storeヒントは便利ですが、恒久的なSQL修正の代わりに放置するものではありません。期限、見直しタイミング、適用理由を残しておきます。
Query Storeの容量とREAD_ONLYに注意する
Query Storeはユーザーデータベース内にデータを保存します。容量上限に達するとREAD_ONLYになり、新しい情報を収集できなくなります。運用では、容量、保持期間、クリーンアップ、収集モードを定期的に確認します。
SELECT
actual_state_desc,
readonly_reason,
current_storage_size_mb,
max_storage_size_mb,
stale_query_threshold_days,
size_based_cleanup_mode_desc,
query_capture_mode_desc
FROM sys.database_query_store_options;
-- 容量上限を増やす例
ALTER DATABASE YourDatabaseName
SET QUERY_STORE (MAX_STORAGE_SIZE_MB = 2048);
-- 古いデータの保持期間を調整する例
ALTER DATABASE YourDatabaseName
SET QUERY_STORE (CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 30));
アドホックSQLが多い環境では、Query Storeのサイズが増えやすいです。QUERY_CAPTURE_MODE = AUTO やCUSTOMを検討し、調査に不要な細かいSQLを集めすぎないようにします。
tempdbやログの問題と切り分ける
Query Storeで遅いSQLを見つけても、原因が実行計画だけとは限りません。Sort や Hash Match が大きくなって tempdb にSpillしている場合、Query Store上では実行時間や読み取り量の増加として見えます。tempdb使用量の確認も合わせて行います。
SQL Serverでtempdbが肥大化する原因と確認方法
更新系SQLやバッチ処理では、トランザクションログの増加も性能に影響します。ログ肥大化やログバックアップの問題も切り分けます。
SQL Serverでトランザクションログがいっぱいになる原因と対処法
Query Storeで見つけた後にやること
Query Storeは問題SQLを見つける入口です。見つけたSQLは、実行計画、統計情報、インデックス、SQLの書き方、待機情報を順番に確認します。
| Query Storeでわかったこと | 次にやること |
|---|---|
| 実行時間が長い | 実行計画、logical reads、CPU時間を確認する |
| 計画が変わって遅くなった | 良い計画との差分、統計情報更新、プラン強制を検討する |
| CPU待ちが多い | 不要な計算、スキャン、関数、並列度を確認する |
| Lock待ちが多い | ブロッキング元とトランザクション範囲を確認する |
| IO待ちが多い | インデックス不足、読み取り量、ストレージ状況を確認する |
| tempdb系の待ちが多い | Sort、Hash Match、Spill、tempdb容量を確認する |
やってはいけない対応
Query Storeは強力ですが、誤った使い方をすると調査が難しくなったり、別の性能問題を作ったりします。
| NG対応 | なぜ危ないか | 代わりにやること |
|---|---|---|
| 容量上限を見ない | READ_ONLYになり、新しい性能劣化を記録できない | 定期的に状態とサイズを確認する |
| プラン強制を放置する | データ分布やスキーマ変更で強制計画が悪化する | 期限を決めて見直す |
| Query Storeだけで原因を決める | ロック待ちやtempdb問題を見落とす | 実行計画と待機情報も見る |
| 全部のSQLを長期間ALLで集める | 容量増加とノイズが増える | AUTOやCUSTOMを検討する |
| 本番でいきなりヒントを大量適用する | 別のSQLや将来のデータ分布に合わない | 対象SQLを絞って検証する |
よくある質問
Query Storeは本番で有効にしてよいですか?
多くの環境で有効化する価値があります。ただし、容量上限、収集モード、クリーンアップ、READ_ONLY状態を監視します。高負荷環境では、まずAUTOやCUSTOMで必要な情報に絞る運用が現実的です。
SQL Server 2022ではQuery Storeは必要ですか?
必要です。SQL Server 2022では新規DBでQuery Storeが既定有効になり、Parameter Sensitive Plan最適化などQuery Storeに依存する機能もあります。状態確認と運用設定は引き続き必要です。
Query Storeとプランキャッシュは何が違いますか?
プランキャッシュは主に現在使われる計画を保持しますが、メモリ圧迫などで消えます。Query Storeはデータベース内に履歴として保存するため、過去の計画や性能変化を追跡できます。
プラン強制すれば解決ですか?
一時的な性能劣化を止めるには有効ですが、根本解決とは限りません。統計情報、インデックス、SQL、データ分布を見直し、強制を解除できる状態を目指します。
Query StoreヒントとSQLヒント句はどちらを使うべきですか?
アプリケーションコードを変更できない場合はQuery Storeヒントが便利です。ただし、恒久的に必要な対策ならSQLやインデックス設計を見直すほうが自然な場合もあります。
Query Storeでロック待ちは見えますか?
SQL Server 2017以降では、Query Storeの待機統計でLock系の待ちを確認できます。ただし、リアルタイムのブロッキング元特定には sys.dm_exec_requests やsys.dm_tran_locks も使います。
まとめ
SQL ServerのQuery Storeは、遅いSQL、実行計画変更、性能劣化をあとから調査するための重要な機能です。まず sys.database_query_store_options で状態を確認し、sys.query_store_query、sys.query_store_plan、sys.query_store_runtime_stats、sys.query_store_wait_stats を使って、実行時間、CPU、logical reads、待機統計、計画変更を確認します。
性能劣化が計画変更によるものなら、過去の良い計画を確認し、必要に応じてプラン強制やQuery Storeヒントを検討します。ただし、Query Storeは原因を見つける入口です。実行計画、統計情報、パラメータスニッフィング、ロック待ち、tempdb、トランザクションログも合わせて確認し、応急処置と恒久対策を分けて進めましょう。

