{"slug":"kql","title":"kql","summary":"KQL language expertise for writing correct, efficient Kusto Query Language queries. Covers syntax gotchas, join patterns, dynamic types, datetime pitfalls, regex patterns, serialization, memory management, result-size discipline, and advanced functions (geo, vector, graph). USE T","platform":"GitHub Copilot","tags":[],"authorName":"Ciza","authorSlug":"ciza","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-08-12T21:05:25.900088Z","repo":{"url":"https://github.com/microsoft/skills","stars":3052,"forks":351,"license":"MIT","updatedAt":"2026-09-24T16:38:17Z"},"bodyHtml":"<hr>\n<h2>name: kql\ndescription: \"KQL language expertise for writing correct, efficient Kusto Query Language queries. Covers syntax gotchas, join patterns, dynamic types, datetime pitfalls, regex patterns, serialization, memory management, result-size discipline, and advanced functions (geo, vector, graph). USE THIS SKILL whenever writing, debugging, or reviewing KQL queries — even simple ones — because the gotchas section prevents the most common errors that waste tool calls and cause expensive retry cascades. Trigger on: KQL, Kusto, ADX, Azure Data Explorer, Fabric Real-Time Intelligence, EventHouse, Log Analytics, log analysis, data exploration, time series, anomaly detection, summarize, where clause, join, extend, project, let statement, parse operator, extract function, any mention of pipe-forward query syntax.\"</h2>\n<h1>KQL Mastery</h1>\n<blockquote>\n<p><strong>Try it yourself</strong>: All <code>✅</code> examples in this skill can be run against the public help cluster:\n<code>https://help.kusto.windows.net</code>, database <code>Samples</code> (contains <code>StormEvents</code>, <code>SimpleGraph_Nodes</code>/<code>Edges</code>, <code>nyc_taxi</code>, and more).</p>\n</blockquote>\n<h2>1. KQL Basics</h2>\n<p>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.</p>\n<h3>Pipe-forward syntax</h3>\n<p>KQL queries are a chain of operators separated by <code>|</code>. Data flows left to right:</p>\n<pre><code>StormEvents                          // start with a table\n| where State == \"TEXAS\"             // filter rows\n| summarize count() by EventType     // aggregate\n| top 5 by count_ desc              // limit results\n</code></pre>\n<h3>Query vs management commands</h3>\n<p>KQL has two execution planes:</p>\n<table>\n<thead>\n<tr>\n<th>Plane</th>\n<th>Starts with</th>\n<th>Examples</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Query</strong></td>\n<td>Table name, <code>let</code>, <code>print</code>, <code>datatable</code></td>\n<td><code>StormEvents \\| where State == \"TEXAS\"</code></td>\n</tr>\n<tr>\n<td><strong>Management</strong></td>\n<td><code>.show</code>, <code>.create</code>, <code>.set</code>, <code>.drop</code>, <code>.alter</code></td>\n<td><code>.show tables</code>, <code>.show table T schema</code></td>\n</tr>\n</tbody>\n</table>\n<p>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.</p>\n<pre><code>// ✅ WORKS — management command piped to query operators\n.show tables | project TableName | where TableName has \"Events\"\n\n// ❌ WRONG — query piped into management command\nStormEvents | take 5 | .show tables\n</code></pre>\n<p>When in doubt: if the first token starts with <code>.</code>, it's a management command. For a full catalog of schema exploration commands, see <code>references/discovery-queries.md</code>.</p>\n<h2>2. Dynamic Type Discipline</h2>\n<p>KQL's <code>dynamic</code> type is flexible but strict in certain contexts. A common mistake is using a dynamic column in <code>summarize by</code>, <code>order by</code>, or <code>join on</code> without casting.</p>\n<p><strong>The rule</strong>: Any time you use a dynamic-typed column in <code>by</code>, <code>on</code>, or <code>order by</code>, wrap it in an explicit cast.</p>\n<pre><code>// ❌ ERROR: \"Summarize group key ... is of a 'dynamic' type\"\nStormEvents | summarize count() by StormSummary.Details.Location\n\n// ✅ FIX\nStormEvents | summarize count() by tostring(StormSummary.Details.Location)\n</code></pre>\n<pre><code>// ❌ ERROR: \"order operator: key can't be of dynamic type\"\nStormEvents | order by StormSummary.TotalDamages desc\n\n// ✅ FIX\nStormEvents | order by tolong(StormSummary.TotalDamages) desc\n</code></pre>\n<pre><code>// ❌ ERROR in join: dynamic join key\nStormEvents | join kind=inner (PopulationData) on $left.StormSummary == $right.State\n\n// ✅ FIX — cast both sides\nStormEvents\n| extend State_str = tostring(StormSummary.Details.Location)\n| join kind=inner (PopulationData) on $left.State_str == $right.State\n</code></pre>\n<p><strong>Self-correction</strong>: When you see \"is of a 'dynamic' type\" in an error, add <code>tostring()</code>, <code>tolong()</code>, or <code>todouble()</code>.</p>\n<h2>3. Join Patterns &amp; Pitfalls</h2>\n<p>KQL joins have constraints that differ from SQL.</p>\n<h3>Equality only</h3>\n<p>KQL join conditions support <strong>only <code>==</code></strong>. No <code>&lt;</code>, <code>&gt;</code>, <code>!=</code>, or function calls in join predicates.</p>\n<pre><code>// ❌ ERROR: \"Only equality is allowed in this context\"\nStormEvents | join (nyc_taxi) on geo_distance_2points(BeginLon, BeginLat, pickup_longitude, pickup_latitude) &lt; 1000\n\n// ✅ WORKAROUND — pre-bucket into spatial cells, then join on cell ID\nStormEvents\n| extend cell = geo_point_to_s2cell(BeginLon, BeginLat, 8)\n| join kind=inner (nyc_taxi | extend cell = geo_point_to_s2cell(pickup_longitude, pickup_latitude, 8)) on cell\n</code></pre>\n<p>For range joins, pre-bin values: <code>| extend bin_val = bin(Value, 100)</code>, then join on <code>bin_val</code>. Note: values near bin boundaries may land in adjacent bins — consider checking neighboring bins or overlapping the range for precision.</p>\n<h3>Left/right attribute matching</h3>\n<p>Both sides of a join <code>on</code> clause must reference <strong>column entities only</strong> — not expressions, not aggregates.</p>\n<pre><code>// ❌ ERROR: \"for each left attribute, right attribute should be selected\"\nStormEvents | join kind=inner (PopulationData) on $left.State\n\n// ✅ FIX — specify both sides explicitly\nStormEvents | join kind=inner (PopulationData) on $left.State == $right.State\n</code></pre>\n<h3>Cardinality check before large joins</h3>\n<p><strong>Always</strong> check cardinality before joining tables with &gt;10K rows. A cross-join explosion was the source of the single <code>E_RUNAWAY_QUERY</code> error (25K × 195 = potential 4.8M rows).</p>\n<pre><code>// Before joining, check how many rows each side contributes\nStormEvents | summarize dcount(State)        // → 67 distinct states\nPopulationData | summarize dcount(State)     // → 52 — safe to join\n</code></pre>\n<h2>4. Regex in KQL</h2>\n<p>KQL handles regex natively — no need for Python.</p>\n<h3>The <code>extract_all</code> gotcha</h3>\n<p>Unlike Python's <code>re.findall()</code>, KQL's <code>extract_all</code> <strong>requires capturing groups</strong> in the regex:</p>\n<pre><code>// ❌ ERROR: \"extractall(): argument 2 must be a valid regex with [1..16] matching groups\"\nStormEvents | extend words = extract_all(@\"[a-zA-Z]{3,}\", EventNarrative)\n\n// ✅ FIX — add parentheses around the pattern\nStormEvents | extend words = extract_all(@\"([a-zA-Z]{3,})\", EventNarrative)\n</code></pre>\n<h3>Regex toolkit — don't fall back to Python</h3>\n<table>\n<thead>\n<tr>\n<th>Function</th>\n<th>Use case</th>\n<th>Example</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>extract(regex, group, source)</code></td>\n<td>Single match</td>\n<td><code>extract(@\"User '([^']+)'\", 1, Msg)</code></td>\n</tr>\n<tr>\n<td><code>extract_all(regex, source)</code></td>\n<td>All matches (needs <code>()</code>)</td>\n<td><code>extract_all(@\"(\\w+)\", Text)</code></td>\n</tr>\n<tr>\n<td><code>parse</code></td>\n<td>Structured extraction</td>\n<td><code>parse Msg with * \"User '\" Sender \"' sent\" *</code></td>\n</tr>\n<tr>\n<td><code>matches regex</code></td>\n<td>Boolean filter</td>\n<td><code>where Url matches regex @\"^https?://\"</code></td>\n</tr>\n<tr>\n<td><code>replace_regex</code></td>\n<td>Find and replace</td>\n<td><code>replace_regex(Text, @\"\\s+\", \" \")</code></td>\n</tr>\n</tbody>\n</table>\n<h2>5. Serialization Requirements</h2>\n<p>Window functions need serialized (ordered) input.</p>\n<pre><code>// ❌ ERROR: \"Function 'row_cumsum' cannot be invoked. The row set must be serialized.\"\nStormEvents\n| where State == \"TEXAS\"\n| summarize DailyCount = count() by bin(StartTime, 1d)\n| extend CumulativeCount = row_cumsum(DailyCount)\n\n// ✅ FIX — add | serialize (or | order by, which implicitly serializes)\nStormEvents\n| where State == \"TEXAS\"\n| summarize DailyCount = count() by bin(StartTime, 1d)\n| order by StartTime asc\n| extend CumulativeCount = row_cumsum(DailyCount)\n</code></pre>\n<p>Functions requiring serialization: <code>row_number()</code>, <code>row_cumsum()</code>, <code>prev()</code>, <code>next()</code>, <code>row_window_session()</code>.</p>\n<h2>6. Memory-Safe Query Patterns</h2>\n<p>The most common memory error. Caused by scanning too much data without pre-filtering.</p>\n<h3>The progression of safety</h3>\n<pre><code>Safest ──────────────────────────────────────────────── Most dangerous\n| count    | take 10    | where + summarize    | summarize (no filter)    | full scan\n</code></pre>\n<h3>Rules for large tables (&gt;1M rows)</h3>\n<ol>\n<li><strong>Always start with <code>| count</code></strong> to understand table size</li>\n<li><strong>Always <code>| where</code> before <code>| summarize</code></strong> — filter time range, partition key, or category first</li>\n<li><strong>Never <code>dcount()</code> on high-cardinality columns</strong> without pre-filtering</li>\n<li><strong>Check join cardinality</strong> before executing (see Section 3)</li>\n<li><strong>Use <code>materialize()</code></strong> for subqueries referenced multiple times</li>\n</ol>\n<pre><code>// ❌ OUT OF MEMORY — large table, no filter, many group-by columns\nStormEvents\n| summarize dcount(EventType), count() by StartTime, State, Source\n| where dcount_EventType &gt; 1\n\n// ✅ SAFE — filter first, then aggregate\nStormEvents\n| where StartTime between (datetime(2007-04-15) .. datetime(2007-04-16))\n| summarize dcount(EventType) by State, Source\n| where dcount_EventType &gt; 1\n</code></pre>\n<h3>When you see <code>E_LOW_MEMORY_CONDITION</code></h3>\n<p>The query touched too much data. Your options:</p>\n<ul>\n<li>Add <code>| where</code> filters (time range, partition key)</li>\n<li>Reduce the number of <code>by</code> columns in <code>summarize</code></li>\n<li>Break into smaller time windows and union results</li>\n<li>Use <code>| sample 10000</code> for exploratory work instead of full scans</li>\n</ul>\n<h3>When you see <code>E_RUNAWAY_QUERY</code></h3>\n<p>A join or aggregation produced too many output rows. Check join cardinality — one or both sides is too large.</p>\n<h2>7. Result Size Discipline</h2>\n<p>Large results slow down analysis. Prevention:</p>\n<table>\n<thead>\n<tr>\n<th>Query type</th>\n<th>Safeguard</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Exploratory</td>\n<td>Always end with <code>\\| take 10</code> or <code>\\| take 20</code></td>\n</tr>\n<tr>\n<td>Aggregation</td>\n<td>Use <code>\\| top 20 by ...</code> not unbounded <code>summarize</code></td>\n</tr>\n<tr>\n<td>Wide rows (vectors, JSON)</td>\n<td><code>\\| project</code> only needed columns</td>\n</tr>\n<tr>\n<td><code>make_list()</code> / <code>make_set()</code></td>\n<td>Avoid on high-cardinality groups (produces huge cells)</td>\n</tr>\n<tr>\n<td>Unknown size</td>\n<td>Run <code>\\| count</code> first</td>\n</tr>\n</tbody>\n</table>\n<p><strong>The vector trap</strong>: Tables with embedding columns (1536-dim float arrays) produce ~30KB per row. Even <code>| take 20</code> yields 600KB. Always <code>| project</code> away vector columns unless you specifically need them.</p>\n<h2>8. String Comparison Strictness</h2>\n<p>KQL sometimes requires explicit casts when comparing computed string values — even when both sides are already strings.</p>\n<pre><code>// ❌ ERROR: \"Cannot compare values of types string and string. Try adding explicit casts\"\nStormEvents | where geo_point_to_s2cell(BeginLon, BeginLat, 16) == other_cell\n\n// ✅ FIX — wrap both sides in tostring()\nStormEvents | where tostring(geo_point_to_s2cell(BeginLon, BeginLat, 16)) == tostring(other_cell)\n</code></pre>\n<p>This is most common with computed values from <code>geo_point_to_s2cell()</code> and <code>strcat()</code> comparisons. When in doubt, cast with <code>tostring()</code>.</p>\n<h2>9. Advanced Functions</h2>\n<p>KQL handles these natively — no need for Python:</p>\n<h3>Vector similarity</h3>\n<pre><code>// try it! — cosine similarity on Iris feature vectors\nlet target = pack_array(5.1, 3.5, 1.4, 0.2);\nIris\n| extend Vec = pack_array(SepalLength, SepalWidth, PetalLength, PetalWidth)\n| extend sim = series_cosine_similarity(Vec, target)\n| top 5 by sim desc\n</code></pre>\n<h3>Geo operations</h3>\n<pre><code>// Distance between two points (meters)\nStormEvents | extend dist = geo_distance_2points(BeginLon, BeginLat, EndLon, EndLat)\n\n// Spatial bucketing for joins\nStormEvents | extend cell = geo_point_to_s2cell(BeginLon, BeginLat, 8)\n</code></pre>\n<h3>Graph queries</h3>\n<pre><code>// Persistent graph model — try it on the help cluster!\ngraph(\"Simple\")\n| graph-match (src)-[e*1..3]-&gt;(dst)\n  where src.name == \"Alice\"\n  project src.name, dst.name, path_length = array_length(e)\n\n// Transient graph — build inline with make-graph\nSimpleGraph_Edges\n| make-graph source --&gt; target with SimpleGraph_Nodes on id\n| graph-match (src)-[e*1..5]-&gt;(dst)\n  where src.name == \"Alice\"\n  project src.name, dst.name, path_length = array_length(e)\n</code></pre>\n<h3>Time series</h3>\n<pre><code>// try it! — create a time series and detect anomalies\nStormEvents\n| make-series count() default=0 on StartTime step 1d\n| extend anomalies = series_decompose_anomalies(count_)\n</code></pre>\n<p>For detailed examples and patterns, consult <code>references/advanced-patterns.md</code>.</p>\n<h2>10. Self-Correction Lookup Table</h2>\n<p>When you encounter an error, look it up here before retrying:</p>\n<table>\n<thead>\n<tr>\n<th>Error message contains</th>\n<th>Likely cause</th>\n<th>Fix</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>is of a 'dynamic' type</code></td>\n<td>Dynamic column in <code>by</code>/<code>on</code>/<code>order by</code></td>\n<td>Wrap in <code>tostring()</code>/<code>tolong()</code></td>\n</tr>\n<tr>\n<td><code>Only equality is allowed</code></td>\n<td>Range predicate in join condition</td>\n<td>Pre-bucket with S2/H3 cells or <code>bin()</code></td>\n</tr>\n<tr>\n<td><code>extractall(): matching groups</code></td>\n<td>Missing <code>()</code> in regex</td>\n<td>Add <code>()</code>: <code>@\"(\\w+)\"</code> not <code>@\"\\w+\"</code></td>\n</tr>\n<tr>\n<td><code>row set must be serialized</code></td>\n<td>Window function on unsorted data</td>\n<td>Add <code>\\| serialize</code> or <code>\\| order by</code> before it</td>\n</tr>\n<tr>\n<td><code>Cannot compare values of types string and string</code></td>\n<td>Computed string comparison</td>\n<td>Add <code>tostring()</code> on both sides</td>\n</tr>\n<tr>\n<td><code>Failed to resolve column named 'X'</code></td>\n<td>Wrong column name or wrong table</td>\n<td>Run <code>.show table T schema</code> to check column names</td>\n</tr>\n<tr>\n<td><code>E_LOW_MEMORY_CONDITION</code></td>\n<td>Query touched too much data</td>\n<td>Add <code>\\| where</code> filters, reduce time range, break into steps</td>\n</tr>\n<tr>\n<td><code>E_RUNAWAY_QUERY</code></td>\n<td>Join/aggregation produced too many rows</td>\n<td>Check cardinality before joining; add pre-filters</td>\n</tr>\n<tr>\n<td><code>for each left attribute, right attribute</code></td>\n<td>Join <code>on</code> clause incomplete</td>\n<td>Use explicit form: <code>on $left.X == $right.Y</code></td>\n</tr>\n<tr>\n<td><code>needs to be bracketed</code></td>\n<td>Reserved word used as identifier</td>\n<td>Use <code>['keyword']</code> syntax</td>\n</tr>\n<tr>\n<td><code>plugin doesn't exist</code></td>\n<td>Unavailable plugin on this cluster</td>\n<td>Fall back to equivalent function or Python</td>\n</tr>\n<tr>\n<td><code>Expected string literal in datetime()</code></td>\n<td>Bare integer in datetime literal</td>\n<td>Use <code>datetime(2024-01-01)</code> not <code>datetime(2024)</code></td>\n</tr>\n<tr>\n<td><code>Unexpected token</code> after <code>by</code></td>\n<td>Complex expression in summarize by-clause</td>\n<td><code>extend</code> the expression first, then <code>summarize by</code> the column</td>\n</tr>\n<tr>\n<td><code>not recognized</code> / <code>unknown operator</code></td>\n<td>Operator not available on this engine</td>\n<td>Check operator support; try equivalent (<code>order by</code> = <code>sort by</code>)</td>\n</tr>\n</tbody>\n</table>\n<h2>11. Datetime Pitfalls</h2>\n<p>Datetime literals are a common source of errors. A wrong literal format can cascade into completely different approaches instead of fixing the small issue.</p>\n<h3>Literal format</h3>\n<pre><code>// ❌ WRONG — bare year is not a valid datetime\nStormEvents | where StartTime &gt; datetime(2007)\n\n// ✅ RIGHT — always use full date format\nStormEvents | where StartTime &gt; datetime(2007-01-01)\n</code></pre>\n<h3>Filtering by year, month, or hour</h3>\n<pre><code>// ❌ WRONG — comparing datetime column to integer\nStormEvents | where StartTime == 2007\n\n// ✅ RIGHT — use datetime_part() to extract components\nStormEvents | where datetime_part(\"year\", StartTime) == 2007\n\n// ✅ ALSO RIGHT — use between with datetime range\nStormEvents | where StartTime between (datetime(2007-01-01) .. datetime(2007-12-31T23:59:59))\n</code></pre>\n<h3>Time bucketing in summarize</h3>\n<pre><code>// This works, but can be harder to read and reuse in complex queries\nStormEvents | summarize count() by startofmonth(StartTime)\n\n// Clearer — extend first, then summarize by the computed column\nStormEvents\n| extend Month = startofmonth(StartTime)\n| summarize count() by Month\n| order by Month asc\n</code></pre>\n<h3>Useful datetime functions</h3>\n<table>\n<thead>\n<tr>\n<th>Function</th>\n<th>Purpose</th>\n<th>Example</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>bin(ts, 1h)</code></td>\n<td>Round down to bucket boundary</td>\n<td><code>bin(Timestamp, 1d)</code></td>\n</tr>\n<tr>\n<td><code>startofmonth(ts)</code></td>\n<td>First day of month</td>\n<td><code>startofmonth(Timestamp)</code></td>\n</tr>\n<tr>\n<td><code>datetime_part(\"hour\", ts)</code></td>\n<td>Extract component</td>\n<td><code>datetime_part(\"year\", Timestamp)</code></td>\n</tr>\n<tr>\n<td><code>format_datetime(ts, fmt)</code></td>\n<td>Format as string</td>\n<td><code>format_datetime(Timestamp, \"yyyy-MM\")</code></td>\n</tr>\n<tr>\n<td><code>ago(1d)</code></td>\n<td>Relative time</td>\n<td><code>where Timestamp &gt; ago(1d)</code></td>\n</tr>\n<tr>\n<td><code>between(a .. b)</code></td>\n<td>Range filter (inclusive)</td>\n<td><code>where Timestamp between (datetime(2024-01-01) .. datetime(2024-01-31T23:59:59))</code></td>\n</tr>\n<tr>\n<td><code>todatetime(str)</code></td>\n<td>Parse string → datetime</td>\n<td><code>todatetime(\"2024-01-15T10:30:00Z\")</code></td>\n</tr>\n<tr>\n<td><code>totimespan(str)</code></td>\n<td>Parse string → timespan</td>\n<td><code>totimespan(\"01:30:00\")</code></td>\n</tr>\n</tbody>\n</table>\n<h2>12. Operator Naming &amp; Equality</h2>\n<p>KQL has subtle differences from SQL syntax.</p>\n<h3>Naming conventions</h3>\n<table>\n<thead>\n<tr>\n<th>Entity</th>\n<th>Convention</th>\n<th>Example</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Tables</td>\n<td>UpperCamelCase</td>\n<td><code>StormEvents</code>, <code>NetworkLogs</code></td>\n</tr>\n<tr>\n<td>Columns</td>\n<td>UpperCamelCase</td>\n<td><code>StartTime</code>, <code>EventType</code></td>\n</tr>\n<tr>\n<td>Variables (<code>let</code>)</td>\n<td>snake_case</td>\n<td><code>let filtered_events = ...</code></td>\n</tr>\n<tr>\n<td>Built-in functions</td>\n<td>snake_case</td>\n<td><code>format_bytes()</code>, <code>geo_distance_2points()</code></td>\n</tr>\n<tr>\n<td>Stored functions</td>\n<td>UpperCamelCase</td>\n<td><code>.create function GetTopUsers</code></td>\n</tr>\n</tbody>\n</table>\n<h3>Equality operators</h3>\n<pre><code>// In where clauses, == is case-sensitive, =~ is case-insensitive\nStormEvents | where State == \"TEXAS\" | count        // exact match\nStormEvents | where State =~ \"texas\" | count        // case-insensitive\n\n// In joins, use == only\nStormEvents | join kind=inner (PopulationData) on State\n</code></pre>\n<h3>sort vs order</h3>\n<p>Both <code>sort by</code> and <code>order by</code> work identically in KQL — they are aliases. Use whichever you prefer, but be consistent.</p>\n<h3>contains vs has</h3>\n<pre><code>// contains: substring match (slower)\nStormEvents | where EventNarrative contains \"tree\"   // finds \"trees\", \"treetop\" too\n\n// has: term/word match (faster, uses index)\nStormEvents | where EventNarrative has \"tree\"        // matches word boundaries only\n\n// For exact prefix/suffix\nStormEvents | where EventType startswith \"Thunder\"\nStormEvents | where Source endswith \"Spotter\"\n</code></pre>\n<h2>13. Error Recovery Strategy</h2>\n<p>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 <strong>fix the specific error</strong>, not change strategy.</p>\n<h3>The pattern to avoid</h3>\n<pre><code>Query 1: extract(@\"pattern\", 1, col)  → Parse error\nQuery 2: todynamic(col)               → Different error  \nQuery 3: parse_json(col)              → Another error\nQuery 4: Python script                → Works but 10x tokens\n</code></pre>\n<h3>The correct pattern</h3>\n<pre><code>Query 1: extract(@\"pattern\", 1, col)  → Parse error (bad escaping)\nQuery 2: extract(@\"pattern\", 1, col)  → Fix the specific escaping issue → Success\n</code></pre>\n<p><strong>Rules for error recovery:</strong></p>\n<ol>\n<li>Read the error message carefully — it almost always tells you exactly what's wrong</li>\n<li>Fix the <strong>specific</strong> syntax/escaping issue, don't switch approaches</li>\n<li>Use the self-correction table (Section 10) to map errors to fixes</li>\n<li>Only switch approaches after 2 failed fixes of the same query</li>\n<li>The <code>parse</code> operator is often simpler than <code>extract()</code> for structured text:</li>\n</ol>\n<pre><code>// Instead of complex regex on TraceLogs:\n// extract(@\"file path: \\\"\\\"([^\\\"]+)\\\"\\\"\", 1, Message)\n\n// Use parse for structured extraction (try it on help cluster, SampleLogs db):\ncluster(\"help\").database(\"SampleLogs\").TraceLogs\n| where Message has \"file path\"\n| parse Message with * \"file path: \\\"\\\"\" FilePath \"\\\"\\\"\" *\n| project Timestamp, FilePath\n| take 5\n</code></pre>\n<h2>14. Query Writing Checklist</h2>\n<p>Before running any KQL query, mentally check:</p>\n<ol>\n<li><strong>Pre-filtered?</strong> Large tables have a <code>| where</code> before any <code>| summarize</code></li>\n<li><strong>Result bounded?</strong> Exploratory queries end with <code>| take N</code> or <code>| top N</code></li>\n<li><strong>Dynamic columns cast?</strong> Any dynamic column in <code>by</code>/<code>on</code>/<code>order by</code> is wrapped</li>\n<li><strong>Regex has groups?</strong> <code>extract_all</code> patterns have <code>()</code> around what you want to capture</li>\n<li><strong>Join cardinality safe?</strong> Both sides checked with <code>dcount()</code> before joining</li>\n<li><strong>Needed columns only?</strong> Wide tables get <code>| project</code> to drop unneeded columns</li>\n<li><strong>Datetime literals valid?</strong> Using <code>datetime(2024-01-01)</code> not <code>datetime(2024)</code> or bare integers</li>\n<li><strong>Complex by-expressions?</strong> Use <code>| extend</code> first, then <code>| summarize by</code> the computed column</li>\n<li><strong>Error recovery plan?</strong> If a query fails, fix the specific error — don't change strategy</li>\n</ol>\n","files":[{"path":"references/advanced-patterns.md","sizeBytes":20504,"isText":true},{"path":"references/discovery-queries.md","sizeBytes":6978,"isText":true},{"path":"references/error-recovery.md","sizeBytes":11123,"isText":true},{"path":"references/query-templates.md","sizeBytes":7716,"isText":true},{"path":"SKILL.md","sizeBytes":19096,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-08-12T21:52:58.871651Z","sha256":"7BC385249F28274B9F122E2BA29C62188041796315DA9BDC4C45C06F682E86B0","sizeBytes":24728},"review":null,"source":{"repositoryUrl":"https://github.com/microsoft/skills","path":".github/skills/kql","license":"MIT","commit":"23d0dac5f83f268166a17f0bc7dc6c73dc348a33","subtreeSha":"0720752B4F6215EB4D82362F78C07F8D431357AB8CF97F4B40F3215F7975DD40","lastSyncedAt":"2026-09-25T06:48:53.330584Z"},"reviewedAt":"2026-08-12T22:00:02.021776Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/microsoft/skills/tree/main/.github/skills/kql"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install microsoft-skills@llmmart"},{"target":"git","command":"git clone https://github.com/microsoft/skills.git"}]}