opción

Escribe consultas correctas y eficientes en el lenguaje de consultas Kusto, con información sobre sintaxis, uniones, tipos dinámicos, problemas relacionados con la fecha y la hora, expresiones regulares, serialización, gestión de la memoria y funciones avanzadas.

...Expandir todo
12
Tiempo actualizado 10 de septiembre de 2026

KQL Dominio

Pruébalo tú mismo: Todos los ejemplos de esta habilidad se pueden ejecutar en el clúster público de ayuda: https://help.kusto.windows.net, base de datos Samples (contiene StormEvents, SimpleGraph_Nodes/Edges, nyc_taxi, y más).

1. Conceptos básicos deKQL

El lenguaje de consulta Kusto (KQL) es un lenguaje de consulta de tuberías hacia adelante para explorar datos. Es el lenguaje de consulta nativo de Azure Data Explorer (ADX), Microsoft Fabric Real-Time Intelligence (EventHouse), Azure Monitor Log Analytics, Microsoft Sentinel y otros servicios de datos de Microsoft.

Las consultas con sintaxis de tuberías hacia adelante

KQL consisten en una cadena de operadores separados por |. Los datos fluyen de izquierda a derecha:

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 frente a comandos de administración

KQL tiene dos planos de ejecución:

Plano Comienza con Ejemplos
Consulta Nombre de la tabla, let, print, datatable StormEvents | where State == "TEXAS"
Gestión .show, .create, .set, .drop, .alter .show tables, .show table T schema

Los comandos de gestión pueden ir seguidos de operadores de consulta (el resultado es tabular), pero toda la solicitud se ejecuta en el plano de gestión. No se puede empezar con una consulta y enlazarla a un comando de gestión.

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

En caso de duda: si el primer token empieza por ., se trata de un comando de gestión. Para consultar el catálogo completo de comandos de exploración de esquemas, véase references/discovery-queries.md.

2. Disciplina de tipos dinámicos

El dynamic es flexible, pero estricto en determinados contextos. Un error habitual es utilizar una columna dinámica en summarize by, order by, o join on sin realizar una conversión de tipo.

La regla: siempre que utilices una columna de tipo dinámico en by, on, o order by, envuélvela en una conversión 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

Autocorrección: cuando veas «es de tipo “dinámico”» en un error, añade tostring(), tolong(), o todouble().

3. Patrones y dificultades de las uniones

KQL Las uniones tienen restricciones que difieren de las de SQL.

Solo igualdad

KQL Las condiciones de unión de igualdad solo admiten «==». No <, >, !=, ni llamadas a funciones en los predicados de unión.

// ❌ 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 las uniones por rango, se deben agrupar previamente los valores: | extend bin_val = bin(Value, 100), y a continuación se realiza la unión en bin_val. Nota: los valores cercanos a los límites de los intervalos pueden acabar en intervalos adyacentes; considera comprobar los intervalos vecinos o solapar el rango para mayor precisión.

Coincidencia de atributos a la izquierda/derecha

Ambos lados de una cláusula de unión on deben hacer referencia únicamente a entidades de columna —ni a expresiones, ni a 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

Comprobación de cardinalidad antes de uniones de gran tamaño

Comprueba siempre la cardinalidad antes de unir tablas con más de 10 000 filas. Una explosión de unión cruzada fue la causa del único E_RUNAWAY_QUERY (25 000 × 195 = 4,8 millones de filas potenciales).

// 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. Las expresiones regulares en KQL

KQL gestiona expresiones regulares de forma nativa, sin necesidad de Python.

El extract_all trampa

A diferencia de Python re.findall(), en KQL extract_all requiere grupos de captura en la expresión 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 herramientas de expresiones regulares: no recurras a Python

Función Caso de uso Ejemplo
extract(regex, group, source) Coincidencia única extract(@"User '([^']+)'", 1, Msg)
extract_all(regex, source) Todas las coincidencias (requiere ()) extract_all(@"(\w+)", Text)
parse Extracción estructurada parse Msg with * "User '" Sender "' sent" *
matches regex Filtro booleano where Url matches regex @"^https?://"
replace_regex Buscar y sustituir replace_regex(Text, @"\s+", " ")

5. Requisitos de serialización

Las funciones de ventana necesitan una 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)

Funciones que requieren serialización: row_number(), row_cumsum(), prev(), next(), row_window_session().

6. Patrones de consulta seguros para la memoria

El error de memoria más habitual. Se debe a escanear demasiados datos sin filtrarlos previamente.

