PART 03 — Query Store による取得とセクション定義
Query Store は SQL Server 2016 以降で利用できる機能で、 クエリのテキスト・実行プラン・実行統計履歴をデータベース内に自動保存します。 Oracle AWR の SQL Statistics セクションに最も近い機能であり、 期間指定でのクエリ性能比較・プランのリグレッション検知・プラン固定化が可能です。
「あのクエリ、先週まで速かったのに」という相談は現場でよく耳にします。Query Store を有効化しておくだけで一瞬で答えが出せる場面が多いと気づいてから、導入のハードルを下げたくてこの記事を書きました。
1. Query Store の概要とアーキテクチャ
図1: Query Storeのデータ収集・保存・分析フロー
Query Store はデータベースレベルで有効化し、以下の情報を自動収集します。
- クエリテキスト(query_store_query_text)
- コンパイル済み実行プラン(query_store_plan)
- 実行統計(query_store_runtime_stats)
- 統計集計期間(query_store_runtime_stats_interval)
- 待機統計 SQL Server 2017+(query_store_wait_stats)
Oracle AWR はインスタンス全体のスナップショットを定期取得するのに対し、 Query Store はクエリ単位で実行統計を連続的に記録します。 AWR の「SQL ordered by Elapsed Time」相当の情報を任意の期間で抽出でき、 プランの変遷もビューで追跡できる点が AWR を超える強みです。
Query Store は SQL Server 2016 (13.0) 以降で利用可能です。 待機統計の記録は SQL Server 2017 (14.0) 以降が必要です。 Azure SQL Database では全バージョンで有効化されています。
2. 有効化と設定
Query Store の有効化
-- データベース単位で有効化
ALTER DATABASE [YourDatabase]
SET QUERY_STORE = ON (
OPERATION_MODE = READ_WRITE, -- READ_WRITE: 収集有効 / READ_ONLY: 収集停止・参照のみ
CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 30), -- データ保持日数
DATA_FLUSH_INTERVAL_SECONDS = 900, -- メモリ→ストレージへのフラッシュ間隔(秒)
INTERVAL_LENGTH_MINUTES = 60, -- 統計集計インターバル(分)
MAX_STORAGE_SIZE_MB = 1000, -- Query Store が使用する最大ストレージ (MB)
QUERY_CAPTURE_MODE = AUTO, -- AUTO: 重要クエリのみ / ALL: 全クエリ
SIZE_BASED_CLEANUP_MODE = AUTO, -- ストレージ上限到達時の自動クリーンアップ
MAX_PLANS_PER_QUERY = 200, -- クエリ1件に保持するプランの最大数
WAIT_STATS_CAPTURE_MODE = ON -- 待機統計収集 (SQL Server 2017+)
);
GO
-- 有効化の確認
SELECT name, is_query_store_on
FROM sys.databases
WHERE name = 'YourDatabase';
設定のチューニングポイント
| 設定項目 | 推奨値(OLTP) | 推奨値(DWH) | 説明 |
|---|---|---|---|
| INTERVAL_LENGTH_MINUTES | 60 | 60 ~ 120 | 統計集計の粒度。細かいほど詳細だがストレージ消費増 |
| MAX_STORAGE_SIZE_MB | 500 ~ 2048 | 2048 ~ 10240 | 少なすぎると古いデータが早期削除される |
| QUERY_CAPTURE_MODE | AUTO | AUTO | ALL は全クエリ記録のためストレージ消費が大きい |
| STALE_QUERY_THRESHOLD_DAYS | 30 | 60 ~ 90 | 長期傾向分析が必要な場合は延ばす |
3. 主要カタログビューとセクション定義
query_store_runtime_stats の主要列定義
| 列名 | 説明 | Oracle AWR 対応 |
|---|---|---|
| execution_type_desc | 実行種別(Regular, Aborted, Exception) | — |
| count_executions | インターバル内の実行回数 | Executions |
| avg_duration | 平均経過時間(マイクロ秒) | Avg Elapsed Time |
| total_duration | インターバル内の合計経過時間(マイクロ秒) | Total Elapsed Time |
| avg_cpu_time | 平均 CPU 時間(マイクロ秒) | Avg CPU Time |
| avg_logical_io_reads | 平均論理読み取り数(8KB ページ単位) | Avg Buffer Gets |
| avg_physical_io_reads | 平均物理読み取り数 | Avg Disk Reads |
| avg_rowcount | 平均処理行数 | Rows Processed |
| avg_memory_grant_kb | 平均メモリ許可量(KB) | — |
| last_execution_time | 最後の実行時刻 | — |
4. 期間指定 SQL 統計取得クエリ集
Top SQL by 合計経過時間(特定期間)
-- 特定期間のTop SQL by 合計経過時間
-- @start_time / @end_time で分析期間を指定
DECLARE @start_time DATETIME2 = '2026-05-29 09:00:00';
DECLARE @end_time DATETIME2 = '2026-05-29 12:00:00';
SELECT TOP 20
qt.query_sql_text,
q.query_id,
p.plan_id,
rs.count_executions,
rs.avg_duration / 1000.0 AS avg_duration_ms,
rs.total_duration / 1000.0 AS total_duration_ms,
rs.avg_cpu_time / 1000.0 AS avg_cpu_ms,
rs.avg_logical_io_reads AS avg_logical_reads,
rs.avg_physical_io_reads AS avg_physical_reads,
rs.avg_rowcount,
rs.avg_memory_grant_kb
FROM sys.query_store_runtime_stats rs
JOIN sys.query_store_runtime_stats_interval rsi
ON rs.runtime_stats_interval_id = rsi.runtime_stats_interval_id
JOIN sys.query_store_plan p
ON rs.plan_id = p.plan_id
JOIN sys.query_store_query q
ON p.query_id = q.query_id
JOIN sys.query_store_query_text qt
ON q.query_text_id = qt.query_text_id
WHERE rsi.start_time >= @start_time
AND rsi.end_time <= @end_time
AND rs.execution_type = 0 -- Regular execution のみ
ORDER BY rs.total_duration DESC;
プランのリグレッション検知(今日 vs 昨日)
-- プランのリグレッション検知:昨日と今日で同一クエリの性能を比較
WITH today AS (
SELECT
q.query_id,
SUM(rs.count_executions) AS exec_today,
AVG(rs.avg_duration) AS avg_dur_today
FROM sys.query_store_runtime_stats rs
JOIN sys.query_store_runtime_stats_interval rsi
ON rs.runtime_stats_interval_id = rsi.runtime_stats_interval_id
JOIN sys.query_store_plan p ON rs.plan_id = p.plan_id
JOIN sys.query_store_query q ON p.query_id = q.query_id
WHERE rsi.start_time >= DATEADD(HOUR, -24, GETUTCDATE())
GROUP BY q.query_id
),
yesterday AS (
SELECT
q.query_id,
SUM(rs.count_executions) AS exec_yesterday,
AVG(rs.avg_duration) AS avg_dur_yesterday
FROM sys.query_store_runtime_stats rs
JOIN sys.query_store_runtime_stats_interval rsi
ON rs.runtime_stats_interval_id = rsi.runtime_stats_interval_id
JOIN sys.query_store_plan p ON rs.plan_id = p.plan_id
JOIN sys.query_store_query q ON p.query_id = q.query_id
WHERE rsi.start_time >= DATEADD(HOUR, -48, GETUTCDATE())
AND rsi.start_time < DATEADD(HOUR, -24, GETUTCDATE())
GROUP BY q.query_id
)
SELECT
t.query_id,
qt.query_sql_text,
y.avg_dur_yesterday / 1000.0 AS avg_dur_yesterday_ms,
t.avg_dur_today / 1000.0 AS avg_dur_today_ms,
CAST(
(t.avg_dur_today - y.avg_dur_yesterday) * 100.0
/ NULLIF(y.avg_dur_yesterday, 0) AS DECIMAL(6,1)
) AS pct_change
FROM today t
JOIN yesterday y ON t.query_id = y.query_id
JOIN sys.query_store_query q ON t.query_id = q.query_id
JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id
WHERE t.avg_dur_today > y.avg_dur_yesterday * 1.5 -- 50% 以上悪化したもの
ORDER BY pct_change DESC;
5. Query Store 待機統計(SQL Server 2017+)
query_store_wait_stats ビューにより、クエリ単位の待機統計が取得できます。
「このクエリは何の待機で時間を使っているか」が一目でわかります。
| 列名 | 説明 |
|---|---|
| wait_category_desc | 待機カテゴリ(CPU, Lock, Latch, Network IO, User IO, Log IO など) |
| total_query_wait_time_ms | 合計待機時間(ミリ秒) |
| avg_query_wait_time_ms | 平均待機時間(ミリ秒) |
-- クエリごとの待機カテゴリ内訳
SELECT TOP 20
qt.query_sql_text,
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
FROM sys.query_store_wait_stats ws
JOIN sys.query_store_plan p ON ws.plan_id = p.plan_id
JOIN sys.query_store_query q ON p.query_id = q.query_id
JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id
JOIN sys.query_store_runtime_stats_interval rsi
ON ws.runtime_stats_interval_id = rsi.runtime_stats_interval_id
WHERE rsi.start_time >= DATEADD(HOUR, -24, GETUTCDATE())
GROUP BY qt.query_sql_text, ws.wait_category_desc
ORDER BY total_wait_ms DESC;
6. SSMS の Query Store GUI 操作
SSMS のオブジェクトエクスプローラーで対象データベースを展開すると、 「クエリストア」フォルダが表示されます。以下のレポートが利用可能です。
| レポート名 | 内容 | Oracle AWR 対応 |
|---|---|---|
| 最もリソースを消費するクエリ | 期間・指標(CPU/経過時間/論理読み取り/実行回数等)でフィルタした Top SQL。プランをグラフで並べて比較できる。 | SQL Statistics |
| クエリウォーター フォール | 時系列での実行統計の推移をバブルチャートで表示。性能劣化の時点を視覚的に特定できる。 | — |
| プランの変更クエリ | プランが変わったクエリを列挙。変更前後の実行時間を比較し、リグレッションを検知。 | — |
| 強制されたクエリ | プランを強制固定しているクエリの一覧。 | — |
| 全クエリのクエリ | Query Store に記録された全クエリの一覧。フリーテキスト検索も可能。 | — |
7. プラン固定化(Plan Forcing)
プランのリグレッションが確認された場合、既知の良好なプランを固定できます。 Oracle の SQL Plan Management(SPM)のベースライン固定に相当します。
-- プランを固定する(plan_id は query_store_plan ビューで確認)
EXEC sys.sp_query_store_force_plan
@query_id = 42,
@plan_id = 101;
-- 固定を解除する
EXEC sys.sp_query_store_unforce_plan
@query_id = 42,
@plan_id = 101;
-- 現在強制されているプランの確認
SELECT
q.query_id, qt.query_sql_text,
p.plan_id, p.is_forced_plan,
p.force_failure_count,
p.last_force_failure_reason_desc
FROM sys.query_store_plan p
JOIN sys.query_store_query q ON p.query_id = q.query_id
JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id
WHERE p.is_forced_plan = 1;
ALTER DATABASE [YourDB] SET AUTOMATIC_TUNING (FORCE_LAST_GOOD_PLAN = ON) を設定すると、
SQL Server がプランのリグレッションを検知して自動的に最良プランを固定します。
Oracle の SQL Plan Management の自動発展に相当する機能です。
Query Store活用でよくある誤り
Query Storeは自動でクエリ統計を蓄積する強力な機能ですが、設定や読み方を誤ると分析精度が下がります。
| 誤り/失敗パターン | 何が起きるか | 正しい対処/防止策 |
|---|---|---|
| Query Storeの容量上限に達したまま放置する | READ_ONLYモードに切り替わりデータ収集が止まり、問題発生時のデータが残らない | sys.database_query_store_optionsでcurrent_storage_size_mbを定期監視し、上限に近づいたらsp_query_store_flush_dbで整理する |
| プランのForce(強制)を検証なしに本番適用する | 統計情報の変化やデータ増加に追随できず、強制プランが逆効果になる場合がある | テスト環境でForceプランの効果を確認し、本番適用後もAvg Duration等の指標を継続的にモニタリングする |
| Regressed QueriesビューのAvg Durationだけで劣化判断する | データ量増加や統計変化による正当な実行計画変更を誤って「リグレッション」と判断してしまう | Execution Countの変化も確認し、実行回数増加による合計時間増加と実際の速度低下を区別する |