option

Rédigez des requêtes Kusto Query Language correctes et efficaces en abordant la syntaxe, les jointures, les types dynamiques, les pièges liés aux dates et heures, les expressions régulières, la sérialisation, la gestion de la mémoire et les fonctions avancées.

...Développer tout
12
Heure mise à jour 10 septembre 2026

KQL Maîtrise

Essayez par vous-même : tous les exemples de cette compétence peuvent être exécutés sur le cluster d'aide public : https://help.kusto.windows.net, base de données Samples (contient StormEvents, SimpleGraph_Nodes/Edges, nyc_taxi, et bien plus encore).

1. Notions de base sur le langage de requêteKQL

Le langage de requête Kusto (KQL) est un langage de requête de type « pipe-forward » destiné à l’exploration des données. Il s’agit du langage de requête natif d’Azure Data Explorer (ADX), de Microsoft Fabric Real-Time Intelligence (EventHouse), d’Azure Monitor Log Analytics, de Microsoft Sentinel et d’autres services de données Microsoft.

Les requêtes en syntaxe « pipe-forward »

KQL sont constituées d’une chaîne d’opérateurs séparés par |. Les données circulent de gauche à droite :

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

Commandes de requête et commandes de gestion

KQL comporte deux plans d’exécution :

Plan Commence par Exemples
Requête Nom de la table, let, print, datatable StormEvents | where State == "TEXAS"
Commandes de gestion .show, .create, .set, .drop, .alter .show tables, .show table T schema

Les commandes de gestion peuvent être suivies d’opérateurs de requête (le résultat s’affiche sous forme de tableau), mais la requête entière s’exécute sur le plan de gestion. Vous ne pouvez pas commencer par une requête et la rediriger vers une commande de gestion.

// ✅ 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 cas de doute : si le premier token commence par ., il s'agit d'une commande de gestion. Pour consulter le catalogue complet des commandes d'exploration du schéma, voir references/discovery-queries.md.

2. Discipline des types dynamiques

Le dynamic est flexible mais stricte dans certains contextes. Une erreur courante consiste à utiliser une colonne dynamique dans summarize by, order by, ou join on sans conversion de type.

La règle : chaque fois que vous utilisez une colonne de type dynamique dans by, on, ou order by, encadrez-la d’un cast explicite.

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

Autocorrection : lorsque vous voyez « est d'un type "dynamique" » dans une erreur, ajoutez tostring(), tolong(), ou todouble().

3. Modèles de jointure et pièges

KQL Les jointures ont des contraintes différentes de celles du SQL.

Égalité uniquement

KQL Les conditions de jointure par égalité ne prennent en charge que l'==e. Aucune <, >, !=ni appels de fonction dans les prédicats de jointure.

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

Pour les jointures par plage, pré-regroupez les valeurs : | extend bin_val = bin(Value, 100), puis effectuez la jointure sur bin_val. Remarque : les valeurs proches des limites de bin peuvent se retrouver dans des bins adjacents — pensez à vérifier les bins voisins ou à faire chevaucher l’intervalle pour plus de précision.

Correspondance des attributs gauche/droite

Les deux côtés d’une clause de jointure on doivent faire référence uniquement à des entités de colonne — ni à des expressions, ni à des agrégats.

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

Vérification de la cardinalité avant les jointures volumineuses

Vérifiez toujours la cardinalité avant de joindre des tables comportant plus de 10 000 lignes. Une explosion de jointure croisée était à l’origine de l’erreur unique E_RUNAWAY_QUERY (25 000 × 195 = 4,8 millions de lignes potentielles).

// 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. Expressions régulières dans l’KQL

KQL gère nativement les expressions régulières — pas besoin de Python.

Le extract_all piège

Contrairement à Python re.findall(), l'KQL extract_all nécessite l’utilisation de groupes de capture dans l’expression régulière :

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

Boîte à outils d’expressions régulières — ne vous rabattez pas sur Python

Fonction Cas d'utilisation Exemple
extract(regex, group, source) Correspondance unique extract(@"User '([^']+)'", 1, Msg)
extract_all(regex, source) Toutes les correspondances (nécessite ()) extract_all(@"(\w+)", Text)
parse Extraction structurée parse Msg with * "User '" Sender "' sent" *
matches regex Filtre booléen where Url matches regex @"^https?://"
replace_regex Recherche et remplacement replace_regex(Text, @"\s+", " ")

5. Exigences de sérialisation

Les fonctions de fenêtre nécessitent des données d'entrée sérialisées (ordonnées).

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

Fonctions nécessitant une sérialisation : row_number(), row_cumsum(), prev(), next(), row_window_session().

6. Modèles de requêtes sans risque pour la mémoire

