вариант

Научитесь составлять правильные и эффективные запросы на языке Kusto Query Language: изучите синтаксис, соединения, динамические типы, особенности работы с датами и временем, регулярные выражения, сериализацию, управление памятью и расширенные функции.

...Расширить все
12
Обновлено время 10 сентября 2026 г.

KQL Мастерство

Попробуйте сами: все примеры в этом навыке можно запустить на общедоступном кластере помощи: https://help.kusto.windows.net, база данных Samples (содержит StormEvents, SimpleGraph_Nodes/Edges, nyc_taxiи многое другое).

1. Основы языка запросовKQL

Язык запросов Kusto (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 имеет две плоскости выполнения:

Уровень Начинается с Примеры
Запрос Имя таблицы, let, print, datatable StormEvents | where State == "TEXAS"
Управление .show, .create, .set, .drop, .alter .show tables, .show table T schema

За командами управления могут следовать операторы запроса (вывод представляет собой таблицу), но весь запрос выполняется на плоскости управления. Нельзя начинать с запроса и передавать его в команду управления через оператор pipe.

// ✅ 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 Условные операторы JOIN имеют ограничения, отличающиеся от 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

Проверка кардинальности перед выполнением больших соединений

Всегда проверяйте кардинальность перед соединением таблиц с более чем 10 тыс. строк. Взрыв кросс-соединения стал причиной единственной E_RUNAWAY_QUERY ошибки (25K × 195 = потенциально 4,8 млн строк).

// 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

Правила для больших таблиц (>1 млн строк)

  1. Всегда начинайте с | count, чтобы понять размер таблицы
  2. Всегда выполняйте проверку «| where» перед проверкой «| summarize» — сначала отфильтруйте временной диапазон, ключ раздела или категорию
  3. Никогда не выполняйте операцию «dcount()» по столбцам с высокой кардинальностью без предварительной фильтрации
  4. Перед выполнением проверьте кардинальность соединений (см. раздел 3)
  5. Используйте 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
  • разбить на более мелкие временные интервалы и объединить результаты
  • Использовать | sample 10000 для предварительного анализа вместо полного сканирования

Если вы заметили, что E_RUNAWAY_QUERY

что в результате соединения или агрегации было сгенерировано слишком много строк на выходе, проверьте кардинальность соединения — одна или обе стороны слишком велики.

7. Контроль размера результатов

Объёмные результаты замедляют анализ. Меры предотвращения:

Тип запроса Меры предосторожности
Исследовательский Всегда заканчивайте на | take 10 или | take 20
Агрегация Использование | top 20 by ... не безграничные summarize
Широкие строки (векторы, JSON) | project только необходимые столбцы
make_list() / make_set() Избегайте применения к группам с высокой кардинальностью (приводит к образованию огромных ячеек)
Неизвестный размер Выполнить | count в первую очередь

«Векторная ловушка»: таблицы со встроенными столбцами (1536-мерные массивы с плавающей запятой) занимают ~30 КБ на строку. Даже | take 20 даёт 600 КБ. Всегда | 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+)" не @"\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) не datetime(2024)
Unexpected token после by Сложное выражение в предложении by в выражении summarize 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))

Группировка по времени в операторе SUMMARY

// 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) Анализ строки → промежуток времени 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

Оба 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

Правила устранения ошибок:

  1. Внимательно прочтите сообщение об ошибке — оно почти всегда точно указывает, в чём проблема
  2. Исправьте конкретную проблему с синтаксисом или экранированием, не меняйте подход
  3. Используйте таблицу самоисправлений (раздел 10) для сопоставления ошибок и способов их устранения
  4. Меняйте подход только после двух неудачных попыток исправления одного и того же запроса
  5. Оператор 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» мысленно проверьте:

  1. Произведена ли предварительная фильтрация? Для больших таблиц | where перед любым | summarize
  2. Ограничен ли результат? Эксplorаторные запросы заканчиваются | take N или | top N
  3. Произведено ли приведение типов динамических столбцов? Любой динамический столбец в by/on/order by обернут
  4. Регулярное выражение содержит группы? extract_all шаблоны содержат () вокруг того, что вы хотите захватить
  5. Безопасность кардинальности соединений? Обе стороны проверяются с помощью dcount() перед объединением
  6. Только необходимые столбцы? Широкие таблицы | project удаляются ненужные столбцы
  7. Литералы даты и времени допустимы? Используется datetime(2024-01-01) не datetime(2024) или простых целых чисел
  8. Сложные побочные выражения? Сначала используйте | extend сначала, а затем | summarize by вычисляемый столбец
  9. План восстановления после ошибки? Если запрос завершился сбоем, устраните конкретную ошибку — не меняйте стратегию
Посмотреть на GitHub
---
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

Копировать Копировать
Быстрая настройка: Скопируйте папку со скиллом в каталог .claude/skills/ Claude автоматически обнаружит и запустит этот скилл
Репозиторий microsoft/skills

Похожие навыки

microservices-patterns
Обновлено время 29 июня 2026 г.
jpa-patterns
Обновлено время 30 июня 2026 г.
fabric-lakehouse
Обновлено время 30 июня 2026 г.
prisma-expert
Обновлено время 29 июня 2026 г.
OR