mcpField
K
kql

kql

github.com/microsoft数据与数据库3.2k60 次安装今天更新
代码仓库

关于 kql

KQL Mastery 是面向 Azure 数据服务用户的 Kusto Query Language 查询技能,涵盖语法、常见错误及安全查询方法。内容包含连接、正则表达式、时间序列、向量和图查询示例,并提供可在公开 help cluster 上运行的示例。

主要功能

  1. 01讲解 KQL 查询与管理命令
  2. 02说明动态类型和连接限制
  3. 03提供正则表达式与序列化模式
  4. 04介绍内存安全及结果控制
  5. 05涵盖向量、地理、图和时间序列查询

适用场景

  1. 01排查 KQL 查询错误
  2. 02构建大型表的安全分析查询
  3. 03探索地理、图或时间序列数据

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/kql确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
advanced-patterns.md
discovery-queries.md
error-recovery.md
query-templates.md

详细介绍

KQL 精通

亲自试试:此技能中的所有 ✅ 示例都可以在公共帮助群集上运行: https://help.kusto.windows.net,数据库 Samples(包含 StormEvents、SimpleGraph_Nodes/Edges、nyc_taxi 等)。

1. KQL 基础

Kusto 查询语言(KQL)是一种通过管道向前传递的查询语言,用于探索数据。它是 Azure Data Explorer(ADX)、Microsoft Fabric Real-Time Intelligence(EventHouse)、Azure Monitor Log Analytics、Microsoft Sentinel 及其他 Microsoft 数据服务的原生查询语言。

管道向前传递语法

KQL 查询由以 | 分隔的一系列运算符组成。数据从左向右流动:

StormEvents                          // start with a table
| where State == "TEXAS"             // filter rows
| summarize count() by EventType     // aggregate
| top 5 by count_ desc              // limit results
查询与管理命令

KQL 有两个执行平面:

平面起始内容示例
查询表名、let、print、datatableStormEvents | where State == "TEXAS"
管理.show、.create、.set、.drop、.alter.show tables、.show table T schema

管理命令后可以跟查询运算符(输出为表格形式),但整个请求都在管理平面上运行。你不能从查询开始,再通过管道传入管理命令。

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

不确定时:如果第一个词元以 . 开头,它就是管理命令。完整的架构探索命令目录请参阅 references/discovery-queries.md。

2. Dynamic 类型规范

KQL 的 dynamic 类型灵活,但在某些上下文中有严格限制。一个常见错误是在 summarize by、order by 或 join on 中使用 dynamic 列,却没有进行类型转换。

规则:每当在 by、on 或 order by 中使用 dynamic 类型的列时,都要对其进行显式类型转换。

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

自行修正:看到错误中出现 "is of a 'dynamic' type" 时,添加 tostring()、tolong() 或 todouble()。

3. Join 模式与注意事项

KQL join 的限制与 SQL 不同。

仅支持相等条件

KQL join 条件仅支持 ==。Join 谓词中不能使用 <、>、!= 或函数调用。

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

对于范围 join,先对值进行分桶:| extend bin_val = bin(Value, 100),然后按 bin_val 进行 join。注意:接近分桶边界的值可能会落入相邻分桶 — 为提高精度,可考虑检查相邻分桶或重叠范围。

左右属性匹配

Join 的 on 子句两侧都必须引用列实体 — 不能是表达式或聚合结果。

// ❌ 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
大型 join 前检查基数

Join 行数超过 10K 的表之前,务必检查基数。单次 E_RUNAWAY_QUERY 错误源于交叉 join 导致的行数激增(25K × 195 = 潜在的 4.8M 行)。

// 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. KQL 中的正则表达式

KQL 原生支持正则表达式 — 无需使用 Python。

extract_all 的易错之处

与 Python 的 re.findall() 不同,KQL 的 extract_all 要求正则表达式中包含捕获组:

