kql について
KQL Masteryは、Azureデータサービスで Kusto Query Language を使うためのスキルです。構文や一般的なエラー、安全なクエリ手法を扱い、結合、正規表現、時系列、ベクトル、グラフの例を公開 help cluster 向けにも示します。
主な機能
- 01KQLクエリと管理コマンドの解説
- 02dynamic型と結合制約の説明
- 03正規表現とシリアライズのパターン
- 04メモリ安全なクエリと結果制限
- 05ベクトル・地理・グラフ・時系列クエリ
活用シーン
- 01KQLクエリエラーの原因調査
- 02大規模テーブルの安全な分析クエリ作成
- 03地理・グラフ・時系列データの探索
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ microsoft/ skills --skill kqlインストール後、Claude Code で/kqlを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/advanced-patterns.mddiscovery-queries.mderror-recovery.mdquery-templates.md詳細
KQL マスター
自分で試す:このスキルにある
✅の例はすべて、公開ヘルプクラスターで実行できます:https://help.kusto.windows.net、データベースはSamplesです(StormEvents、SimpleGraph_Nodes/Edges、nyc_taxiなどが含まれています)。
1. KQL の基本
Kusto Query Language(KQL)は、データを探索するためのパイプフォワード型クエリ言語です。Azure Data Explorer(ADX)、Microsoft Fabric Real-Time Intelligence(EventHouse)、Azure Monitor Log Analytics、Microsoft Sentinel、その他の Microsoft データサービスで使われるネイティブのクエリ言語です。
パイプフォワード構文
KQL クエリは、| で区切られた演算子の連鎖です。データは左から右へ流れます:
StormEvents // start with a table
| where State == "TEXAS" // filter rows
| summarize count() by EventType // aggregate
| top 5 by count_ desc // limit results
クエリと管理コマンド
KQL には 2 つの実行プレーンがあります:
| プレーン | 開始要素 | 例 |
|---|---|---|
| クエリ | テーブル名、let、print、datatable | StormEvents | where State == "TEXAS" |
| 管理 | .show、.create、.set、.drop、.alter | .show tables、.show table T schema |
管理コマンドの後にクエリ演算子を続けることはできます(出力は表形式です)が、リクエスト全体は管理プレーンで実行されます。クエリから始めて管理コマンドへパイプでつなぐことはできません。
// ✅ WORKS — management command piped to query operators
.show tables | project TableName | where TableName has "Events"
// ❌ WRONG — query piped into management command
StormEvents | take 5 | .show tables
判断に迷ったら、最初のトークンが . で始まる場合は管理コマンドです。スキーマ探索コマンドの一覧については、references/discovery-queries.md を参照してください。
2. Dynamic 型の扱い
KQL の dynamic 型は柔軟ですが、特定のコンテキストでは厳格です。よくある間違いは、キャストせずに summarize by、order by、または join on で dynamic 型の列を使用することです。
ルール:by、on、または order by で dynamic 型の列を使うときは、必ず明示的なキャストで囲みます。
// ❌ ERROR: "Summarize group key ... is of a 'dynamic' type"
StormEvents | summarize count() by StormSummary.Details.Location
// ✅ FIX
StormEvents | summarize count() by tostring(StormSummary.Details.Location)
// ❌ ERROR: "order operator: key can't be of dynamic type"
StormEvents | order by StormSummary.TotalDamages desc
// ✅ FIX
StormEvents | order by tolong(StormSummary.TotalDamages) desc
// ❌ ERROR in join: dynamic join key
StormEvents | join kind=inner (PopulationData) on $left.StormSummary == $right.State
// ✅ FIX — cast both sides
StormEvents
| extend State_str = tostring(StormSummary.Details.Location)
| join kind=inner (PopulationData) on $left.State_str == $right.State
自己修正:エラーに「is of a 'dynamic' type」と表示された場合は、tostring()、tolong()、または todouble() を追加します。
3. Join のパターンと注意点
KQL の join には、SQL とは異なる制約があります。
等価演算子のみ
KQL の join 条件でサポートされるのは == のみです。join 述語では、<、>、!= や関数呼び出しは使えません。
// ❌ ERROR: "Only equality is allowed in this context"
StormEvents | join (nyc_taxi) on geo_distance_2points(BeginLon, BeginLat, pickup_longitude, pickup_latitude) < 1000
// ✅ WORKAROUND — pre-bucket into spatial cells, then join on cell ID
StormEvents
| extend cell = geo_point_to_s2cell(BeginLon, BeginLat, 8)
| join kind=inner (nyc_taxi | extend cell = geo_point_to_s2cell(pickup_longitude, pickup_latitude, 8)) on cell
範囲 join では、値を事前にビン分割します:| extend bin_val = bin(Value, 100) とし、その後 bin_val で join します。注:ビンの境界付近の値は隣接するビンに入る場合があります。精度を高めるには、隣接するビンを確認するか、範囲を重ねることを検討してください。
左右の属性の照合
join の on 句の両側で参照できるのは、列エンティティのみです。式や集計は使えません。
// ❌ ERROR: "for each left attribute, right attribute should be selected"
StormEvents | join kind=inner (PopulationData) on $left.State
// ✅ FIX — specify both sides explicitly
StormEvents | join kind=inner (PopulationData) on $left.State == $right.State
大規模な join の前にカーディナリティを確認する
10K 行を超えるテーブルを join する前に、必ずカーディナリティを確認してください。クロス join による爆発が、唯一の E_RUNAWAY_QUERY エラーの原因でした(25K × 195 で、最大 4.8M 行になる可能性がありました)。
// Before joining, check how many rows each side contributes
StormEvents | summarize dcount(State) // → 67 distinct states
PopulationData | summarize dcount(State) // → 52 — safe to join
4. KQL の正規表現
KQL は正規表現をネイティブに扱うため、Python は不要です。
extract_all の落とし穴
Python の re.findall() とは異なり、KQL の extract_all では、正規表現にキャプチャグループが必要です:
// ❌ ERROR: "extractall(): argument 2 must be a valid regex with [1..16] matching groups"
StormEvents | extend words = extract_all(@"[a-zA-Z]{3,}", EventNarrative)
// ✅ FIX — add parentheses around the pattern
StormEvents | extend words = extract_all(@"([a-zA-Z]{3,})", EventNarrative)
正規表現ツールキット — Python に頼らない
| 関数 | 用途 | 例 |
|---|---|---|
extract(regex, group, source) | 単一の一致 | extract(@"User '([^']+)'", 1, Msg) |
extract_all(regex, source) | すべての一致(() が必要) | extract_all(@"(\w+)", Text) |
parse | 構造化データの抽出 | parse Msg with * "User '" Sender "' sent" * |
matches regex | ブール値によるフィルター | where Url matches regex @"^https?://" |
replace_regex | 検索と置換 | replace_regex(Text, @"\s+", " ") |
5. シリアル化の要件
ウィンドウ関数には、シリアル化(順序付け)された入力が必要です。
// ❌ ERROR: "Function 'row_cumsum' cannot be invoked. The row set must be serialized."
StormEvents
| where State == "TEXAS"
| summarize DailyCount = count() by bin(StartTime, 1d)
| extend CumulativeCount = row_cumsum(DailyCount)
// ✅ FIX — add | serialize (or | order by, which implicitly serializes)
StormEvents
| where State == "TEXAS"
| summarize DailyCount = count() by bin(StartTime, 1d)
| order by StartTime asc
| extend CumulativeCount = row_cumsum(DailyCount)
シリアル化が必要な関数:row_number()、row_cumsum()、prev()、next()、row_window_session()。
6. メモリセーフなクエリパターン
最もよくあるメモリエラーです。事前にフィルターをかけず、データをスキャンしすぎることが原因です。
安全性の段階
Safest ──────────────────────────────────────────────── Most dangerous
| count | take 10 | where + summarize | summarize (no filter) | full scan
大きなテーブル (>100万行) に関するルール
- まず
| countで始める — テーブルのサイズを把握するため | summarizeの前に必ず| where— まず時間範囲、パーティションキー、またはカテゴリで絞り込む- 事前フィルタなしで基数の高い列に
dcount()を使わない - 実行前に結合の基数を確認する (セクション 3 を参照)
- 複数回参照されるサブクエリには
materialize()を使う
// ❌ OUT OF MEMORY — large table, no filter, many group-by columns
StormEvents
| summarize dcount(EventType), count() by StartTime, State, Source
| where dcount_EventType > 1
// ✅ SAFE — filter first, then aggregate
StormEvents
| where StartTime between (datetime(2007-04-15) .. datetime(2007-04-16))
| summarize dcount(EventType) by State, Source
| where dcount_EventType > 1
E_LOW_MEMORY_CONDITION が表示された場合
クエリが扱うデータ量が多すぎます。選択肢:
| whereフィルタ (時間範囲、パーティションキー) を追加するsummarizeのby列の数を減らす- より小さな時間枠に分割し、結果を union する
- 全体スキャンの代わりに、探索的な作業には
| sample 10000を使う
E_RUNAWAY_QUERY が表示された場合
結合または集計が生成する出力行が多すぎます。結合の基数を確認してください — 片方または両方の側が大きすぎます。
7. 結果サイズの規律
大きな結果は分析を遅くします。防止策:
| クエリの種類 | 安全策 |
|---|---|
| 探索的 | 常に | take 10 または | take 20 で終える |
| 集計 | 無制限の summarize ではなく | top 20 by ... を使う |
| 幅の広い行 (ベクトル、JSON) | 必要な列だけを | project する |
make_list() / make_set() | 基数の高いグループでは避ける (巨大なセルを生成する) |
| サイズ不明 | まず | count を実行する |
ベクトルの罠: 埋め込み列 (1536 次元の float 配列) を持つテーブルは、1 行あたり約 30KB を生成します。| take 20 でも 600KB になります。特に必要でない限り、常に | project でベクトル列を除外してください。
8. 文字列比較の厳格さ
KQL では、計算された文字列値を比較する際に明示的なキャストが必要になることがあります — 両側がすでに文字列であってもです。
// ❌ ERROR: "Cannot compare values of types string and string. Try adding explicit casts"
StormEvents | where geo_point_to_s2cell(BeginLon, BeginLat, 16) == other_cell
// ✅ FIX — wrap both sides in tostring()
StormEvents | where tostring(geo_point_to_s2cell(BeginLon, BeginLat, 16)) == tostring(other_cell)
これは geo_point_to_s2cell() からの計算値と strcat() の比較で最も一般的です。迷ったときは tostring() でキャストしてください。
9. 高度な関数
KQL はこれらをネイティブに処理します — Python は不要です:
ベクトル類似度
// try it! — cosine similarity on Iris feature vectors
let target = pack_array(5.1, 3.5, 1.4, 0.2);
Iris
| extend Vec = pack_array(SepalLength, SepalWidth, PetalLength, PetalWidth)
| extend sim = series_cosine_similarity(Vec, target)
| top 5 by sim desc
地理操作
// Distance between two points (meters)
StormEvents | extend dist = geo_distance_2points(BeginLon, BeginLat, EndLon, EndLat)
// Spatial bucketing for joins
StormEvents | extend cell = geo_point_to_s2cell(BeginLon, BeginLat, 8)
グラフクエリ
// Persistent graph model — try it on the help cluster!
graph("Simple")
| graph-match (src)-[e*1..3]->(dst)
where src.name == "Alice"
project src.name, dst.name, path_length = array_length(e)
// Transient graph — build inline with make-graph
SimpleGraph_Edges
| make-graph source --> target with SimpleGraph_Nodes on id
| graph-match (src)-[e*1..5]->(dst)
where src.name == "Alice"
project src.name, dst.name, path_length = array_length(e)
時系列
// try it! — create a time series and detect anomalies
StormEvents
| make-series count() default=0 on StartTime step 1d
| extend anomalies = series_decompose_anomalies(count_)
詳細な例とパターンについては、references/advanced-patterns.md を参照してください。
10. 自己修正ルックアップテーブル
エラーが発生したら、再試行する前にここで調べてください:
| エラーメッセージに含まれる内容 | 考えられる原因 | 修正方法 |
|---|---|---|
is of a 'dynamic' type | by/on/order by 内の dynamic 列 | tostring()/tolong() でラップする |
Only equality is allowed | 結合条件内の範囲述語 | S2/H3 セルまたは bin() で事前にバケット化する |
extractall(): matching groups | 正規表現に () がない | () を追加する: @"\w+" ではなく @"(\w+)" |
row set must be serialized | ソートされていないデータへのウィンドウ関数 | その前に | serialize または | order by を追加する |
Cannot compare values of types string and string | 計算された文字列の比較 | 両側に tostring() を追加する |
Failed to resolve column named 'X' | 列名が間違っているかテーブルが間違っている | .show table T schema を実行して列名を確認する |
E_LOW_MEMORY_CONDITION | クエリが扱うデータ量が多すぎる | | where フィルタを追加し、時間範囲を減らし、ステップに分割する |
E_RUNAWAY_QUERY | 結合/集計が生成する行が多すぎる | 結合前に基数を確認し、事前フィルタを追加する |
for each left attribute, right attribute | 結合の on 句が不完全 | 明示的な形式を使用する: on $left.X == $right.Y |
needs to be bracketed | 予約語を識別子として使用 | ['keyword'] 構文を使用する |
plugin doesn't exist | このクラスターで利用できないプラグイン | 同等の関数または Python にフォールバックする |
Expected string literal in datetime() | datetime リテラル内の裸の整数 | datetime(2024) ではなく datetime(2024-01-01) を使用する |
by の後の Unexpected token | summarize の by 句内の複雑な式 | まず式を extend し、その後その列で summarize by する |
not recognized / unknown operator | このエンジンで利用できない演算子 | 演算子のサポートを確認し、同等のものを試す (order by = sort by) |
11. 日時に関する落とし穴
日時リテラルは、よくあるエラーの原因です。リテラルの形式を間違えると、小さな問題を修正する代わりに、まったく異なるアプローチへと発展してしまうことがあります。
リテラルの形式
// ❌ WRONG — bare year is not a valid datetime
StormEvents | where StartTime > datetime(2007)
// ✅ RIGHT — always use full date format
StormEvents | where StartTime > datetime(2007-01-01)
年、月、時によるフィルター処理
// ❌ WRONG — comparing datetime column to integer
StormEvents | where StartTime == 2007
// ✅ RIGHT — use datetime_part() to extract components
StormEvents | where datetime_part("year", StartTime) == 2007
// ✅ ALSO RIGHT — use between with datetime range
StormEvents | where StartTime between (datetime(2007-01-01) .. datetime(2007-12-31T23:59:59))
summarize での時間のバケット化
// This works, but can be harder to read and reuse in complex queries
StormEvents | summarize count() by startofmonth(StartTime)
// Clearer — extend first, then summarize by the computed column
StormEvents
| extend Month = startofmonth(StartTime)
| summarize count() by Month
| order by Month asc
便利な日時関数
| 関数 | 用途 | 例 |
|---|---|---|
bin(ts, 1h) | バケット境界まで切り下げる | bin(Timestamp, 1d) |
startofmonth(ts) | 月の初日 | startofmonth(Timestamp) |
datetime_part("hour", ts) | 要素を抽出する | datetime_part("year", Timestamp) |
format_datetime(ts, fmt) | 文字列として書式設定する | format_datetime(Timestamp, "yyyy-MM") |
ago(1d) | 相対時刻 | where Timestamp > ago(1d) |
between(a .. b) | 範囲フィルター(両端を含む) | where Timestamp between (datetime(2024-01-01) .. datetime(2024-01-31T23:59:59)) |
todatetime(str) | 文字列をdatetimeに解析する | todatetime("2024-01-15T10:30:00Z") |
totimespan(str) | 文字列をtimespanに解析する | totimespan("01:30:00") |
12. 演算子の命名規則と等価性
KQLの構文は、SQLと微妙に異なります。
命名規則
| エンティティ | 規則 | 例 |
|---|---|---|
| テーブル | UpperCamelCase | StormEvents, NetworkLogs |
| 列 | UpperCamelCase | StartTime, EventType |
変数(let) | snake_case | let filtered_events = ... |
| 組み込み関数 | snake_case | format_bytes(), geo_distance_2points() |
| 保存済み関数 | UpperCamelCase | .create function GetTopUsers |
等価演算子
// In where clauses, == is case-sensitive, =~ is case-insensitive
StormEvents | where State == "TEXAS" | count // exact match
StormEvents | where State =~ "texas" | count // case-insensitive
// In joins, use == only
StormEvents | join kind=inner (PopulationData) on State
sort と order
KQLでは、sort by と order by は同じように動作し、エイリアスとして使えます。どちらを使ってもかまいませんが、一貫して使用してください。
contains と has
// contains: substring match (slower)
StormEvents | where EventNarrative contains "tree" // finds "trees", "treetop" too
// has: term/word match (faster, uses index)
StormEvents | where EventNarrative has "tree" // matches word boundaries only
// For exact prefix/suffix
StormEvents | where EventType startswith "Thunder"
StormEvents | where Source endswith "Spotter"
13. エラーからの復旧戦略
最初のKQLクエリが失敗すると、アプローチ全体を諦めてまったく別の方法を試したくなるかもしれません。正しい対応は、ほとんどの場合、戦略を変えるのではなく、特定のエラーを修正することです。
避けるべきパターン
Query 1: extract(@"pattern", 1, col) → Parse error
Query 2: todynamic(col) → Different error
Query 3: parse_json(col) → Another error
Query 4: Python script → Works but 10x tokens
正しいパターン
Query 1: extract(@"pattern", 1, col) → Parse error (bad escaping)
Query 2: extract(@"pattern", 1, col) → Fix the specific escaping issue → Success
エラーから復旧する際のルール:
- エラーメッセージを注意深く読みます。ほとんどの場合、何が問題なのかが正確に示されています
- 特定の構文またはエスケープの問題を修正し、アプローチを切り替えないでください
- エラーと修正方法を対応させるには、自己修正表(セクション10)を使用します
- 同じクエリに対して2回修正を試みても失敗した場合にのみ、アプローチを切り替えます
- 構造化テキストには、
extract()よりもparse演算子のほうがシンプルな場合がよくあります。
// Instead of complex regex on TraceLogs:
// extract(@"file path: \"\"([^\"]+)\"\"", 1, Message)
// Use parse for structured extraction (try it on help cluster, SampleLogs db):
cluster("help").database("SampleLogs").TraceLogs
| where Message has "file path"
| parse Message with * "file path: \"\"" FilePath "\"\"" *
| project Timestamp, FilePath
| take 5
14. クエリ作成チェックリスト
KQLクエリを実行する前に、次の項目を確認してください。
- 事前にフィルター処理されていますか? 大きなテーブルでは、
| summarizeの前に| whereを使用します - 結果の件数は制限されていますか? 探索的なクエリの末尾には、
| take Nまたは| top Nを付けます - dynamic列はキャストされていますか?
by/on/order byに使用するdynamic列はすべて、ラップされています - 正規表現にグループがありますか?
extract_allのパターンでは、キャプチャする部分を()で囲みます - 結合のカーディナリティは安全ですか? 結合する前に、両側を
dcount()で確認します - 必要な列だけを使用していますか? 列数の多いテーブルでは、不要な列を削除するために
| projectを使用します - 日時リテラルは有効ですか?
datetime(2024)や整数をそのまま使うのではなく、datetime(2024-01-01)を使用します - 複雑なby式を使っていますか? まず
| extendを使用し、その後、計算した列を| summarize byに指定します - エラーから復旧する計画はありますか? クエリが失敗した場合は、戦略を変えず、特定のエラーを修正します