关于 kql
KQL Mastery 是面向 Azure 数据服务用户的 Kusto Query Language 查询技能,涵盖语法、常见错误及安全查询方法。内容包含连接、正则表达式、时间序列、向量和图查询示例,并提供可在公开 help cluster 上运行的示例。
主要功能
- 01讲解 KQL 查询与管理命令
- 02说明动态类型和连接限制
- 03提供正则表达式与序列化模式
- 04介绍内存安全及结果控制
- 05涵盖向量、地理、图和时间序列查询
适用场景
- 01排查 KQL 查询错误
- 02构建大型表的安全分析查询
- 03探索地理、图或时间序列数据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ microsoft/ skills --skill kql安装后在 Claude Code 中输入/kql确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/advanced-patterns.mddiscovery-queries.mderror-recovery.mdquery-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、datatable | StormEvents | 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 行)的规则
- 始终从
| count开始,以了解表的大小 - 始终先使用
| where,再使用| summarize——先筛选时间范围、分区键或类别 - 对高基数列使用
dcount()前,必须先进行筛选 - 执行前检查联接基数(见第 3 节)
- 对多次引用的子查询使用
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' type | by/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 by | summarize 的 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
错误恢复规则:
- 仔细阅读错误消息——它几乎总会准确指出问题所在
- 修复具体的语法/转义问题,不要更换方法
- 使用自我修正表(第 10 节)将错误对应到修复方法
- 只有在同一查询修复失败 2 次后,才更换方法
- 对于结构化文本,
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 查询之前,请先在脑中检查:
- 是否已预先筛选? 大型表应在任何
| summarize之前使用| where - 结果是否有界? 探索性查询应以
| take N或| top N结尾 - 动态列是否已转换类型?
by/on/order by中的任何动态列都应使用类型转换函数包装 - 正则表达式是否包含捕获组?
extract_all模式中,应在要捕获的内容两侧加上() - Join 的基数是否安全? Join 前使用
dcount()检查两侧 - 是否只保留需要的列? 对宽表使用
| project删除不需要的列 - 日期时间字面量是否有效? 使用
datetime(2024-01-01),而不是datetime(2024)或裸整数 - 是否存在复杂的 by 表达式? 先使用
| extend,再通过计算出的列进行| summarize by - 是否有错误恢复计划? 如果查询失败,修复具体错误——不要改变策略