L'erreur de mémoire la plus courante. Elle est causée par l'analyse d'un volume trop important de données sans filtrage préalable.

Les niveaux de sécurité

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

Règles pour les grandes tables (> 1 million de lignes)

  1. Commencez toujours par « | count » pour connaître la taille de la table
  2. Effectuez toujours une «| where » avant une «| summarize » — filtrez d’abord la plage temporelle, la clé de partition ou la catégorie
  3. Ne jamais effectuer d’dcount() sur des colonnes à cardinalité élevée sans filtrage préalable
  4. Vérifiez la cardinalité des jointures avant l’exécution (voir section 3)
  5. Utilisez l’materialize() pour les sous-requêtes référencées plusieurs fois
// ❌ 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

Lorsque vous voyez E_LOW_MEMORY_CONDITION

« La requête a porté sur un volume de données trop important. Vos options :

  • Ajoutez | where des filtres (plage de temps, clé de partition)
  • Réduire le nombre de by colonnes de la summarize
  • Divisez en fenêtres temporelles plus petites et fusionnez les résultats
  • Utiliser | sample 10000 pour le travail exploratoire plutôt que des analyses complètes

Lorsque vous constatez que E_RUNAWAY_QUERY

qu’une jointure ou une agrégation a produit un nombre trop élevé de lignes de sortie, vérifiez la cardinalité de la jointure : l’un des côtés, voire les deux, est trop grand.

7. Maîtrise de la taille des résultats

Des résultats volumineux ralentissent l’analyse. Prévention :

Type de requête Mesure de prévention
Exploratoire Toujours terminer par | take 10 ou | take 20
Agrégation Utilisation | top 20 by ... pas de données illimitées summarize
Lignes larges (vecteurs, JSON) | project uniquement les colonnes nécessaires
make_list() / make_set() À éviter sur les groupes à cardinalité élevée (génère des cellules très volumineuses)
Taille inconnue Exécuter | count en premier

Le piège des vecteurs : les tables comportant des colonnes d'intégration (tableaux flottants de dimension 1 536) génèrent environ 30 Ko par ligne. Même | take 20 , cela représente 600 Ko. Évitez toujours | project les colonnes vectorielles, sauf si vous en avez spécifiquement besoin.

8. Stricteur de la comparaison de chaînes

KQL nécessite parfois des transtypages explicites lors de la comparaison de valeurs de chaînes calculées — même lorsque les deux côtés sont déjà des chaînes.

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

C’est particulièrement fréquent avec les valeurs calculées à partir de geo_point_to_s2cell() et strcat() . En cas de doute, effectuez un transtypage avec tostring().

9. Fonctions avancées

KQL gère cela de manière native — pas besoin de Python :

Similitude vectorielle

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

Opérations géographiques

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

Requêtes sur les graphes

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

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

Pour des exemples et des modèles détaillés, consultez references/advanced-patterns.md.

10. Table de correction automatique

Lorsque vous rencontrez une erreur, consultez cette section avant de réessayer :

Le message d'erreur contient Cause probable Solution
is of a 'dynamic' type Colonne dynamique dans by/on/order by Envelopper dans tostring()/tolong()
Only equality is allowed Prédicat de plage dans la condition de jointure Pré-regroupement avec des cellules S2/H3 ou bin()
extractall(): matching groups Manquant () dans l’expression régulière Ajouter (): @"(\w+)" pas @"\w+"
row set must be serialized Fonction de fenêtre sur des données non triées Ajouter | serialize ou | order by avant celle-ci
Cannot compare values of types string and string Comparaison de chaînes calculées Ajouter tostring() des deux côtés
Failed to resolve column named 'X' Nom de colonne ou table incorrect Exécuter .show table T schema pour vérifier les noms de colonnes
E_LOW_MEMORY_CONDITION La requête porte sur un volume de données trop important Ajouter | where des filtres, réduisez la plage temporelle, divisez en étapes
E_RUNAWAY_QUERY La jointure/agrégation a généré trop de lignes Vérifier la cardinalité avant la jointure ; ajouter des pré-filtres
for each left attribute, right attribute Clause de jointure on incomplète Utiliser la forme explicite : on $left.X == $right.Y
needs to be bracketed Mot réservé utilisé comme identifiant Utilisez la ['keyword'] syntaxe
plugin doesn't exist Plugin indisponible sur ce cluster Recourir à une fonction équivalente ou à Python
Expected string literal in datetime() Entier nu dans un littéral de date-heure Utiliser datetime(2024-01-01) pas datetime(2024)
Unexpected token après by Expression complexe dans la clause « by » de summarize extend l'expression d'abord, puis summarize by la colonne
not recognized / unknown operator Opérateur non disponible sur ce moteur Vérifiez la prise en charge de l'opérateur ; essayez une alternative (order by = sort by)

