Azure SQL DatabaseでDTU100%を招く謎のクエリの発生元を特定する方法|クエリ ストアとExtended Eventsで追跡

Azure SQL Databaseで3時間ごとに同じSQLが走り、DTUが100%に跳ね上がる現象は「誰が投げたSQLか」を掴めないと終わりません。クエリ ストアで内容を確認したうえで、Extended Eventsでclient_app_name/hostname/userを記録し、実行元アプリやジョブを特定する具体的な手順をまとめます。

目次

なぜ「クエリの内容」は分かっても「実行元」は分からないのか

Azure SQL Databaseでは、パフォーマンス問題が起きると多くの人が最初にクエリ ストア(Query Store)やQuery Performance Insightを開きます。ここで「どのクエリが重いか」「いつ実行されたか」「CPUやI/Oをどれだけ使ったか」はかなりの精度で追えます。

一方で、これらの画面やビューが主に扱うのは“SQL文と実行統計”です。どのアプリケーション(アプリ名)やどのホスト(接続元)から飛んできたか、つまり発生元までを常時保存してくれる仕組みではありません。発生元特定には、実行時点の接続情報をログとして残す必要があります。

手段分かること分かりにくい/分からないことおすすめ用途
クエリ ストアクエリ テキスト、実行回数、CPU/論理読み取り、待機、プラン変化実行したアプリ名、ホスト名、接続元IP「何が重いか」を確定する
Query Performance Insight期間別の上位クエリ、CPU/DTU寄与、傾向発生元(client_app_name 等)「いつから増えたか」「傾向」を掴む
DMV(sys.dm_exec_*)いま動いているセッションのアプリ名・ホスト名・SQL過去に終わったクエリの発生元スパイクの瞬間を捕まえられるとき
Extended Events実行時のアプリ名/ホスト名/ユーザー/SQLをイベントとして記録フィルタ設計を誤るとイベント量が増える「誰が投げているか」を確実に突き止める
監査(Auditing)/ログ基盤認証・操作ログ、場合によってはSQLテキストや接続情報要件・コスト・保持設計が必要セキュリティ要件や長期追跡が必要な場合

まず整理しておきたい前提:3時間おきのスパイクは「定期処理」の可能性が高い

「3時間ごとに同じクエリが走る」という周期性は、たとえば次のような実装でよく起きます。

  • バッチジョブ/スケジューラ(Windows タスク、Cron、Kubernetes CronJob、Azure WebJob/Functions のタイマー)
  • データ連携(ETL、データ同期、ログ集計、エクスポート)
  • 監視・ヘルスチェックの誤設定(頻度が高すぎる、取得範囲が広い)
  • キャッシュの期限切れ → 再構築が重い(3時間でキャッシュ破棄など)

この段階で「アプリ側の心当たり」を探すのも有効ですが、推測だけだと時間が溶けます。この記事では、SQL側で発生元を“証拠付きで”特定して、アプリ担当者とも合意形成できる状態まで持っていきます。

クエリ ストアでやるべき確認

質問の状況では、クエリ ストア上で query_id = 10126 として表示され、Portal でもSQLテキストが確認できているとのことです。ここでは「発生元特定の前準備」として、クエリの特徴を整理します。

query_id からクエリ テキストを取得する

すでに実施済みでも、手順として残しておきます。後でフィルタ文字列を作るときに役立ちます。

SELECT qt.query_sql_text
FROM   sys.query_store_query q
JOIN   sys.query_store_query_text qt
           ON q.query_text_id = qt.query_text_id
WHERE  q.query_id = 10126;

自動生成っぽい SELECT [列名] AS [_列名] 形式で sdk_token など特徴的な列を読んでいるなら、Extended Eventsのフィルタ条件に使える“識別子”になりやすいです。

「いつ」「どれくらい」重いのかを数値で固める

発生元特定の会話では「本当にそのクエリがDTUを使っているのか?」が争点になることがあります。クエリ ストアの実行統計をSQLで引けるようにしておくと、説明が一気にラクになります。

-- 直近の実行傾向をざっくり見る例(期間は適宜調整)
SELECT TOP (50)
       q.query_id,
       p.plan_id,
       rs.last_execution_time,
       rs.count_executions,
       rs.avg_cpu_time,
       rs.avg_logical_io_reads,
       rs.avg_duration,
       rs.max_cpu_time,
       rs.max_logical_io_reads,
       rs.max_duration
FROM sys.query_store_query q
JOIN sys.query_store_plan p
  ON q.query_id = p.query_id