// ❌ 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)
正则表达式工具集 — 不要退回使用 Python
函数用途示例
extract(regex, group, source)单次匹配extract(@"User '([^']+)'", 1, Msg)
extract_all(regex, source)所有匹配项(需要 ())extract_all(@"(\w+)", Text)
parse结构化提取parse Msg with * "User '" Sender "' sent" *
matches regex布尔筛选where Url matches regex @"^https?://"
replace_regex查找并替换replace_regex(Text, @"\s+", " ")

5. 序列化要求

窗口函数需要已序列化(有序)的输入。

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

需要序列化的函数:row_number()、row_cumsum()、prev()、next()、row_window_session()。

6. 内存安全查询模式

这是最常见的内存错误。原因是在未预先筛选的情况下扫描了过多数据。

安全性逐步提升
Safest ──────────────────────────────────────────────── Most dangerous
| count    | take 10    | where + summarize    | summarize (no filter)    | full scan
大型表(>1M 行)的规则
  1. 始终从 | count 开始,以了解表的大小
  2. 始终先使用 | where,再使用 | summarize——先筛选时间范围、分区键或类别
  3. 对高基数列使用 dcount() 前,必须先进行筛选
  4. 执行前检查联接基数(见第 3 节)
  5. 对多次引用的子查询使用 materialize()
// ❌ 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
遇到 E_LOW_MEMORY_CONDITION 时

查询触及的数据过多。可采取以下措施:

  • 添加 | where 筛选条件(时间范围、分区键)
  • 减少 summarize 中的 by 列数量
  • 拆分成较小的时间窗口,再合并结果
  • 探索性分析时使用 | sample 10000,而不是全表扫描
遇到 E_RUNAWAY_QUERY 时

联接或聚合产生了过多输出行。检查联接基数——一侧或两侧的数据量可能过大。

7. 结果大小控制

大型结果会拖慢分析。预防措施:

查询类型防护措施
探索性查询始终以 | take 10 或 | take 20 结尾
聚合使用 | top 20 by ...,而不是不设限制的 summarize
宽行(向量、JSON)使用 | project,仅选取需要的列
make_list() / make_set()避免用于高基数组(会生成超大的单元格)
大小未知先运行 | count

向量陷阱:包含嵌入列(1536 维浮点数组)的表每行约占 30KB。即使使用 | take 20,结果也会达到 600KB。除非确实需要向量列,否则始终使用 | project 将其排除。

8. 字符串比较严格性

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)

这种情况最常见于比较 geo_point_to_s2cell() 和 strcat() 生成的计算值。无法确定时,使用 tostring() 进行转换。

9. 高级函数

KQL 原生支持这些操作,无需使用 Python:

向量相似度
// 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
地理操作
// 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)
图查询
// 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)
时间序列
// 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_)

详细示例和模式请参阅 references/advanced-patterns.md。

10. 自我纠错查找表

遇到错误时,重试前先在此查找:

错误消息包含可能原因修复方法
is of a 'dynamic' typeby/on/order by 中使用了动态列使用 tostring()/tolong() 包装
Only equality is allowed联接条件中使用了范围谓词使用 S2/H3 单元格或 bin() 预先分桶
extractall(): matching groups正则表达式中缺少 ()添加 ():使用 @"(\w+)",而非 @"\w+"
row set must be serialized在未排序的数据上使用窗口函数在其前添加 | serialize 或 | order by
Cannot compare values of types string and string比较计算得到的字符串在两侧添加 tostring()
Failed to resolve column named 'X'列名错误或表错误运行 .show table T schema 检查列名
E_LOW_MEMORY_CONDITION查询触及的数据过多添加 | where 筛选条件、缩短时间范围、拆分步骤
E_RUNAWAY_QUERY联接/聚合产生了过多行联接前检查基数;添加预筛选条件
for each left attribute, right attribute联接的 on 子句不完整使用显式形式:on $left.X == $right.Y
needs to be bracketed标识符使用了保留字使用 ['keyword'] 语法
plugin doesn't exist此群集上没有可用的插件改用等效函数或 Python
Expected string literal in datetime()日期时间字面量中使用了裸整数使用 datetime(2024-01-01),而非 datetime(2024)
Unexpected token after bysummarize 的 by 子句中使用了复杂表达式先用 extend 扩展该表达式,再对该列执行 summarize by
not recognized / unknown operator此引擎不支持该运算符检查运算符支持情况;尝试等效写法(order by = sort by)

