kql
microsoft/skills
구문, 조인, 동적 유형, 날짜/시간 관련 주의사항, 정규 표현식, 직렬화, 메모리 관리 및 고급 함수 등을 포괄적으로 다루며, 정확하고 효율적인 Kusto 쿼리 언어 쿼리를 작성하는 방법을 배웁니다.
...모든 것을 확장하십시오KQL 숙달
직접 해보세요: 이 기술에 대한 모든
✅이 스킬의 모든 예제는 공개 도움말 클러스터에서 실행할 수 있습니다:https://help.kusto.windows.net, 데이터베이스Samples(StormEvents,SimpleGraph_Nodes/Edges,nyc_taxi, 그 외).
1. KQL(KQL) 기본 사항
Kusto 쿼리 언어(KQL)는 데이터를 탐색하기 위한 파이프-포워드(pipe-forward) 쿼리 언어입니다. 이는 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 에는 두 가지 실행 계층이 있습니다:
| 평면 | 다음으로 시작합니다 | 예시 |
|---|---|---|
| 쿼리 | 테이블 이름, 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. 동적 유형 규율
KQL의 dynamic 유형은 유연하지만 특정 상황에서는 엄격하게 적용됩니다. 흔히 저지르는 실수는 summarize by, order by에서 사용하거나 join on 캐스팅 없이 동적 열을 사용하는 것입니다.
규칙: 동적 타입의 열을 by, on, 또는 order by에서 동적 타입 열을 사용할 때는 항상 명시적인 형 변환으로 감싸야 합니다.
// ❌ 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
자동 수정: 오류 메시지에서 “'동적' 유형입니다”라는 문구가 보이면 tostring(), tolong(), 또는 todouble().
3. 조인 패턴 및 주의사항
KQL 조인에는 SQL과는 다른 제약 조건이 있습니다.
등가 조건만
KQL 조인 조건은 ==만 지원합니다. 조인 조건에서는 <, >, !=, 또는 함수 호출은 조인 술어에 사용할 수 없습니다.
// ❌ 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
범위 조인의 경우, 값을 미리 그룹화해야 합니다: | extend bin_val = bin(Value, 100), 그런 다음 bin_val. 참고: 빈 경계에 가까운 값은 인접한 빈에 포함될 수 있으므로, 정확성을 위해 인접한 빈을 확인하거나 범위를 중첩시키는 것을 고려하십시오.
왼쪽/오른쪽 속성 매칭
조인 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
대규모 조인 전 카디널리티 확인
행 수가 1만 행을 초과하는 테이블을 조인하기 전에는 항상 카디널리티를 확인하십시오. 크로스 조인 폭발이 유일한 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)
정규식 툴킷 — 파이썬으로 되돌아가지 마세요
| 함수 | 사용 사례 | 예시 |
|---|---|---|
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필터(시간 범위, 파티션 키)를 추가하십시오 - 다음 열의 수를 줄이십시오
by열 수를 줄이세요summarize - 더 작은 시간 창으로 나누고 결과를 UNION하기
- 탐색적 분석 시
| sample 10000전체 스캔 대신 탐색적 분석에 활용
다음과 같은 경우 E_RUNAWAY_QUERY
조인이나 집계로 인해 출력 행이 너무 많이 생성된 경우, 조인 카디널리티를 확인하십시오 — 한쪽 또는 양쪽 모두의 값이 너무 큽니다.
7. 결과 크기 관리
결과 크기가 크면 분석 속도가 느려집니다. 예방 방법:
| 쿼리 유형 | 예방 조치 |
|---|---|
| 탐색적 | 항상 다음으로 끝내야 함 | take 10 또는 | take 20 |
| 집계 | 사용 | top 20 by ... 무제한이 아닌 summarize |
| 넓은 행(벡터, JSON) | | project 필요한 열만 |
make_list() / make_set() |
카드널리티가 높은 그룹에서는 피해야 함 (셀 크기가 지나치게 커짐) |
| 크기 미상 | 실행 | count 먼저 |
벡터 함정: 임베딩 열(1536차원 부동소수점 배열)이 포함된 테이블은 행당 약 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 |
다음으로 감싸기 tostring()/tolong() |
Only equality is allowed |
조인 조건의 범위 술어 | S2/H3 셀을 사용한 사전 버킷화 또는 bin() |
extractall(): matching groups |
누락 () 정규식 내 누락 |
추가 (): @"(\w+)" not @"\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-01-01) not datetime(2024) |
Unexpected token 'after' by |
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) |
문자열 분석 → 날짜/시간 | todatetime("2024-01-15T10:30:00Z") |
totimespan(str) |
문자열 → 기간으로 구문 분석 | 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
정렬 vs 순서
둘 다 sort by 와 order by KQL에서는 동일하게 작동합니다 — 둘은 별칭입니다. 선호하는 것을 사용하되, 일관성을 유지하십시오.
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절)를 활용하십시오
- 동일한 쿼리에 대해 두 번의 수정 시도가 실패한 후에만 접근 방식을 변경하십시오
- The
parse연산자는 종종extract()보다 구조화된 텍스트에 더 간단합니다:
// 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 쿼리를 실행하기 전에 다음 사항을 머릿속으로 확인하십시오:
- 사전 필터링이 되었나요? 대규모 테이블의 경우
| where어떤| summarize - 결과 범위가 정해졌나요? 탐색적 쿼리는
| take N로 끝납니다.| top N - 동적 열의 형 변환이 완료되었나요?
by/on/order by는 감싸여 있습니다 - 정규 표현식에 그룹이 있나요?
extract_all패턴에는()캡처하려는 내용 주위에 - 조인 카디널리티는 안전한가요? 양쪽 모두
dcount()조인 전에 - 필요한 열만 포함되나요? 넓은 테이블은
| project불필요한 열을 제거해야 합니다 - 날짜/시간 리터럴이 유효한가요?
datetime(2024-01-01)아니면datetime(2024)또는 단순한 정수 사용 - 복잡한 by-표현식이 있나요?
| extend를 먼저 사용하고, 그 다음| summarize by계산된 열 - 오류 복구 계획? 쿼리가 실패하면 특정 오류를 수정하십시오 — 전략을 변경하지 마십시오
---
name: kql
description: Write correct, efficient Kusto Query Language queries with coverage of syntax, joins, dynamic types, datetime pitfalls, regex, serialization, memory management, and advanced functions.
---
# KQL Mastery
> **Try it yourself**: All `✅` examples in this skill can be run against the public help cluster:
> `https://help.kusto.windows.net`, database `Samples` (contains `StormEvents`, `SimpleGraph_Nodes`/`Edges`, `nyc_taxi`, and more).
## 1. KQL Basics
Kusto Query Language (KQL) is a pipe-forward query language for exploring data. It is the native query language for Azure Data Explorer (ADX), Microsoft Fabric Real-Time Intelligence (EventHouse), Azure Monitor Log Analytics, Microsoft Sentinel, and other Microsoft data services.
### Pipe-forward syntax
KQL queries are a chain of operators separated by `|`. Data flows left to right:
```kql
StormEvents // start with a table
| where State == "TEXAS" // filter rows
| summarize count() by EventType // aggregate
| top 5 by count_ desc // limit results
```
### Query vs management commands
KQL has two execution planes:
| Plane | Starts with | Examples |
|-------|-------------|----------|
| **Query** | Table name, `let`, `print`, `datatable` | `StormEvents \| where State == "TEXAS"` |
| **Management** | `.show`, `.create`, `.set`, `.drop`, `.alter` | `.show tables`, `.show table T schema` |
Management commands can be followed by query operators (the output is tabular), but the entire request runs on the management plane. You cannot start with a query and pipe into a management command.
```kql
// ✅ 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
```
When in doubt: if the first token starts with `.`, it's a management command. For a full catalog of schema exploration commands, see `references/discovery-queries.md`.
## 2. Dynamic Type Discipline
KQL's `dynamic` type is flexible but strict in certain contexts. A common mistake is using a dynamic column in `summarize by`, `order by`, or `join on` without casting.
**The rule**: Any time you use a dynamic-typed column in `by`, `on`, or `order by`, wrap it in an explicit cast.
```kql
// ❌ 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)
```
```kql
// ❌ ERROR: "order operator: key can't be of dynamic type"
StormEvents | order by StormSummary.TotalDamages desc
// ✅ FIX
StormEvents | order by tolong(StormSummary.TotalDamages) desc
```
```kql
// ❌ 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
```
**Self-correction**: When you see "is of a 'dynamic' type" in an error, add `tostring()`, `tolong()`, or `todouble()`.
## 3. Join Patterns & Pitfalls
KQL joins have constraints that differ from SQL.
### Equality only
KQL join conditions support **only `==`**. No `<`, `>`, `!=`, or function calls in join predicates.
```kql
// ❌ 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
```
For range joins, pre-bin values: `| extend bin_val = bin(Value, 100)`, then join on `bin_val`. Note: values near bin boundaries may land in adjacent bins — consider checking neighboring bins or overlapping the range for precision.
### Left/right attribute matching
Both sides of a join `on` clause must reference **column entities only** — not expressions, not aggregates.
```kql
// ❌ 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
```
### Cardinality check before large joins
**Always** check cardinality before joining tables with >10K rows. A cross-join explosion was the source of the single `E_RUNAWAY_QUERY` error (25K × 195 = potential 4.8M rows).
```kql
// 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. Regex in KQL
KQL handles regex natively — no need for Python.
### The `extract_all` gotcha
Unlike Python's `re.findall()`, KQL's `extract_all` **requires capturing groups** in the regex:
```kql
// ❌ 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)
```
### Regex toolkit — don't fall back to Python
| Function | Use case | Example |
|----------|----------|---------|
| `extract(regex, group, source)` | Single match | `extract(@"User '([^']+)'", 1, Msg)` |
| `extract_all(regex, source)` | All matches (needs `()`) | `extract_all(@"(\w+)", Text)` |
| `parse` | Structured extraction | `parse Msg with * "User '" Sender "' sent" *` |
| `matches regex` | Boolean filter | `where Url matches regex @"^https?://"` |
| `replace_regex` | Find and replace | `replace_regex(Text, @"\s+", " ")` |
## 5. Serialization Requirements
Window functions need serialized (ordered) input.
```kql
// ❌ 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)
```
Functions requiring serialization: `row_number()`, `row_cumsum()`, `prev()`, `next()`, `row_window_session()`.
## 6. Memory-Safe Query Patterns
The most common memory error. Caused by scanning too much data without pre-filtering.
### The progression of safety
```
Safest ──────────────────────────────────────────────── Most dangerous
| count | take 10 | where + summarize | summarize (no filter) | full scan
```
### Rules for large tables (>1M rows)
1. **Always start with `| count`** to understand table size
2. **Always `| where` before `| summarize`** — filter time range, partition key, or category first
3. **Never `dcount()` on high-cardinality columns** without pre-filtering
4. **Check join cardinality** before executing (see Section 3)
5. **Use `materialize()`** for subqueries referenced multiple times
```kql
// ❌ 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
```
### When you see `E_LOW_MEMORY_CONDITION`
The query touched too much data. Your options:
- Add `| where` filters (time range, partition key)
- Reduce the number of `by` columns in `summarize`
- Break into smaller time windows and union results
- Use `| sample 10000` for exploratory work instead of full scans
### When you see `E_RUNAWAY_QUERY`
A join or aggregation produced too many output rows. Check join cardinality — one or both sides is too large.
## 7. Result Size Discipline
Large results slow down analysis. Prevention:
| Query type | Safeguard |
|-----------|-----------|
| Exploratory | Always end with `\| take 10` or `\| take 20` |
| Aggregation | Use `\| top 20 by ...` not unbounded `summarize` |
| Wide rows (vectors, JSON) | `\| project` only needed columns |
| `make_list()` / `make_set()` | Avoid on high-cardinality groups (produces huge cells) |
| Unknown size | Run `\| count` first |
**The vector trap**: Tables with embedding columns (1536-dim float arrays) produce ~30KB per row. Even `| take 20` yields 600KB. Always `| project` away vector columns unless you specifically need them.
## 8. String Comparison Strictness
KQL sometimes requires explicit casts when comparing computed string values — even when both sides are already strings.
```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)
```
This is most common with computed values from `geo_point_to_s2cell()` and `strcat()` comparisons. When in doubt, cast with `tostring()`.
## 9. Advanced Functions
KQL handles these natively — no need for Python:
### Vector similarity
```kql
// 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
```
### Geo operations
```kql
// 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)
```
### Graph queries
```kql
// 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)
```
### Time series
```kql
// 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_)
```
For detailed examples and patterns, consult `references/advanced-patterns.md`.
## 10. Self-Correction Lookup Table
When you encounter an error, look it up here before retrying:
| Error message contains | Likely cause | Fix |
|---|---|---|
| `is of a 'dynamic' type` | Dynamic column in `by`/`on`/`order by` | Wrap in `tostring()`/`tolong()` |
| `Only equality is allowed` | Range predicate in join condition | Pre-bucket with S2/H3 cells or `bin()` |
| `extractall(): matching groups` | Missing `()` in regex | Add `()`: `@"(\w+)"` not `@"\w+"` |
| `row set must be serialized` | Window function on unsorted data | Add `\| serialize` or `\| order by` before it |
| `Cannot compare values of types string and string` | Computed string comparison | Add `tostring()` on both sides |
| `Failed to resolve column named 'X'` | Wrong column name or wrong table | Run `.show table T schema` to check column names |
| `E_LOW_MEMORY_CONDITION` | Query touched too much data | Add `\| where` filters, reduce time range, break into steps |
| `E_RUNAWAY_QUERY` | Join/aggregation produced too many rows | Check cardinality before joining; add pre-filters |
| `for each left attribute, right attribute` | Join `on` clause incomplete | Use explicit form: `on $left.X == $right.Y` |
| `needs to be bracketed` | Reserved word used as identifier | Use `['keyword']` syntax |
| `plugin doesn't exist` | Unavailable plugin on this cluster | Fall back to equivalent function or Python |
| `Expected string literal in datetime()` | Bare integer in datetime literal | Use `datetime(2024-01-01)` not `datetime(2024)` |
| `Unexpected token` after `by` | Complex expression in summarize by-clause | `extend` the expression first, then `summarize by` the column |
| `not recognized` / `unknown operator` | Operator not available on this engine | Check operator support; try equivalent (`order by` = `sort by`) |
## 11. Datetime Pitfalls
Datetime literals are a common source of errors. A wrong literal format can cascade into completely different approaches instead of fixing the small issue.
### Literal format
```kql
// ❌ 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)
```
### Filtering by year, month, or hour
```kql
// ❌ 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))
```
### Time bucketing in summarize
```kql
// 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
```
### Useful datetime functions
| Function | Purpose | Example |
|----------|---------|---------|
| `bin(ts, 1h)` | Round down to bucket boundary | `bin(Timestamp, 1d)` |
| `startofmonth(ts)` | First day of month | `startofmonth(Timestamp)` |
| `datetime_part("hour", ts)` | Extract component | `datetime_part("year", Timestamp)` |
| `format_datetime(ts, fmt)` | Format as string | `format_datetime(Timestamp, "yyyy-MM")` |
| `ago(1d)` | Relative time | `where Timestamp > ago(1d)` |
| `between(a .. b)` | Range filter (inclusive) | `where Timestamp between (datetime(2024-01-01) .. datetime(2024-01-31T23:59:59))` |
| `todatetime(str)` | Parse string → datetime | `todatetime("2024-01-15T10:30:00Z")` |
| `totimespan(str)` | Parse string → timespan | `totimespan("01:30:00")` |
## 12. Operator Naming & Equality
KQL has subtle differences from SQL syntax.
### Naming conventions
| Entity | Convention | Example |
|--------|-----------|---------|
| Tables | UpperCamelCase | `StormEvents`, `NetworkLogs` |
| Columns | UpperCamelCase | `StartTime`, `EventType` |
| Variables (`let`) | snake_case | `let filtered_events = ...` |
| Built-in functions | snake_case | `format_bytes()`, `geo_distance_2points()` |
| Stored functions | UpperCamelCase | `.create function GetTopUsers` |
### Equality operators
```kql
// 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 vs order
Both `sort by` and `order by` work identically in KQL — they are aliases. Use whichever you prefer, but be consistent.
### contains vs has
```kql
// 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. Error Recovery Strategy
When a first KQL query fails, the temptation is to abandon the entire approach and try something completely different. The correct response is almost always to **fix the specific error**, not change strategy.
### The pattern to avoid
```
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
```
### The correct pattern
```
Query 1: extract(@"pattern", 1, col) → Parse error (bad escaping)
Query 2: extract(@"pattern", 1, col) → Fix the specific escaping issue → Success
```
**Rules for error recovery:**
1. Read the error message carefully — it almost always tells you exactly what's wrong
2. Fix the **specific** syntax/escaping issue, don't switch approaches
3. Use the self-correction table (Section 10) to map errors to fixes
4. Only switch approaches after 2 failed fixes of the same query
5. The `parse` operator is often simpler than `extract()` for structured text:
```kql
// 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. Query Writing Checklist
Before running any KQL query, mentally check:
1. **Pre-filtered?** Large tables have a `| where` before any `| summarize`
2. **Result bounded?** Exploratory queries end with `| take N` or `| top N`
3. **Dynamic columns cast?** Any dynamic column in `by`/`on`/`order by` is wrapped
4. **Regex has groups?** `extract_all` patterns have `()` around what you want to capture
5. **Join cardinality safe?** Both sides checked with `dcount()` before joining
6. **Needed columns only?** Wide tables get `| project` to drop unneeded columns
7. **Datetime literals valid?** Using `datetime(2024-01-01)` not `datetime(2024)` or bare integers
8. **Complex by-expressions?** Use `| extend` first, then `| summarize by` the computed column
9. **Error recovery plan?** If a query fails, fix the specific error — don't change strategy
모든 파일
0개 파일kql 설치
스킬 파일을 다운로드하여 .claude/skills/ 디렉터리에 압축을 풀어주세요.
ZIP 다운로드저장소를 클론하고 스킬 파일을 프로젝트에 복사하세요.
git clone https://github.com/microsoft/skills/tree/main/.github/skills/kql # Copy SKILL.md to your .claude/skills/ directory
복사





집
