【SQL Server】Query Storeの使い方|遅いSQL・実行計画の変更・性能劣化を調査

【SQL Server】Query Storeの使い方|遅いSQL・実行計画の変更・性能劣化を調査 SQL

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_querysys.query_store_plansys.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は、その実行計画を過去分も含めて追えるのが強みです。

SQL Serverの実行計画の見方

Query Storeが向いている調査

Query Storeは、現在実行中の瞬間的な情報を見るだけでなく、過去から現在までの変化を見るときに便利です。特に、計画変更による性能劣化、アップデート後の回帰、パラメータ値による計画差の調査に向いています。

調査したいこと Query Storeで見るもの
急に遅くなったSQL 直近の平均実行時間、CPU、logical readsの増加
実行計画が変わったか 同じquery_idに複数plan_idがあるか
過去の良い計画に戻したい プラン強制の候補
パラメータスニッフィングを疑う 同じSQLで複数計画・性能差があるか
ロック待ちやIO待ちを見たい Query Storeのwait stats
リリース前後を比較したい 変更前後の時間帯別統計

パラメータスニッフィングの調査では、同じSQLに複数の実行計画があり、ある計画だけ極端に遅くなっていないかを見ると切り分けやすくなります。

SQL Serverのパラメータスニッフィングとは

Query Storeの状態を確認する

まず、対象データベースでQuery Storeが有効か、実際に書き込み可能な状態かを確認します。設定上はONでも、容量上限に達してREAD_ONLYになっていると新しい情報が集まりません。

check-query-store-options.sql
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では必ず状態を確認します。

enable-query-store.sql
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の統計は時間帯ごとに集計されるため、直近だけでなく期間を決めて見るのが大事です。

query-store-top-duration.sql
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_durationavg_cpu_time はマイクロ秒単位です。ミリ秒で見たい場合は1000で割ります。また、Query Storeのruntime statsは同じ時間窓で複数行になることがあるため、集計して見るのが基本です。

直近で性能劣化したSQLを探す

「昨日から遅い」のような調査では、過去の期間と直近の期間を比較します。次の例は、直近1時間とその前の1時間をざっくり比較する入口です。実際には業務時間帯やバッチ時間帯に合わせて期間を調整します。

query-store-regression-check.sql
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に対して複数の実行計画が使われたことを示します。パラメータスニッフィング、統計情報更新、インデックス変更、互換性レベル変更などで発生します。

query-store-multiple-plans.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ヒントの候補になります。

query-store-plan-comparison.sql
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待ちなのかを切り分けやすくなります。

query-store-wait-stats.sql
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;

ロック待ちが多い場合は、実行計画より先にブロッキング元を確認します。テーブルロックやデッドロックの調査は次の記事が参考になります。

SQL Serverでテーブルロックを確認するSQL

SQL Serverのデッドロックの原因と確認方法

プラン強制で一時的に戻す

Query Storeでは、過去に使われた良い実行計画を強制できます。急な性能劣化を止める応急処置として便利です。ただし、プラン強制は根本原因の解決ではありません。統計情報、インデックス、SQLの書き方を後で必ず確認します。

force-query-store-plan.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_planforce_failure_countlast_force_failure_reason_desc を確認し、オブジェクト変更や互換性の問題がないかを見ます。

プラン強制が向いている場面 注意点
直近の計画変更で明らかに遅くなった 良い計画が今のデータにも妥当か確認する
緊急で性能劣化を止めたい あとで根本原因を調査する
リリース後の回帰を一時的に戻したい 恒久対策として放置しない
パラメータスニッフィングで悪い計画が選ばれた PSP最適化やSQL修正も検討する

統計情報更新によって良い計画が変わることもあります。統計情報の扱いは次の記事で整理しています。

SQL Serverで統計情報を更新する方法

Query Storeヒントを使う

SQL Server 2022以降やAzure SQL Databaseなどでは、Query Storeヒントを使えます。これは、アプリケーションコードを変更せずに、特定クエリへヒントを適用する仕組みです。たとえば、OPTION (RECOMPILE)OPTIMIZE FOR UNKNOWN 相当の調整を、Query Store経由で行えます。

query-store-hint-example.sql
-- 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;
check-query-store-hints.sql
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になり、新しい情報を収集できなくなります。運用では、容量、保持期間、クリーンアップ、収集モードを定期的に確認します。

query-store-maintenance.sql
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を見つけても、原因が実行計画だけとは限りません。SortHash 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_requestssys.dm_tran_locks も使います。

まとめ

SQL ServerのQuery Storeは、遅いSQL、実行計画変更、性能劣化をあとから調査するための重要な機能です。まず sys.database_query_store_options で状態を確認し、sys.query_store_querysys.query_store_plansys.query_store_runtime_statssys.query_store_wait_stats を使って、実行時間、CPU、logical reads、待機統計、計画変更を確認します。

性能劣化が計画変更によるものなら、過去の良い計画を確認し、必要に応じてプラン強制やQuery Storeヒントを検討します。ただし、Query Storeは原因を見つける入口です。実行計画、統計情報、パラメータスニッフィング、ロック待ち、tempdb、トランザクションログも合わせて確認し、応急処置と恒久対策を分けて進めましょう。