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éesSamples(contientStormEvents,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)
- Toujours commencer avec
| countpour comprendre la taille de la table - Toujours
| whereavant| summarize— filtrer d'abord par plage de temps, clé de partition ou catégorie - Ne jamais
dcount()sur les colonnes à haute cardinalité sans pré-filtrage - Vérifier la cardinalité de join avant d'exécuter (voir section 3)
- 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
bydanssummarize - Diviser en fenêtres de temps plus petites et unionner les résultats
- Utiliser
| sample 10000pour 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 :
- Lire attentivement le message d'erreur — il vous dit presque toujours exactement ce qui ne va pas
- Corriger le problème spécifique de syntaxe/échappement, ne pas changer d'approche
- Utiliser le tableau d'autocorrection (section 10) pour mapper les erreurs aux corrections
- Changer d'approche uniquement après 2 tentatives échouées de correction de la même requête
- L'opérateur
parseest souvent plus simple queextract()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 :
- Pré-filtré ? Les grandes tables ont un
| whereavant tout| summarize - Résultat limité ? Les requêtes exploratoires se terminent avec
| take Nou| top N - Colonnes dynamiques castées ? Toute colonne dynamique dans
by/on/order byest enveloppée - Regex a des groupes ? Les motifs
extract_allont()autour de ce que vous voulez capturer - Cardinalité de join sûre ? Les deux côtés vérifiés avec
dcount()avant joindre - Colonnes nécessaires uniquement ? Les tables larges obtiennent
| projectpour supprimer les colonnes non nécessaires - Littéraux datetime valides ? Utiliser
datetime(2024-01-01)pasdatetime(2024)ou entiers nus - Expressions by complexes ? Utiliser
| extendd'abord, puis| summarize byla colonne calculée - Plan de récupération d'erreur ? Si une requête échoue, corriger l'erreur spécifique — ne pas changer de stratégie