kql

Expertise linguistique KQL pour l'écriture de requêtes Kusto Query Language correctes et efficaces. Couvre les pièges de syntaxe, les patterns de join, les types dynamiques, les écueils liés aux datetime, les patterns regex, la sérialisation, la gestion mémoire, la discipline sur la taille des résultats et les fonctions avancées (géo, vecteur, graphe). UTILISER CETTE COMPÉTENCE dès qu'il s'agit d'écrire, déboguer ou réviser des requêtes KQL — même simples — car la section des pièges permet d'éviter les erreurs les plus courantes qui gaspillent des appels d'outils et provoquent des cascades de relances coûteuses. Déclencheurs : KQL, Kusto, ADX, Azure Data Explorer, Fabric Real-Time Intelligence, EventHouse, Log Analytics, analyse de logs, exploration de données, séries temporelles, détection d'anomalies, summarize, clause where, join, extend, project, instruction let, opérateur parse, fonction extract, toute mention de syntaxe de requête pipe-forward.

npx skills add https://github.com/microsoft/skills --skill kql

Maîtrise de KQL

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

1. Bases de KQL

Kusto Query Language (KQL) est un langage de requête pipe-forward pour explorer les données. C'est le langage de requête natif pour Azure Data Explorer (ADX), Microsoft Fabric Real-Time Intelligence (EventHouse), Azure Monitor Log Analytics, Microsoft Sentinel et autres services de données Microsoft.

Syntaxe pipe-forward

Les requêtes KQL sont une chaîne d'opérateurs séparés par |. Les données circulent de gauche à droite :

StormEvents                          // commencer avec une table
| where State == "TEXAS"             // filtrer les lignes
| summarize count() by EventType     // agréger
| top 5 by count_ desc              // limiter les résultats

Requête vs commandes de gestion

KQL a deux plans d'exécution :

Plan Commence par Exemples
Requête Nom de table, let, print, datatable StormEvents \| where State == "TEXAS"
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 (la sortie est tabulaire), mais la demande entière s'exécute sur le plan de gestion. Vous ne pouvez pas commencer par une requête et faire un pipe vers une commande de gestion.

// ✅ FONCTIONNE — commande de gestion en pipe vers les opérateurs de requête
.show tables | project TableName | where TableName has "Events"

// ❌ FAUX — requête en pipe vers une commande de gestion
StormEvents | take 5 | .show tables

En cas de doute : si le premier token commence par ., c'est une commande de gestion. Pour un catalogue complet des commandes d'exploration de schéma, voir references/discovery-queries.md.

2. Discipline de type dynamique

Le type dynamic de KQL est flexible mais strict dans certains contextes. Une erreur courante est d'utiliser une colonne dynamique dans summarize by, order by ou join on sans cast.

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

// ❌ ERREUR : "Summarize group key ... is of a 'dynamic' type"
StormEvents | summarize count() by StormSummary.Details.Location

// ✅ CORRECTION
StormEvents | summarize count() by tostring(StormSummary.Details.Location)
// ❌ ERREUR : "order operator: key can't be of dynamic type"
StormEvents | order by StormSummary.TotalDamages desc

// ✅ CORRECTION
StormEvents | order by tolong(StormSummary.TotalDamages) desc
// ❌ ERREUR dans join : clé de join dynamique
StormEvents | join kind=inner (PopulationData) on $left.StormSummary == $right.State

// ✅ CORRECTION — caster les deux côtés
StormEvents
| extend State_str = tostring(StormSummary.Details.Location)
| join kind=inner (PopulationData) on $left.State_str == $right.State

Autocorrection : Quand vous voyez "is of a 'dynamic' type" dans une erreur, ajoutez tostring(), tolong() ou todouble().

3. Motifs et pièges de join

Les joins de KQL ont des contraintes qui diffèrent de SQL.

Égalité uniquement

Les conditions de join de KQL supportent uniquement ==. Pas de <, >, != ou d'appels de fonction dans les prédicats de join.

// ❌ ERREUR : "Only equality is allowed in this context"
StormEvents | join (nyc_taxi) on geo_distance_2points(BeginLon, BeginLat, pickup_longitude, pickup_latitude) < 1000

// ✅ CONTOURNEMENT — pré-partitionner en cellules spatiales, puis joindre par ID de cellule
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 joins de plage, pré-partitionner les valeurs : | extend bin_val = bin(Value, 100), puis joindre sur bin_val. Remarque : les valeurs près des limites de partition peuvent tomber dans des partitions adjacentes — pensez à vérifier les partitions voisines ou chevaucher la plage pour la précision.