La progresión de la seguridad

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

Reglas para tablas grandes (>1 millón de filas)

  1. Empieza siempre con `| count` para conocer el tamaño de la tabla
  2. | wherea siempre antes de | summarize: filtra primero el intervalo de tiempo, la clave de partición o la categoría
  3. Nunca realices un «dcount()» en columnas de alta cardinalidad sin filtrar previamente
  4. Comprueba la cardinalidad de la unión antes de ejecutarla (véase la sección 3)
  5. Utiliza «materialize()» para subconsultas a las que se hace referencia varias veces
// ❌ 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

Cuando veas E_LOW_MEMORY_CONDITION

La consulta ha afectado a demasiados datos. Tus opciones:

  • Añade | where filtros (intervalo de tiempo, clave de partición)
  • Reducir el número de by columnas en summarize
  • Divide en ventanas de tiempo más pequeñas y une los resultados
  • Utilizar | sample 10000 para el trabajo exploratorio en lugar de escaneos completos

Cuando veas que E_RUNAWAY_QUERY

que una unión o una agregación ha generado demasiadas filas de salida, comprueba la cardinalidad de la unión: uno de los lados, o ambos, es demasiado grande.

7. Disciplina en el tamaño de los resultados

Los resultados de gran tamaño ralentizan el análisis. Prevención:

Tipo de consulta Medida de protección
Exploratoria Termina siempre con | take 10 o | take 20
Agregación Uso | top 20 by ... no ilimitado summarize
Filas anchas (vectores, JSON) | project solo las columnas necesarias
make_list() / make_set() Evitar en grupos de alta cardinalidad (genera celdas enormes)
Tamaño desconocido Ejecutar | count primero

La trampa del vector: las tablas con columnas incrustadas (matrices flotantes de 1536 dimensiones) generan unos 30 KB por fila. Incluso | take 20 se obtienen 600 KB. Evita siempre | project elimina las columnas vectoriales a menos que las necesites específicamente.

8. Rigurosidad en la comparación de cadenas

KQL a veces requiere conversiones explícitas al comparar valores de cadena calculados, incluso cuando ambos lados ya son cadenas.

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

Esto es más habitual con valores calculados a partir de geo_point_to_s2cell() y strcat() . En caso de duda, realiza la conversión con tostring().

9. Funciones avanzadas

KQL las gestiona de forma nativa; no hace falta usar Python:

Similitud vectorial

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

Operaciones 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 de 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)

Series temporales

// 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 ver ejemplos y patrones detallados, consulta references/advanced-patterns.md.

10. Tabla de autocorrección

Cuando se produzca un error, consúltelo aquí antes de volver a intentarlo:

El mensaje de error contiene Causa probable Solución
is of a 'dynamic' type Columna dinámica en by/on/order by Encerrar entre tostring()/tolong()
Only equality is allowed Predicado de rango en la condición de unión Pre-bucket con celdas S2/H3 o bin()
extractall(): matching groups Falta () en expresión regular Añadir (): @"(\w+)" no @"\w+"
row set must be serialized Función de ventana en datos sin ordenar Añadir | serialize o | order by antes de ella
Cannot compare values of types string and string Comparación de cadenas calculadas Añadir tostring() a ambos lados
Failed to resolve column named 'X' Nombre de columna o tabla incorrectos Ejecutar .show table T schema para comprobar los nombres de las columnas
E_LOW_MEMORY_CONDITION La consulta ha procesado demasiados datos Añade | where filtros, reducir el intervalo de tiempo o dividir en pasos
E_RUNAWAY_QUERY La unión o la agregación ha generado demasiadas filas Comprueba la cardinalidad antes de la unión; añade filtros previos
for each left attribute, right attribute La cláusula de unión on cláusula de unión incompleta Utilizar la forma explícita: on $left.X == $right.Y
needs to be bracketed Se ha utilizado una palabra reservada como identificador Utiliza ['keyword'] sintaxis
plugin doesn't exist Complemento no disponible en este clúster Recurrir a una función equivalente o a Python
Expected string literal in datetime() Número entero sin formato en un literal de fecha y hora Utilizar datetime(2024-01-01) no datetime(2024)
Unexpected token después de by Expresión compleja en la cláusula «by» de «summarize» extend la expresión primero, luego summarize by la columna
not recognized / unknown operator Operador no disponible en este motor Comprueba la compatibilidad del operador; prueba con un equivalente (order by = sort by)

11. Errores con las fechas y horas