JOIN sys.query_store_runtime_stats rs
  ON p.plan_id = rs.plan_id
WHERE q.query_id = 10126
ORDER BY rs.last_execution_time DESC;

ポイントは、last_execution_time が3時間間隔で並ぶか、max_cpu_time / max_logical_io_reads / max_duration が突出しているかです。これで「周期性」と「負荷の大きさ」を客観的に示せます。

より堅牢に追跡したいなら query_hash も控える

Query Storeには、クエリ テキストの正規化に基づいたquery_hash(同一形のクエリを同一視するためのハッシュ)が保持されています。環境によっては、テキストの微妙な差分(空白やコメントなど)でLIKEフィルタが揺れることがあるため、可能なら「query_hashを控える→ログ側でもハッシュを取る」運用が安定します。

SELECT query_id, query_hash
FROM sys.query_store_query
WHERE query_id = 10126;

ただし、Extended Events側でハッシュを直接フィルタできるか、取得できるアクションが何かは環境・イベント種別で差が出ることがあります。まずはLIKEで確実に捕まえ、必要になった段階でハッシュ運用へ寄せるのが安全です。

DTU 100%の“内訳”を見て、調査の優先順位を決める

DTUは「CPU」「データI/O」「ログI/O」など複数リソースの合成指標です。DTUが100%に張り付いているとき、まずどれがボトルネックになっているかを把握しておくと、発生元特定後の対策(インデックスなのか、書き込み削減なのか、スケールなのか)がブレません。

Azure Portalで見やすい指標高いときに疑うこと典型的な対策の方向性
CPU(CPU Percentage)大きな集計/結合、ソート/ハッシュ、非効率なプラン、過度な並列実行クエリ/インデックス見直し、統計、プラン安定化、処理分割
Data IO(Data IO Percentage)フルスキャン、読み取り量が多い、SELECT列が多すぎる絞り込み条件の追加、カバリング/適切なインデックス、列削減、差分化
Log IO(Log IO Percentage)大量更新/挿入、1回のトランザクションが大きい、ログ書き込み集中バッチ分割、トランザクション縮小、更新範囲削減、スケジュール調整
Sessions / Workers接続数が多い、同時実行が多い、コネクションプール枯渇接続管理の見直し、同時実行制御、プール設定、アプリ側スロットリング

この“内訳”は、発生元特定の前でも確認できます。たとえば「Data IOが100%に近い」なら、今回のような自動生成SELECTが広範囲スキャンになっている可能性が高く、アプリ側で取得列や絞り込みを見直す価値が大きい、といった判断ができます。

最短で当たりを付ける方法:スパイク中にDMVで“現行セッション”を覗く

DTUが100%になっている瞬間に接続できるなら、まずはDMV(動的管理ビュー)で「今動いているSQL」と「接続元」を取れる場合があります。これは設定不要で、成功すれば最速です。

ただし、スパイクが短時間で終わる/接続できない時間帯に起きる場合は取り逃がしやすいので、次章のExtended Eventsが本命になります。

-- スパイク中に実行して「いま重いもの」を掴む(Azure SQL Databaseでもよく使う形)
SELECT TOP (50)
       r.session_id,
       s.login_name,
       s.host_name,
       s.program_name,
       r.status,
       r.cpu_time,
       r.total_elapsed_time,
       r.reads,
       r.writes,
       r.logical_reads,
       DB_NAME(r.database_id) AS database_name,
       SUBSTRING(t.text, (r.statement_start_offset/2)+1,
                       ((CASE r.statement_end_offset
                           WHEN -1 THEN DATALENGTH(t.text)
                           ELSE r.statement_end_offset END
                         - r.statement_start_offset)/2)+1) AS running_statement
FROM sys.dm_exec_requests r
JOIN sys.dm_exec_sessions s
  ON r.session_id = s.session_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.database_id = DB_ID()
ORDER BY r.cpu_time DESC;

この結果の program_name(≒アプリ名)や host_name(≒ホスト名)が埋まっていれば、その時点で発生元の候補はかなり絞れます。逆に、ここで捕まらない/情報が欠ける場合に備えて、次のExtended Eventsで“待ち伏せ”します。

本命:Extended Eventsで“その瞬間の接続情報”を捕まえる

Extended Events(拡張イベント)は、SQLの実行をイベントとして記録できる仕組みです。クエリの実行イベントに対して「どのアプリ名で接続してきたか」「どのホスト名か」「どのユーザーか」などをACTIONとして付与し、問題のクエリだけをフィルタして捕まえます。

発生元特定で集めたい情報