Correspondance d'attributs gauche/droite

Les deux côtés d'une clause de join on doivent référencer uniquement des entités de colonnes — pas d'expressions, pas d'agrégats.

// ❌ ERREUR : "for each left attribute, right attribute should be selected"
StormEvents | join kind=inner (PopulationData) on $left.State

// ✅ CORRECTION — spécifier les deux côtés explicitement
StormEvents | join kind=inner (PopulationData) on $left.State == $right.State

Vérifier la cardinalité avant les grands joins

Toujours vérifier la cardinalité avant de joindre des tables avec >10K lignes. Une explosion de cross-join était à l'origine de l'unique erreur E_RUNAWAY_QUERY (25K × 195 = potentiellement 4,8M lignes).

// Avant de joindre, vérifier combien de lignes chaque côté contribue
StormEvents | summarize dcount(State)        // → 67 états distincts
PopulationData | summarize dcount(State)     // → 52 — sûr pour joindre

4. Regex dans KQL

KQL gère les regex nativement — pas besoin de Python.

La piège extract_all

Contrairement à re.findall() de Python, extract_all de KQL nécessite des groupes de capture dans la regex :

// ❌ ERREUR : "extractall(): argument 2 must be a valid regex with [1..16] matching groups"
StormEvents | extend words = extract_all(@"[a-zA-Z]{3,}", EventNarrative)

// ✅ CORRECTION — ajouter des parenthèses autour du motif
StormEvents | extend words = extract_all(@"([a-zA-Z]{3,})", EventNarrative)

Kit d'outils regex — ne pas revenir à Python

Fonction Cas d'usage 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 Chercher et remplacer replace_regex(Text, @"\s+", " ")

5. Exigences de sérialisation

Les fonctions de fenêtre nécessitent une entrée sérialisée (ordonnée).

// ❌ ERREUR : "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)

// ✅ CORRECTION — ajouter | serialize (ou | order by, qui sérialise implicitement)
StormEvents
| where State == "TEXAS"
| summarize DailyCount = count() by bin(StartTime, 1d)
| order by StartTime asc
| extend CumulativeCount = row_cumsum(DailyCount)

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

6. Motifs de requête sûrs pour la mémoire

L'erreur mémoire la plus courante. Causée par l'analyse de trop de données sans pré-filtrage.

La progression de sécurité

Plus sûr ────────────────────────────────────────────── Plus dangereux
| count    | take 10    | where + summarize    | summarize (sans filtre)    | scan complet

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

  1. Toujours commencer avec | count pour comprendre la taille de la table
  2. Toujours | where avant | summarize — filtrer d'abord par plage de temps, clé de partition ou catégorie
  3. Ne jamais dcount() sur les colonnes à haute cardinalité sans pré-filtrage
  4. Vérifier la cardinalité de join avant d'exécuter (voir section 3)
  5. Utiliser materialize() pour les sous-requêtes référencées plusieurs fois
// ❌ HORS DE LA MÉMOIRE — grande table, pas de filtre, nombreuses colonnes de groupement
StormEvents
| summarize dcount(EventType), count() by StartTime, State, Source
| where dcount_EventType > 1

// ✅ SÛR — filtrer d'abord, puis agréger
StormEvents
| where StartTime between (datetime(2007-04-15) .. datetime(2007-04-16))
| summarize dcount(EventType) by State, Source
| where dcount_EventType > 1

Quand vous voyez E_LOW_MEMORY_CONDITION

La requête a touché trop de données. Vos options :

  • Ajouter des filtres | where (plage de temps, clé de partition)
  • Réduire le nombre de colonnes by dans summarize
  • Diviser en fenêtres de temps plus petites et unionner les résultats
  • Utiliser | sample 10000 pour le travail exploratoire au lieu de scans complets

Quand vous voyez E_RUNAWAY_QUERY

Un join ou une agrégation a produit trop de lignes en sortie. Vérifier la cardinalité de join — un ou les deux côtés sont trop grands.

7. Discipline sur la taille des résultats

Les grands résultats ralentissent l'analyse. Prévention :

Type de requête Protection
Exploratoire Toujours terminer avec \| take 10 ou \| take 20
Agrégation Utiliser \| top 20 by ... pas summarize sans limite
Lignes larges (vecteurs, JSON) \| project uniquement les colonnes nécessaires
make_list() / make_set() Éviter sur les groupes à haute cardinalité (produit des cellules énormes)
Taille inconnue Exécuter \| count d'abord

