opção

Escreva consultas corretas e eficientes na Linguagem de Consulta Kusto, com abordagem de sintaxe, junções, tipos dinâmicos, armadilhas relacionadas a data e hora, expressões regulares, serialização, gerenciamento de memória e funções avançadas.

...Expandir tudo
12
Tempo atualizado 10 de Setembro de 2026

KQL Domínio

Experimente você mesmo: Todos os exemplos desta habilidade podem ser executados no cluster público de ajuda: https://help.kusto.windows.net, banco de dados Samples (contém StormEvents, SimpleGraph_Nodes/Edges, nyc_taxi, e muito mais).

1. Noções básicas da KQL

A KQL (KQL) é uma linguagem de consulta do tipo “pipe-forward” para explorar dados. É a linguagem de consulta nativa do Azure Data Explorer (ADX), do Microsoft Fabric Real-Time Intelligence (EventHouse), do Azure Monitor Log Analytics, do Microsoft Sentinel e de outros serviços de dados da Microsoft.

As consultas com sintaxe pipe-forward

KQL são uma cadeia de operadores separados por |. Os dados fluem da esquerda para a direita:

StormEvents                          // start with a table
| where State == "TEXAS"             // filter rows
| summarize count() by EventType     // aggregate
| top 5 by count_ desc              // limit results

Comandos de consulta versus comandos de gerenciamento

KQL possui dois planos de execução:

Plano Começa com Exemplos
Consulta Nome da tabela, let, print, datatable StormEvents | where State == "TEXAS"
Gerenciamento .show, .create, .set, .drop, .alter .show tables, .show table T schema

Os comandos de gerenciamento podem ser seguidos por operadores de consulta (a saída é tabular), mas toda a solicitação é executada no plano de gerenciamento. Não é possível começar com uma consulta e canalizá-la para um comando de gerenciamento.

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

Em caso de dúvida: se o primeiro token começar com ., trata-se de um comando de gerenciamento. Para obter um catálogo completo de comandos de exploração de esquema, consulte references/discovery-queries.md.

2. Disciplina de Tipos Dinâmicos

O dynamic é flexível, mas rigorosa em determinados contextos. Um erro comum é usar uma coluna dinâmica em summarize by, order byou join on sem conversão de tipo.

A regra: sempre que você usar uma coluna de tipo dinâmico em by, onou order by, coloque-a entre um conversão explícita.

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

Autocorreção: quando você vir “é de um tipo ‘dinâmico’” em um erro, adicione tostring(), tolong(), ou todouble().

3. Padrões e armadilhas de junção

KQL As junções têm restrições diferentes das do SQL.

Apenas igualdade

KQL apenas aceitam o operador ==”. Não são permitidas <, >, !=, nem chamadas de função nos predicados de junção.

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

Para junções por intervalo, pré-agrupe os valores: | extend bin_val = bin(Value, 100), então faça a junção com base em bin_val. Observação: valores próximos aos limites dos intervalos podem cair em intervalos adjacentes — considere verificar os intervalos vizinhos ou sobrepor o intervalo para maior precisão.

Correspondência de atributos à esquerda/direita

Ambos os lados de uma cláusula de junção on devem referenciar apenas entidades de coluna — não expressões, nem agregados.

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

Verificação de cardinalidade antes de junções grandes

Sempre verifique a cardinalidade antes de unir tabelas com mais de 10 mil linhas. Uma explosão de junção cruzada foi a causa do único E_RUNAWAY_QUERY erro (25 mil × 195 = 4,8 milhões de linhas em potencial).

// 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. Expressões regulares no `KQL`

KQL suporta expressões regulares nativamente — sem necessidade de Python.

A extract_all pega-pega

Ao contrário do Python re.findall(), o KQL extract_all exige grupos de captura na expressão regular:

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

Kit de ferramentas de expressões regulares — não recorra ao Python

Função Caso de uso Exemplo
extract(regex, group, source) Correspondência única extract(@"User '([^']+)'", 1, Msg)
extract_all(regex, source) Todas as correspondências (requer ()) extract_all(@"(\w+)", Text)
parse Extração estruturada parse Msg with * "User '" Sender "' sent" *
matches regex Filtro booleano where Url matches regex @"^https?://"
replace_regex Localizar e substituir replace_regex(Text, @"\s+", " ")

5. Requisitos de serialização

As funções de janela precisam de entrada serializada (ordenada).

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

Funções que exigem serialização: row_number(), row_cumsum(), prev(), next(), row_window_session().

6. Padrões de consulta seguros para a memória

O erro de memória mais comum. Causado pela varredura de uma quantidade excessiva de dados sem pré-filtragem.

