Maîtrise de KQL
Essayez par vous-même : Tous les exemples
✅de 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 plus).
1. Bases de KQL
Kusto Query Language (KQL) est un langage de requête orienté pipe 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 d'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 par une table
| where State == "TEXAS" // filtrer les lignes
| summarize count() by EventType // agréger
| top 5 by count_ desc // limiter les résultats
Requêtes 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 l'ensemble de la requête s'exécute sur le plan de gestion. Vous ne pouvez pas commencer par une requête et pivoter vers une commande de gestion.
// ✅ FONCTIONNE — commande de gestion pivotée vers des opérateurs de requête
.show tables | project TableName | where TableName has "Events"
// ❌ FAUX — requête pivotée 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 du type dynamique
Le type dynamic de KQL est flexible mais strict dans certains contextes. Une erreur courante consiste à utiliser une colonne dynamique dans summarize by, order by ou join on sans conversion.
La règle : Chaque fois que vous utilisez une colonne de type dynamique dans by, on ou order by, enveloppez-la dans une conversion 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 jointure dynamique
StormEvents | join kind=inner (PopulationData) on $left.StormSummary == $right.State
// ✅ CORRECTION — convertir les deux côtés
StormEvents
| extend State_str = tostring(StormSummary.Details.Location)
| join kind=inner (PopulationData) on $left.State_str == $right.State
Auto-correction : Quand vous voyez "is of a 'dynamic' type" dans une erreur, ajoutez tostring(), tolong() ou todouble().
3. Modèles de jointure et pièges
Les jointures KQL ont des contraintes qui diffèrent de SQL.
Égalité uniquement
Les conditions de jointure KQL supportent uniquement ==. Pas de <, >, != ou d'appels de fonction dans les prédicats de jointure.
// ❌ 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é-segmenter dans des cellules spatiales, puis joindre sur l'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 jointures de plage, pré-segmentez les valeurs : | extend bin_val = bin(Value, 100), puis joignez sur bin_val. Remarque : les valeurs près des limites de segment peuvent atterrir dans des segments adjacents — envisagez de vérifier les segments voisins ou de chevaucher la plage pour plus de précision.
Correspondance des attributs gauche/droite
Les deux côtés d'une clause de jointure on doivent référencer uniquement des entités de colonne — 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érification de cardinalité avant les grandes jointures
Toujours vérifier la cardinalité avant de joindre des tables avec >10 000 lignes. Une explosion de cross-join était à l'origine de l'unique erreur E_RUNAWAY_QUERY (25 000 × 195 = 4,8M lignes potentielles).
// 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.
Le piège de 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)
Boîte à outils regex — ne pas se rabattre sur 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 |
Rechercher et remplacer | replace_regex(Text, @"\s+", " ") |
5. Exigences de sérialisation
Les fonctions de fenêtre ont besoin d'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. Modèles de requête sûrs pour la mémoire
L'erreur de mémoire la plus courante. Causée par l'analyse de trop de données sans pré-filtrage.
La progression de la sécurité
Plus sûr ──────────────────────────────────────────────── Plus dangereux
| count | take 10 | where + summarize | summarize (no filter) | full scan
Règles pour les grandes tables (>1M lignes)
- Commencez toujours par
| countpour comprendre la taille de la table - Toujours
| whereavant| summarize— filtrer d'abord la plage horaire, la clé de partition ou la catégorie - Ne jamais
dcount()sur des colonnes de haute cardinalité sans pré-filtrage - Vérifier la cardinalité de jointure 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 regroupement
StormEvents
| summarize dcount(EventType), count() by StartTime, State, Source
| where dcount_EventType > 1
// ✅ SÛRE — 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 horaire, clé de partition) - Réduire le nombre de colonnes
bydanssummarize - Diviser en fenêtres temporelles plus petites et fusionner les résultats
- Utiliser
| sample 10000pour le travail exploratoire au lieu des analyses complètes
Quand vous voyez E_RUNAWAY_QUERY
Une jointure ou une agrégation a produit trop de lignes de sortie. Vérifier la cardinalité de jointure — un ou les deux côtés sont trop grands.
7. Discipline de taille de résultat
Les résultats volumineux ralentissent l'analyse. Prévention :
| Type de requête | Protection |
|---|---|
| Exploratoire | Toujours terminer par \| 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 de haute cardinalité (produit des cellules énormes) |
| Taille inconnue | Exécuter \| count d'abord |
Le piège du vecteur : Les tables avec colonnes d'embedding (matrices float de dimension 1536) produisent ~30 KB par ligne. Même | take 20 produit 600 KB. Toujours | project pour exclure les colonnes vectorielles sauf si vous en avez spécifiquement besoin.
8. Rigueur de comparaison de chaînes
KQL nécessite parfois des conversions explicites lors de la comparaison de valeurs de chaîne calculées — même si 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, convertir 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)
// Segmentation spatiale pour les jointures
StormEvents | extend cell = geo_point_to_s2cell(BeginLon, BeginLat, 8)
Requêtes graphiques
// Modèle graphique 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 modèles, consultez references/advanced-patterns.md.
10. Tableau de recherche d'auto-correction
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 jointure | Pré-segmenter avec des cellules S2/H3 ou bin() |
extractall(): matching groups |
Manquement de () 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 plage horaire, diviser en étapes |
E_RUNAWAY_QUERY |
Jointure/agrégation a produit trop de lignes | Vérifier cardinalité avant jointure ; ajouter pré-filtres |
for each left attribute, right attribute |
Clause on de jointure incomplète |
Utiliser forme explicite : on $left.X == $right.Y |
needs to be bracketed |
Mot réservé utilisé comme identifiant | Utiliser syntaxe ['keyword'] |
plugin doesn't exist |
Plugin indisponible sur ce cluster | Utiliser fonction équivalente ou Python |
Expected string literal in datetime() |
Entier nu dans littéral datetime | Utiliser datetime(2024-01-01) pas datetime(2024) |
Unexpected token après by |
Expression complexe dans clause summarize by | extend l'expression d'abord, puis summarize by la colonne |
not recognized / unknown operator |
Opérateur non disponible sur ce moteur | Vérifier support de l'opérateur ; essayer équivalent (order by = sort by) |
11. Pièges des datetime
Les littéraux datetime sont une source courante d'erreurs. Un mauvais format de littéral peut aboutir à des approches complètement différentes au lieu de corriger le petit problème.
Format du littéral
// ❌ FAUX — l'année seule n'est pas un datetime valide
StormEvents | where StartTime > datetime(2007)
// ✅ JUSTE — toujours utiliser le format de date complet
StormEvents | where StartTime > datetime(2007-01-01)
Filtrer par année, mois ou heure
// ❌ FAUX — comparer la colonne datetime à un entier
StormEvents | where StartTime == 2007
// ✅ JUSTE — utiliser datetime_part() pour extraire des composants
StormEvents | where datetime_part("year", StartTime) == 2007
// ✅ AUSSI JUSTE — utiliser between avec plage datetime
StormEvents | where StartTime between (datetime(2007-01-01) .. datetime(2007-12-31T23:59:59))
Segmentation temporelle dans summarize
// Cela fonctionne, mais peut être plus difficile à lire et réutiliser dans des 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 au limite de segment | bin(Timestamp, 1d) |
startofmonth(ts) |
Premier jour du mois | startofmonth(Timestamp) |
datetime_part("hour", ts) |
Extraire le 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) |
Analyser chaîne → datetime | todatetime("2024-01-15T10:30:00Z") |
totimespan(str) |
Analyser chaîne → timespan | totimespan("01:30:00") |
12. Dénomination et égalité des opérateurs
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 jointures, 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. Utiliser celui que vous préférez, mais être 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 index)
StormEvents | where EventNarrative has "tree" // correspond aux limites des mots uniquement
// Pour correspondance exacte de préfixe/suffixe
StormEvents | where EventType startswith "Thunder"
StormEvents | where Source endswith "Spotter"
13. Stratégie de récupération après erreur
Quand une première requête KQL échoue, la tentation est d'abandonner l'approche entière 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 modèle à éviter
Requête 1 : extract(@"pattern", 1, col) → Erreur d'analyse
Requête 2 : todynamic(col) → Erreur différente
Requête 3 : parse_json(col) → Une autre erreur
Requête 4 : Script Python → Fonctionne mais 10x les tokens
Le modèle correct
Requête 1 : extract(@"pattern", 1, col) → Erreur d'analyse (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 après erreur :
- Lire attentivement le message d'erreur — il 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'auto-correction (Section 10) pour mapper les erreurs aux corrections
- Ne changer d'approche qu'après 2 tentatives échouées de correction de la même requête
- L'opérateur
parseest souvent plus simple queextract()pour du texte structuré :
// Au lieu d'une regex complexe sur TraceLogs :
// extract(@"file path: \"\"([^\"]+)\"\"", 1, Message)
// Utiliser parse pour extraction structurée (essayez sur le cluster d'aide, base 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 n'importe quelle requête KQL, vérifier mentalement :
- Pré-filtrée ? Les grandes tables ont un
| whereavant tout| summarize - Résultat limité ? Les requêtes exploratoires se terminent par
| take Nou| top N - Colonnes dynamiques converties ? 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 jointure sûre ? Les deux côtés vérifiés avec
dcount()avant jointure - Uniquement colonnes nécessaires ? Les tables larges obtiennent
| projectpour supprimer les colonnes inutiles - Littéraux datetime valides ? Utilisant
datetime(2024-01-01)pasdatetime(2024)ou entiers nus - Expressions complexes by ? Utiliser
| extendd'abord, puis| summarize byla colonne calculée - Plan de récupération après erreur ? Si une requête échoue, corriger l'erreur spécifique — ne pas changer de stratégie