11. Pièges liés aux dates et heures

Les littéraux de date-heure sont une source fréquente d’erreurs. Un format de littéral incorrect peut entraîner des approches complètement différentes au lieu de résoudre le petit problème.

Format du littéral

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

Filtrage par année, mois ou heure

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

Regroupement temporel dans « 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

Fonctions de date et d’heure utiles

Fonction Objectif Exemple
bin(ts, 1h) Arrondir à la limite inférieure de la tranche bin(Timestamp, 1d)
startofmonth(ts) Premier jour du mois startofmonth(Timestamp)
datetime_part("hour", ts) Extraire la composante datetime_part("year", Timestamp)
format_datetime(ts, fmt) Formater en chaîne de caractères format_datetime(Timestamp, "yyyy-MM")
ago(1d) Temps relatif where Timestamp > ago(1d)
between(a .. b) Filtre de plage (inclusif) where Timestamp between (datetime(2024-01-01) .. datetime(2024-01-31T23:59:59))
todatetime(str) Analyser la chaîne → date-heure todatetime("2024-01-15T10:30:00Z")
totimespan(str) Analyser une chaîne de caractères → durée totimespan("01:30:00")

12. Dénomination des opérateurs et égalité

KQL présente de légères différences par rapport à la syntaxe SQL.

Conventions de nommage

Entité Convention Exemple
Tables UpperCamelCase StormEvents, NetworkLogs
Colonnes UpperCamelCase StartTime, EventType
Variables (let) snake_case let filtered_events = ...
Fonctions intégrées snake_case format_bytes(), geo_distance_2points()
Fonctions stockées UpperCamelCase .create function GetTopUsers

Opérateurs d'égalité

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

Les deux sort by et order by fonctionnent de manière identique dansKQL : ce sont des alias. Utilisez celui que vous préférez, mais restez cohérent.

« contient » vs « possède »

// 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. Stratégie de récupération en cas d’erreur

Lorsqu'une première requête d'KQL échoue, on est tenté d'abandonner l'approche dans son ensemble et d'essayer quelque chose de complètement différent. La bonne réaction consiste presque toujours à corriger l'erreur spécifique, et non à changer de stratégie.

Le modèle à éviter

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

Le modèle correct

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

Règles pour la récupération après erreur :

  1. Lisez attentivement le message d’erreur — il vous indique presque toujours exactement ce qui ne va pas
  2. Corrigez le problème de syntaxe ou d’échappement en question, ne changez pas d’approche
  3. Utilisez le tableau d'autocorrection (section 10) pour associer les erreurs aux solutions
  4. Ne changez d’approche qu’après deux tentatives infructueuses de correction de la même requête
  5. L' parse opérateur est souvent plus simple que extract() pour le texte structuré :
// 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. Liste de contrôle pour la rédaction des requêtes

Avant d’exécuter toute requête « KQL », vérifiez mentalement :

  1. Pré-filtrée ? Les tables volumineuses disposent d’un | where avant toute | summarize
  2. Résultat borné ? Les requêtes exploratoires se terminent par | take N ou | top N
  3. Les colonnes dynamiques sont-elles converties ? Toute colonne dynamique dans by/on/order by est encadrée
  4. L'expression régulière comporte-t-elle des groupes ? extract_all Les motifs comportent () autour de ce que vous souhaitez capturer
  5. La cardinalité des jointures est-elle garantie ? Les deux côtés sont vérifiés avec dcount() avant la jointure
  6. Uniquement les colonnes nécessaires ? Les tables larges sont | project supprimer les colonnes inutiles
  7. Les littéraux de date-heure sont-ils valides ? Utilisation de datetime(2024-01-01) pas datetime(2024) ni d’entiers bruts
  8. Expressions secondaires complexes ? Utilisez | extend d’abord, puis | summarize by la colonne calculée
  9. Plan de récupération en cas d’erreur ? Si une requête échoue, corrigez l’erreur spécifique — ne changez pas de stratégie
Voir sur 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

Tous les fichiers

0 fichiers

Installer kql

Téléchargez et décompressez les fichiers de compétences dans votre répertoire .claude/skills/.

Télécharger le ZIP

Clonez le dépôt et copiez les fichiers de compétence dans votre projet.

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

Copier Copier
Configuration rapide: Copiez le dossier de la compétence dans .claude/skills/ Claude détectera automatiquement la compétence et l'utilisera

Compétences similaires

microservices-patterns
Heure mise à jour 29 juin 2026
jpa-patterns
Heure mise à jour 30 juin 2026
fabric-lakehouse
Heure mise à jour 30 juin 2026
prisma-expert
Heure mise à jour 29 juin 2026
OR