A progressão da segurança

Safest ──────────────────────────────────────────────── Most dangerous
| count    | take 10    | where + summarize    | summarize (no filter)    | full scan

Regras para tabelas grandes (>1 milhão de linhas)

  1. Sempre comece com `| count` para entender o tamanho da tabela
  2. Sempre use o `| where` antes do `| summarize` — filtre primeiro o intervalo de tempo, a chave de partição ou a categoria
  3. Nunca execute `dcount()` em colunas de alta cardinalidade sem pré-filtragem
  4. Verifique a cardinalidade da junção antes de executar (consulte a Seção 3)
  5. Use `materialize()` para subconsultas referenciadas várias vezes
// ❌ 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

Quando você vir E_LOW_MEMORY_CONDITION

A consulta acessou dados em excesso. Suas opções:

  • Adicione | where filtros (intervalo de tempo, chave de partição)
  • Reduzir o número de by colunas na summarize
  • Divida em janelas de tempo menores e una os resultados
  • Use | sample 10000 para análises exploratórias em vez de varreduras completas

Quando você perceber que E_RUNAWAY_QUERY

Uma junção ou agregação gerou muitas linhas de saída, verifique a cardinalidade da junção — um ou ambos os lados são grandes demais.

7. Disciplina no tamanho dos resultados

Resultados grandes tornam a análise mais lenta. Prevenção:

Tipo de consulta Medida de proteção
Exploratória Sempre termine com | take 10 ou | take 20
Agregação Usar | top 20 by ... não ilimitado summarize
Linhas largas (vetores, JSON) | project apenas as colunas necessárias
make_list() / make_set() Evite em grupos de alta cardinalidade (gera células enormes)
Tamanho desconhecido Executar | count primeiro

A armadilha do vetor: tabelas com colunas incorporadas (matrizes de tipo float de 1.536 dimensões) geram cerca de 30 KB por linha. Mesmo | take 20 isso resulta em 600 KB. Sempre | project as colunas vetoriais, a menos que você precise delas especificamente.

8. Rigor na comparação de strings

KQL às vezes exige conversões explícitas ao comparar valores de string calculados — mesmo quando ambos os lados já são strings.

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

Isso é mais comum com valores calculados a partir de geo_point_to_s2cell() e strcat() . Em caso de dúvida, utilize o cast com tostring().

9. Funções avançadas

KQL lida com isso nativamente — não há necessidade de usar Python:

Semelhança entre vetores

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

Operações geográficas

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

Consultas em grafos

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

Séries temporais

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

Para exemplos e padrões detalhados, consulte references/advanced-patterns.md.

10. Tabela de consulta de autocorreção

Ao encontrar um erro, verifique aqui antes de tentar novamente:

A mensagem de erro contém Causa provável Solução
is of a 'dynamic' type Coluna dinâmica em by/on/order by Envolva em tostring()/tolong()
Only equality is allowed Predicado de intervalo na condição de junção Pré-agrupamento com células S2/H3 ou bin()
extractall(): matching groups Ausente () em expressão regular Adicionar (): @"(\w+)" não @"\w+"
row set must be serialized Função de janela em dados não classificados Adicionar | serialize ou | order by antes dela
Cannot compare values of types string and string Comparação de cadeias de caracteres calculadas Adicionar tostring() em ambos os lados
Failed to resolve column named 'X' Nome de coluna ou tabela incorreto Execute .show table T schema para verificar os nomes das colunas
E_LOW_MEMORY_CONDITION A consulta acessou dados em excesso Adicione | where filtros, reduza o intervalo de tempo, divida em etapas
E_RUNAWAY_QUERY A junção/agregação gerou muitas linhas Verifique a cardinalidade antes da junção; adicione pré-filtros
for each left attribute, right attribute A cláusula de junção on cláusula incompleta Use a forma explícita: on $left.X == $right.Y
needs to be bracketed Palavra reservada usada como identificador Use ['keyword'] sintaxe
plugin doesn't exist Plug-in indisponível neste cluster Recorra à função equivalente ou ao Python
Expected string literal in datetime() Número inteiro sem formato em literal de data e hora Usar datetime(2024-01-01) não datetime(2024)
Unexpected token após by Expressão complexa na cláusula “by” de summarize extend a expressão primeiro, depois summarize by a coluna
not recognized / unknown operator Operador indisponível neste mecanismo Verifique a compatibilidade do operador; tente um equivalente (order by = sort by)

11. Armadilhas com datetime