Le piège du vecteur : Les tables avec colonnes d'embedding (tableaux de floats de 1536 dimensions) produisent ~30KB par ligne. Même | take 20 donne 600KB. Toujours | project loin des colonnes de vecteurs sauf si vous en avez spécifiquement besoin.

8. Strictesse de comparaison de chaînes

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

// ❌ ERREUR : "Cannot compare values of types string and string. Try adding explicit casts"
StormEvents | where geo_point_to_s2cell(BeginLon, BeginLat, 16) == other_cell

// ✅ CORRECTION — envelopper les deux côtés dans tostring()
StormEvents | where tostring(geo_point_to_s2cell(BeginLon, BeginLat, 16)) == tostring(other_cell)

C'est plus courant avec les valeurs calculées de geo_point_to_s2cell() et les comparaisons strcat(). En cas de doute, caster avec tostring().

9. Fonctions avancées

KQL les gère nativement — pas besoin de Python :

Similarité vectorielle

// essayez ! — similarité cosinus sur les vecteurs de caractéristiques Iris
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éospatiales

// Distance entre deux points (mètres)
StormEvents | extend dist = geo_distance_2points(BeginLon, BeginLat, EndLon, EndLat)

// Partitionnement spatial pour joins
StormEvents | extend cell = geo_point_to_s2cell(BeginLon, BeginLat, 8)

Requêtes de graphe

// Modèle de graphe persistant — essayez sur le cluster d'aide !
graph("Simple")
| graph-match (src)-[e*1..3]->(dst)
  where src.name == "Alice"
  project src.name, dst.name, path_length = array_length(e)

// Graphe transitoire — construire en ligne avec 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 temporelles

// essayez ! — créer une série temporelle et détecter les anomalies
StormEvents
| make-series count() default=0 on StartTime step 1d
| extend anomalies = series_decompose_anomalies(count_)

Pour des exemples détaillés et des motifs, consulter references/advanced-patterns.md.

10. Tableau de recherche d'autocorrection

Quand vous rencontrez une erreur, cherchez-la ici avant de réessayer :

Le message d'erreur contient Cause probable Correction
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 join Pré-partitionner avec cellules S2/H3 ou bin()
extractall(): matching groups () manquantes dans la regex Ajouter () : @"(\w+)" pas @"\w+"
row set must be serialized Fonction de fenêtre sur données non triées Ajouter \| serialize ou \| order by avant
Cannot compare values of types string and string Comparaison de chaîne calculée Ajouter tostring() des deux côtés
Failed to resolve column named 'X' Mauvais nom de colonne ou mauvaise table Exécuter .show table T schema pour vérifier les noms
E_LOW_MEMORY_CONDITION Requête a touché trop de données Ajouter des filtres \| where, réduire la plage de temps, diviser en étapes
E_RUNAWAY_QUERY Join/agrégation a produit trop de lignes Vérifier la cardinalité avant joindre ; ajouter des pré-filtres
for each left attribute, right attribute Clause on de join incomplète Utiliser la forme explicite : on $left.X == $right.Y
needs to be bracketed Mot réservé utilisé comme identifiant Utiliser la syntaxe ['keyword']
plugin doesn't exist Plugin indisponible sur ce cluster Revenir à la fonction équivalente ou Python
Expected string literal in datetime() Entier nu dans le littéral datetime 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érifier le support de l'opérateur ; essayer l'équivalent (order by = sort by)

11. Pièges datetime

Les littéraux datetime sont une source courante d'erreurs. Un mauvais format de littéral peut se transformer en approches complètement différentes au lieu de corriger le petit problème.

Format de littéral

// ❌ FAUX — année seule n'est pas un datetime valide
StormEvents | where StartTime > datetime(2007)

// ✅ CORRECT — toujours utiliser le format de date complet
StormEvents | where StartTime > datetime(2007-01-01)

Filtrer par année, mois ou heure

// ❌ FAUX — comparer colonne datetime à entier
StormEvents | where StartTime == 2007

// ✅ CORRECT — utiliser datetime_part() pour extraire les composants
StormEvents | where datetime_part("year", StartTime) == 2007

// ✅ AUSSI CORRECT — utiliser between avec plage datetime
StormEvents | where StartTime between (datetime(2007-01-01) .. datetime(2007-12-31T23:59:59))

Partitionnement de temps dans summarize

// Ça fonctionne, mais peut être plus difficile à lire et réutiliser dans les requêtes complexes
StormEvents | summarize count() by startofmonth(StartTime)