最低限、次の項目が取れると“アプリを指名”しやすくなります。

項目Extended Eventsの例分かること活用のコツ
アプリケーション名sqlserver.client_app_nameどのクライアント/ライブラリ/アプリ名で接続しているか接続文字列のApplication Nameを設定していれば、ほぼ一発で特定できます
ホスト名sqlserver.client_hostname接続元サーバー名(VM名など)PaaS経由だと抽象名になることがあります。その場合は別情報と組み合わせます
ユーザー名sqlserver.usernameどのログイン/ユーザーで実行されたか“共有アカウント”だと特定が難しいので、アプリごとにユーザーを分ける設計が有利です
SQLテキストsqlserver.sql_text / statement / batch_text実際に実行されたSQL「本当にあのクエリか」を突き合わせる証拠になります
接続IDsqlserver.client_connection_id同一接続を追跡するキーDMVと突合してIPなどを追う“つなぎ”に使えます

フィルタを決めるコツ:LIKEだけに頼らない

Extended Eventsで一番大事なのはフィルタ設計です。フィルタが広すぎるとイベント量が増え、逆に狭すぎると取り逃がします。まずは次の優先度で「クエリ固有の特徴」を探してください。

  1. 特徴的な列名(例:sdk_token のように他に出にくいもの)
  2. 特徴的なテーブル名(テレメトリ系、キュー系など)
  3. SQLコメント(アプリ側で埋め込めるなら最強。例:/* service:xxx job:yyy */)
  4. query_hash(クエリ ストアから取得できる場合は、文字列変化に強く運用しやすい)

まずは運用しやすいLIKEフィルタで捕まえ、見つかったらより厳密な条件(コメントやハッシュ)へ寄せていくのが現実的です。

そのまま使える拡張イベント セッション例(sdk_tokenを含むSQLだけ監視)

以下は「sdk_token を含むクエリだけ」を対象に、RPC(sp_executesqlなど)とバッチの両方を捕まえる構成例です。セッション名やフィルタ文字列は、実環境の“特徴語”に置き換えてください。

CREATE EVENT SESSION [capture_sdk_token] ON DATABASE
ADD EVENT sqlserver.rpc_completed(
    SET collect_statement = (1)
    ACTION (
        sqlserver.client_app_name,
        sqlserver.client_connection_id,
        sqlserver.client_hostname,
        sqlserver.sql_text,
        sqlserver.username
    )
    WHERE (
        [sqlserver].[like_i_sql_unicode_string]([statement], N'%sdk_token%')
    )
),
ADD EVENT sqlserver.sql_batch_completed(
    SET collect_batch_text = (1)
    ACTION (
        sqlserver.client_app_name,
        sqlserver.client_connection_id,
        sqlserver.client_hostname,
        sqlserver.sql_text,
        sqlserver.username
    )
    WHERE (
        [sqlserver].[like_i_sql_unicode_string]([batch_text], N'%sdk_token%')
    )
)
ADD TARGET package0.ring_buffer
WITH (
    MAX_MEMORY = 4096 KB,
    EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS,
    MAX_DISPATCH_LATENCY = 30 SECONDS,
    MEMORY_PARTITION_MODE = NONE,
    TRACK_CAUSALITY = OFF,
    STARTUP_STATE = OFF
);
GO

ALTER EVENT SESSION [capture_sdk_token]
ON DATABASE
STATE = START;
GO
  • rpc_completed:アプリがsp_executesql等で発行する“RPC系”を捕まえます(ORMやドライバ経由で多い)
  • sql_batch_completed:SSMS等からのバッチ実行や、まとめて投げられるSQLを捕まえます
  • ring_buffer:DB内メモリに保持するターゲットです。短期の切り分けに向きます(長期保持には不向き)

イベントは「3時間おきに1回だけ」という前提なら、ring_bufferでも十分なことが多いです。逆に、同じ特徴語を含むクエリが大量に流れる環境では、フィルタをさらに絞るか、短時間だけ開始して捕まえたらすぐ止める運用にしてください。

ライブ データで確認する手順(SSMS/Azure Data Studio)

  1. 上記SQLでセッションを作成し、STATE = STARTで開始します。
  2. SSMSまたはAzure Data Studioでデータベースに接続し、Extended Events(拡張イベント)のセッション一覧からcapture_sdk_tokenを開きます。
  3. 「ライブ データの監視」を開き、DTUが跳ねるタイミングまで待ちます。
  4. イベントが記録されたら、client_app_name / client_hostname / username を確認し、実行元の候補を絞ります。