Literais de data e hora são uma fonte comum de erros. Um formato literal incorreto pode levar a abordagens completamente diferentes, em vez de resolver o pequeno problema.

Formato do literal

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

Filtragem por ano, mês ou hora

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

Agrupamento de tempo na função `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

Funções úteis de data e hora

Função Finalidade Exemplo
bin(ts, 1h) Arredondar para baixo até o limite do intervalo bin(Timestamp, 1d)
startofmonth(ts) Primeiro dia do mês startofmonth(Timestamp)
datetime_part("hour", ts) Extrair componente datetime_part("year", Timestamp)
format_datetime(ts, fmt) Formatar como string format_datetime(Timestamp, "yyyy-MM")
ago(1d) Tempo relativo where Timestamp > ago(1d)
between(a .. b) Filtro de intervalo (inclusive) where Timestamp between (datetime(2024-01-01) .. datetime(2024-01-31T23:59:59))
todatetime(str) Analisar string → data e hora todatetime("2024-01-15T10:30:00Z")
totimespan(str) Analisar string → intervalo de tempo totimespan("01:30:00")

12. Nomeação de operadores e igualdade

KQL apresenta diferenças sutis em relação à sintaxe do SQL.

Convenções de nomenclatura

Entidade Convenção Exemplo
Tabelas UpperCamelCase StormEvents, NetworkLogs
Colunas UpperCamelCase StartTime, EventType
Variáveis (let) snake_case let filtered_events = ...
Funções embutidas snake_case format_bytes(), geo_distance_2points()
Funções armazenadas UpperCamelCase .create function GetTopUsers

Operadores de igualdade

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

Ambos sort by e order by funcionam da mesma forma no `KQL` — são sinônimos. Use o que preferir, mas seja consistente.

contém vs tem

// 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. Estratégia de recuperação de erros

Quando uma primeira consulta no KQL falha, a tentação é abandonar toda a abordagem e tentar algo completamente diferente. A resposta correta é quase sempre corrigir o erro específico, não mudar de estratégia.

O padrão a ser evitado

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

O padrão correto

Query 1: extract(@"pattern", 1, col)  → Parse error (bad escaping)
Query 2: extract(@"pattern", 1, col)  → Fix the specific escaping issue → Success

Regras para recuperação de erros:

  1. Leia a mensagem de erro com atenção — ela quase sempre indica exatamente o que está errado
  2. Corrija o problema específico de sintaxe ou escape; não mude de abordagem
  3. Use a tabela de autocorreção (Seção 10) para mapear os erros às soluções
  4. Só mude de abordagem após duas tentativas frustradas de correção da mesma consulta
  5. O parse operador costuma ser mais simples do que extract() para texto estruturado:
// 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. Lista de verificação para redação de consultas

Antes de executar qualquer consulta do tipo “KQL”, verifique mentalmente:

  1. Já foi pré-filtrada? Tabelas grandes têm um | where antes de qualquer | summarize
  2. O resultado está delimitado? Consultas exploratórias terminam com | take N ou | top N
  3. As colunas dinâmicas foram convertidas? Qualquer coluna dinâmica em by/on/order by é envolvida
  4. A expressão regular tem grupos? extract_all Os padrões têm () ao redor do que você deseja capturar
  5. A cardinalidade da junção está segura? Ambos os lados foram verificados com dcount() antes da junção
  6. Apenas as colunas necessárias? Tabelas largas são | project a descartar colunas desnecessárias
  7. Literais de data e hora válidos? Usando datetime(2024-01-01) não datetime(2024) ou inteiros simples
  8. Expressões secundárias complexas? Use | extend primeiro, depois | summarize by a coluna calculada
  9. Plano de recuperação de erros? Se uma consulta falhar, corrija o erro específico — não mude de estratégia
Ver no 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

Todos os arquivos

0 arquivos

Instalar kql

Baixe e descompacte os arquivos das habilidades no diretório .claude/skills/.

Baixar ZIP

Clone o repositório e copie os arquivos da habilidade para o seu projeto.

git clone https://github.com/microsoft/skills/tree/main/.github/skills/kql # Copy SKILL.md to your .claude/skills/ directory

Copiar Copiar
Configuração rápida: Copie a pasta da habilidade para .claude/skills/ O Claude detectará e utilizará automaticamente a habilidade
Repositório microsoft/skills

Habilidades relacionadas

microservices-patterns
Tempo atualizado 29 de Junho de 2026
jpa-patterns
Tempo atualizado 30 de Junho de 2026
fabric-lakehouse
Tempo atualizado 30 de Junho de 2026
PostgreSQL Syntax Reference
Tempo atualizado 29 de Junho de 2026
OR