// Plus clair — extend d'abord, puis summarize par la colonne calculée
StormEvents
| extend Month = startofmonth(StartTime)
| summarize count() by Month
| order by Month asc

Fonctions datetime utiles

Fonction Objectif Exemple
bin(ts, 1h) Arrondir vers le bas jusqu'à la limite de partition bin(Timestamp, 1d)
startofmonth(ts) Premier jour du mois startofmonth(Timestamp)
datetime_part("hour", ts) Extraire un composant datetime_part("year", Timestamp)
format_datetime(ts, fmt) Formater en chaîne 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) Parser chaîne → datetime todatetime("2024-01-15T10:30:00Z")
totimespan(str) Parser chaîne → timespan totimespan("01:30:00")

12. Dénomination d'opérateurs et égalité

KQL a des différences subtiles avec la syntaxe SQL.

Conventions de dénomination

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é

// Dans les clauses where, == est sensible à la casse, =~ ne l'est pas
StormEvents | where State == "TEXAS" | count        // correspondance exacte
StormEvents | where State =~ "texas" | count        // insensible à la casse

// Dans les joins, utiliser == uniquement
StormEvents | join kind=inner (PopulationData) on State

sort vs order

À la fois sort by et order by fonctionnent de manière identique dans KQL — ce sont des alias. Utilisez celui que vous préférez, mais soyez cohérent.

contains vs has

// contains : correspondance de sous-chaîne (plus lent)
StormEvents | where EventNarrative contains "tree"   // trouve "trees", "treetop" aussi

// has : correspondance de terme/mot (plus rapide, utilise l'index)
StormEvents | where EventNarrative has "tree"        // correspond uniquement aux limites de mot

// Pour préfixe/suffixe exact
StormEvents | where EventType startswith "Thunder"
StormEvents | where Source endswith "Spotter"

13. Stratégie de récupération d'erreur

Quand une première requête KQL échoue, la tentation est d'abandonner toute l'approche et d'essayer quelque chose de complètement différent. La bonne réponse est presque toujours de corriger l'erreur spécifique, pas de changer de stratégie.

Le motif à éviter

Requête 1 : extract(@"pattern", 1, col)  → Erreur parse
Requête 2 : todynamic(col)               → Erreur différente  
Requête 3 : parse_json(col)              → Autre erreur
Requête 4 : Script Python                → Fonctionne mais 10x tokens

Le motif correct

Requête 1 : extract(@"pattern", 1, col)  → Erreur parse (mauvais échappement)
Requête 2 : extract(@"pattern", 1, col)  → Corriger le problème d'échappement spécifique → Succès

Règles pour la récupération d'erreur :

  1. Lire attentivement le message d'erreur — il vous dit presque toujours exactement ce qui ne va pas
  2. Corriger le problème spécifique de syntaxe/échappement, ne pas changer d'approche
  3. Utiliser le tableau d'autocorrection (section 10) pour mapper les erreurs aux corrections
  4. Changer d'approche uniquement après 2 tentatives échouées de correction de la même requête
  5. L'opérateur parse est souvent plus simple que extract() pour le texte structuré :
// Au lieu de regex complexe sur TraceLogs :
// extract(@"file path: \"\"([^\"]+)\"\"", 1, Message)

// Utiliser parse pour l'extraction structurée (essayez sur le cluster d'aide, db SampleLogs) :
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 de rédaction de requête

Avant d'exécuter toute requête KQL, vérifier mentalement :

  1. Pré-filtré ? Les grandes tables ont un | where avant tout | summarize
  2. Résultat limité ? Les requêtes exploratoires se terminent avec | take N ou | top N
  3. Colonnes dynamiques castées ? Toute colonne dynamique dans by/on/order by est enveloppée
  4. Regex a des groupes ? Les motifs extract_all ont () autour de ce que vous voulez capturer
  5. Cardinalité de join sûre ? Les deux côtés vérifiés avec dcount() avant joindre
  6. Colonnes nécessaires uniquement ? Les tables larges obtiennent | project pour supprimer les colonnes non nécessaires
  7. Littéraux datetime valides ? Utiliser datetime(2024-01-01) pas datetime(2024) ou entiers nus
  8. Expressions by complexes ? Utiliser | extend d'abord, puis | summarize by la colonne calculée
  9. Plan de récupération d'erreur ? Si une requête échoue, corriger l'erreur spécifique — ne pas changer de stratégie

Skills similaires