「ライブ データの監視」は分かりやすい一方、画面を開きっぱなしにしづらいこともあります。次は、SQLだけでring_bufferから取り出す方法です。

ring_bufferをSQLで取り出す(画面を開きっぱなしにできない場合)

ring_bufferターゲットに溜まったイベントは、DMVからXMLとして取得できます。まずはXMLを取り出します。

SELECT CAST(t.target_data AS XML) AS target_data
FROM sys.dm_xe_database_session_targets t
JOIN sys.dm_xe_database_sessions s
  ON s.address = t.event_session_address
WHERE s.name = N'capture_sdk_token'
  AND t.target_name = N'ring_buffer';

次に、XMLをパースして見たい項目を表形式で抜き出します(必要な列だけ残してください)。

;WITH x AS (
  SELECT CAST(t.target_data AS XML) AS xdata
  FROM sys.dm_xe_database_session_targets t
  JOIN sys.dm_xe_database_sessions s
    ON s.address = t.event_session_address
  WHERE s.name = N'capture_sdk_token'
    AND t.target_name = N'ring_buffer'
)
SELECT
  n.value('@timestamp','datetime2') AS [timestamp],
  n.value('(action[@name="client_app_name"]/value/text())[1]','nvarchar(256)') AS client_app_name,
  n.value('(action[@name="client_hostname"]/value/text())[1]','nvarchar(256)') AS client_hostname,
  n.value('(action[@name="username"]/value/text())[1]','nvarchar(256)') AS username,
  n.value('(action[@name="client_connection_id"]/value/text())[1]','nvarchar(100)') AS client_connection_id,
  n.value('(data[@name="duration"]/value/text())[1]','bigint')/1000 AS duration_ms,
  n.value('(data[@name="cpu_time"]/value/text())[1]','bigint') AS cpu_time,
  n.value('(action[@name="sql_text"]/value/text())[1]','nvarchar(max)') AS sql_text
FROM x.xdata.nodes('//event') AS e(n)
ORDER BY [timestamp] DESC;

イベントが取れたら、次の章の“読み解き”に進みます。逆にイベントが一切出ない場合は、フィルタ文字列が一致していないか、スパイクの原因クエリが別のイベント種別(ストアド実行など)で発生している可能性があります。その場合は、フィルタ語を見直すか、sqlserver.sp_statement_completed等の別イベント追加を検討します(ただしイベント量が増えるので注意)。

client_app_name / client_hostname の読み解き方

取得できた値を“人間が分かるアプリ名”に翻訳するのが最後の壁です。とくに client_app_name は、接続文字列の設定や利用ライブラリによって粒度が変わります。

取得値の例(client_app_name)よくある意味次に確認するポイント
.Net SqlClient Data Provider / Microsoft.Data.SqlClientADO.NET系ドライバの既定名接続文字列にApplication Name=が未設定の可能性。アプリごとに設定すると一気に特定しやすくなります
Microsoft Entity Framework / EntityFrameworkCoreORM経由(EF/EF Core)で発行どのサービスがEFを使っているか、バッチかWebか、対象テーブルを使う機能は何か
JDBC Driver / Microsoft JDBC Driver for SQL ServerJava系クライアントバッチ基盤、ETL、Springアプリ、接続先の設定ファイル
ODBC Driver XX for SQL ServerODBC経由BI/ETLツール、レガシーアプリ、SSISなどの可能性
自社サービス名(例:MyBatchJob, OrdersApi)Application Nameが設定済みほぼ特定完了。ジョブ定義やリリース履歴と突き合わせる

client_hostname はVM名がそのまま取れると強力ですが、App ServiceやFunctionsなどPaaS経由では抽象的な名前になることがあります。その場合は、username(どのDBユーザーか)や、取得できるなら接続ID/セッションIDを手掛かりに“どの経路か”を詰めます。

Extended Eventsで掴めないときの補助線

現場では「client_app_nameが全部同じ」「ホスト名が空」「ユーザーが共有アカウント」で詰まることがあります。そんなときは、SQL側だけで完結させようとせず、次の“補助線”を引くと突破できます。

接続文字列に Application Name を付ける(最もコスパが良い)

多くのドライバは、接続文字列でアプリ名を指定できます。たとえば .NET 系なら次のように付けるだけで、Extended Events/DMVの client_app_name(または program_name)が見違えるほど分かりやすくなります。

Server=tcp:xxx.database.windows.net,1433;
Database=xxx;
User ID=xxx;
Password=xxx;
Application Name=OrdersApi-Prod;