11. 日期时间陷阱

日期时间字面量是常见的错误来源。格式错误的字面量可能导致你转而采用完全不同的方法,而不是修复这个小问题。

字面量格式
// ❌ 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)
按年、月或小时筛选
// ❌ 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))
在 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
实用的日期时间函数
函数用途示例
bin(ts, 1h)向下取整到分桶边界bin(Timestamp, 1d)
startofmonth(ts)月份的第一天startofmonth(Timestamp)
datetime_part("hour", ts)提取组成部分datetime_part("year", Timestamp)
format_datetime(ts, fmt)格式化为字符串format_datetime(Timestamp, "yyyy-MM")
ago(1d)相对时间where Timestamp > ago(1d)
between(a .. b)范围筛选(包含边界)where Timestamp between (datetime(2024-01-01) .. datetime(2024-01-31T23:59:59))
todatetime(str)将字符串解析为日期时间todatetime("2024-01-15T10:30:00Z")
totimespan(str)将字符串解析为时间跨度totimespan("01:30:00")

12. 运算符命名与相等性

KQL 的语法与 SQL 存在一些细微差别。

命名约定
实体约定示例
表大驼峰命名法StormEvents, NetworkLogs
列大驼峰命名法StartTime, EventType
变量(let)蛇形命名法let filtered_events = ...
内置函数蛇形命名法format_bytes(), geo_distance_2points()
存储函数大驼峰命名法.create function GetTopUsers
相等运算符
// 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 与 order

在 KQL 中,sort by 和 order by 的作用完全相同——它们是别名。任选其一即可,但请保持一致。

contains 与 has
// 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. 错误恢复策略

首次 KQL 查询失败时,人们很容易放弃整个思路,转而尝试完全不同的方法。正确的做法几乎总是修复具体错误,而不是改变策略。

应避免的模式
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
正确的模式
Query 1: extract(@"pattern", 1, col)  → Parse error (bad escaping)
Query 2: extract(@"pattern", 1, col)  → Fix the specific escaping issue → Success

错误恢复规则:

  1. 仔细阅读错误消息——它几乎总会准确指出问题所在
  2. 修复具体的语法/转义问题,不要更换方法
  3. 使用自我修正表(第 10 节)将错误对应到修复方法
  4. 只有在同一查询修复失败 2 次后,才更换方法
  5. 对于结构化文本,parse 运算符通常比 extract() 更简单:
// 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. 查询编写检查清单

运行任何 KQL 查询之前,请先在脑中检查:

  1. 是否已预先筛选? 大型表应在任何 | summarize 之前使用 | where
  2. 结果是否有界? 探索性查询应以 | take N 或 | top N 结尾
  3. 动态列是否已转换类型? by/on/order by 中的任何动态列都应使用类型转换函数包装
  4. 正则表达式是否包含捕获组? extract_all 模式中,应在要捕获的内容两侧加上 ()
  5. Join 的基数是否安全? Join 前使用 dcount() 检查两侧
  6. 是否只保留需要的列? 对宽表使用 | project 删除不需要的列
  7. 日期时间字面量是否有效? 使用 datetime(2024-01-01),而不是 datetime(2024) 或裸整数
  8. 是否存在复杂的 by 表达式? 先使用 | extend,再通过计算出的列进行 | summarize by
  9. 是否有错误恢复计划? 如果查询失败,修复具体错误——不要改变策略

评论

支持 Markdown