Los literales de fecha y hora son una fuente habitual de errores. Un formato de literal incorrecto puede dar lugar a enfoques completamente diferentes en lugar de solucionar el pequeño problema.

Formato del 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)

Filtrado por año, mes u 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))

Agrupación de datos temporales en «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

Funciones útiles de fecha y hora

Función Finalidad Ejemplo
bin(ts, 1h) Redondear hacia abajo al límite del intervalo bin(Timestamp, 1d)
startofmonth(ts) Primer día del mes startofmonth(Timestamp)
datetime_part("hour", ts) Extraer componente datetime_part("year", Timestamp)
format_datetime(ts, fmt) Formatear como cadena format_datetime(Timestamp, "yyyy-MM")
ago(1d) Tiempo relativo where Timestamp > ago(1d)
between(a .. b) Filtro de rango (incluido) where Timestamp between (datetime(2024-01-01) .. datetime(2024-01-31T23:59:59))
todatetime(str) Analizar cadena → fecha y hora todatetime("2024-01-15T10:30:00Z")
totimespan(str) Analizar cadena → intervalo de tiempo totimespan("01:30:00")

12. Nombres de operadores e igualdad

KQL presenta sutiles diferencias con respecto a la sintaxis de SQL.

Convenciones de nomenclatura

Entidad Convención Ejemplo
Tablas UpperCamelCase StormEvents, NetworkLogs
Columnas Mayúscula-CamelCase StartTime, EventType
Variables (let) snake_case let filtered_events = ...
Funciones integradas snake_case format_bytes(), geo_distance_2points()
Funciones almacenadas UpperCamelCase .create function GetTopUsers

Operadores de igualdad

// 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 frente a order

Ambos sort by y order by funcionan de forma idéntica en KQL: son sinónimos. Utiliza el que prefieras, pero sé coherente.

«contains» frente a «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. Estrategia de recuperación ante errores

Cuando falla una primera consulta de «KQL», la tentación es abandonar todo el enfoque e intentar algo completamente diferente. La respuesta correcta es casi siempre corregir el error específico, no cambiar de estrategia.

El patrón que hay que evitar

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

El patrón correcto

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

Reglas para la recuperación ante errores:

  1. Lee atentamente el mensaje de error: casi siempre te indica exactamente qué es lo que falla
  2. Corrige el problema específico de sintaxis o de escape; no cambies de enfoque
  3. Utiliza la tabla de autocorrección (sección 10) para relacionar los errores con sus soluciones
  4. Cambia de enfoque solo después de dos intentos fallidos de corregir la misma consulta
  5. El parse operador suele ser más sencillo que extract() para el texto estructurado:
// 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 comprobación para la redacción de consultas

Antes de ejecutar cualquier consulta KQL, comprueba mentalmente:

  1. ¿Se ha filtrado previamente? Las tablas grandes tienen un | where antes de cualquier | summarize
  2. ¿El resultado está delimitado? Las consultas exploratorias terminan con | take N o | top N
  3. ¿Se ha convertido el tipo de las columnas dinámicas? Cualquier columna dinámica de by/on/order by se envuelve
  4. ¿La expresión regular tiene grupos? extract_all Los patrones tienen () alrededor de lo que quieres capturar
  5. ¿Es segura la cardinalidad de la unión? Se comprueban ambos lados con dcount() antes de la unión
  6. ¿Solo las columnas necesarias? Las tablas anchas se | project eliminar las columnas innecesarias
  7. ¿Son válidos los literales de fecha y hora? Usando datetime(2024-01-01) no datetime(2024) ni números enteros sin formato
  8. ¿Expresiones secundarias complejas? Utiliza | extend primero, y luego | summarize by la columna calculada
  9. ¿Plan de recuperación de errores? Si una consulta falla, corrige el error específico; no cambies de estrategia
Ver en 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 los archivos

0 archivos

Instalar kql

Descarga y descomprime los archivos de habilidades en tu directorio .claude/skills/.

Descargar ZIP

Clona el repositorio y copia los archivos de la habilidad a tu proyecto.

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

Copiar Copiar
Configuración rápida: Copia la carpeta de la habilidad en .claude/skills/ Claude detectará y utilizará automáticamente la habilidad
Repositorio microsoft/skills

Habilidades relacionadas

microservices-patterns
Tiempo actualizado 29 de junio de 2026
jpa-patterns
Tiempo actualizado 30 de junio de 2026
fabric-lakehouse
Tiempo actualizado 30 de junio de 2026
prisma-expert
Tiempo actualizado 29 de junio de 2026
OR