「どのアプリか分からない」を根本から減らせるので、複数アプリが同一DBを共有している環境では特におすすめです。

SESSION_CONTEXT(またはCONTEXT_INFO)で“実行元タグ”を入れる

アプリがDBに接続した直後に、セッションにタグを持たせる方法です。アプリ改修が必要ですが、うまく設計できると発生元の追跡が非常に楽になります。

  • 接続直後に SESSION_CONTEXT にサービス名やジョブ名をセットする
  • Extended Eventsでcontext_info等をアクションとして捕まえる

監視のためにSQLコメントを付ける方法(/* service:xxx */)と組み合わせると、フィルタもログの可読性も上がります。

監査ログ/ログ基盤を使う

運用要件(長期保管、改ざん耐性、横断検索など)があるなら、監査ログやログ基盤(Log Analytics 等)に寄せる選択肢もあります。ここは組織のポリシーとコストに依存するため、まずはExtended Eventsで短期に発生元を特定し、必要なら長期運用に拡張する流れが現実的です。

発生元が分かったら:DTUスパイクを潰すための実践的な対策

「誰が投げているか」が確定したら、次はDTUスパイクの原因(CPUなのかI/Oなのか、スキャンなのか)を潰していきます。対策は大きく4系統に分かれます。

対策カテゴリ具体例効果が出やすいケース注意点
アプリ側の改修取得列を減らす、ページング、キャッシュ、処理の分割、不要ジョブ停止自動生成SQLが“必要以上に広いSELECT”になっている影響範囲が広いので、リリース計画とセットで進める
クエリチューニングインデックス追加、統計更新、クエリ書き換え、ヒントの見直しフルスキャンや大きなソート/ハッシュが発生しているインデックスは書き込みコストも増える。目的列とフィルタ条件に合わせて設計する
実行タイミング調整3時間ごと→夜間へ、他処理と被らない時間へ、段階実行処理は必要だが「同時刻に集中」している根本解決ではないが、SLAを守る応急処置として有効
スケール/プラン見直しDTUの引き上げ、vCoreへの移行、読み取りスケール、サーバーレス検討改善しても処理量が物理的に多いコストが増える。先に“無駄な負荷”を削るのが基本

「SELECT 列名 AS _列名」が大量に並ぶクエリで起きやすい落とし穴

自動生成SQLでは、アプリ側の都合で「とりあえず全部取る」「画面で使う列は一部なのに全列をSELECTする」「結局WHEREが弱くて大量に読む」という形になりがちです。DTU 100%の原因がデータI/O(論理読み取り)に寄っているなら、次の観点で見直すと改善しやすいです。

  • 本当に必要な列だけに絞れているか(列数を減らすとI/Oもネットワークも減る)
  • 絞り込み条件が効いているか(インデックス設計の出発点)
  • 3時間ごとの処理が差分になっているか(毎回フルスキャンになっていないか)

発生元が特定できれば、アプリ担当者と「どの画面/どのジョブが、なぜこの列を読むのか」を具体的に議論できます。これが、SQL側だけで闇雲にチューニングするより早いことが多いです。

切り分けが終わったら必ずやる:セッションの停止と削除

Extended Eventsは強力ですが、常時回しっぱなしにする設計ではありません(特にLIKEフィルタを緩くしている場合)。目的のイベントが取れたら、不要になった監視は止めて片付けます。

ALTER EVENT SESSION [capture_sdk_token]
ON DATABASE
STATE = STOP;
GO

DROP EVENT SESSION [capture_sdk_token]
ON DATABASE;
GO 

まとめ:発生元が分かれば、DTUスパイクは“対話可能な問題”になる

クエリ ストアやQuery Performance Insightは「重いクエリの特定」には非常に有効ですが、「誰が投げたか」までは基本的に教えてくれません。3時間ごとにDTU 100%へ跳ねるような現象では、推測ではなく接続情報の証拠が必要です。

  • クエリ ストアで対象クエリを確定し、特徴語(列名・テーブル名)を整理する
  • Extended Eventsで問題クエリだけをフィルタし、client_app_name/hostname/usernameを記録する
  • 得られた情報を手掛かりに、ジョブ/サービス/監視の実行元を特定する
  • 特定後は、アプリ改修・チューニング・スケジュール・スケールを現実的に組み合わせて再発防止する

「謎のクエリ」を“名前のあるアプリ”に変えられた瞬間から、関係者全員が同じ土俵で改善に動けます。まずはExtended Eventsで、3時間後の次のスパイクを確実に捕まえてください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次