DQL¶
DQL(Debug Query Language)是观测云平台的核心查询语言,专为高效查询和分析时间序列数据、日志数据、事件数据等设计。DQL 结合了 SQL 的语义表达和 PromQL 的语法结构,旨在为用户提供一种灵活且强大的查询工具。
本文档将帮助您快速理解 DQL 的基本语法和设计思路,并通过示例展示如何编写 DQL 查询。
基本查询结构¶
DQL 的基本查询结构如下:
namespace[index]::datasource[:select-clause] [{where-clause}] [time-expr] [group-by-clause] [having-clause] [order-by-clause] [limit-clause] [sorder-by-clause] [slimit-clause] [soffset-clause]
执行顺序¶
DQL 查询的执行顺序非常重要,它决定了查询的语义和性能。
-
数据筛选:根据 namespace::datasource、where-clause、time-expr 筛选数据
- 确定数据源
- 应用 WHERE 条件过滤原始数据行
- 应用时间范围筛选
- 在此阶段尽早过滤数据,提高后续处理效率
-
时间聚合:如果 time-expr 包含 rollup,先执行 rollup 逻辑
- Rollup 函数在时间维度上对数据进行预处理
- 对于 Counter 类型指标,通常会计算速率或增量而不是直接使用原始值
- 对于 Gauge 类型指标,可能使用 last、avg 等聚合函数
-
分组聚合:执行 group-by-clause 分组,并在分组内执行 select-clause 中的聚合函数
- 根据 BY 子句的表达式将数据分组
- 在每个分组内计算聚合函数(sum、count、avg、max、min 等)
- 如果同时有时间窗口,会形成二维数据结构
-
分组筛选:执行 having-clause 筛选聚合分组
- HAVING 子句作用于聚合后的结果
- 可以使用聚合函数的结果进行筛选
- 这是与 WHERE 子句的关键区别
-
非聚合函数:执行 select-clause 中的非聚合函数
- 处理不需要聚合的表达式和函数
- 对聚合结果进行进一步计算
-
组内排序:执行 order-by-clause、limit-clause 对分组内数据排序分页
- ORDER BY 在每个分组内部独立执行
- LIMIT 限制每个分组返回的数据行数
-
组间排序:执行 sorder-by-clause、slimit-clause、soffset-clause 对分组排序分页
- SORDER BY 对分组本身进行排序
- 需要对分组结果进行降维(如使用 max、avg、last 等函数)
- SLIMIT 限制返回的分组数量
完整示例¶
让我们通过一个完整的例子来理解 DQL 的结构:
M("production")::cpu:(avg(usage) as avg_usage, max(usage) as max_usage) {host =~ 'web-.*', usage > 50} [1h::5m] BY host, env HAVING avg_usage > 60 ORDER BY time DESC LIMIT 100 SORDER BY avg_usage DESC SLIMIT 10
这个查询的含义是:
- 命名空间:M(指标数据)
- 索引:
production
- 数据源:
cpu
- 选择字段:
usage
字段的平均值和最大值 - 时间范围:过去 1 小时,按 5 分钟聚合
- 过滤条件:主机名以 web 开头且 CPU 使用率大于 50%
- 分组:按主机名和环境分组
- 分组筛选:平均 CPU 使用率大于 60%
- 排序:按时间降序排列,每组最多 100 条
- 组间排序:按平均使用率降序排列,最多 10 个分组
命名空间(namespace)¶
命名空间用于区分不同类型的数据,每种数据类型都有其特定的查询方式和存储策略。DQL 支持查询多种业务数据类型:
命名空间 | 说明 | 典型用途 |
---|---|---|
M | Metric,时序指标数据 | CPU 使用率、内存使用量、请求计数等 |
L | Logging,日志数据 | 应用日志、系统日志、错误日志等 |
O | Object,基础设施对象数据 | 服务器信息、容器信息、网络设备等 |
OH | History object,对象历史数据 | 服务器配置变更历史、性能指标历史等 |
CO | Custom object,自定义对象数据 | 业务自定义的对象信息 |
COH | History custom object,自定义对象历史数据 | 自定义对象的历史变更记录 |
N | Network,网络数据 | 网络流量、DNS 查询、HTTP 请求等 |
T | Trace,链路调用数据 | 分布式追踪、调用链分析等 |
P | Profile,调用剖析数据 | 性能剖析、CPU 火焰图等 |
R | RUM,用户访问数据 | 前端性能、用户行为分析等 |
E | Event,事件数据 | 告警事件、部署事件、系统事件等 |
UE | Unrecover Event,未恢复事件数据 | 未解决的告警和事件 |
索引(index)¶
索引是 DQL 查询优化的重要机制,可以理解为传统数据库中的表或分区。在单个命名空间下,系统可能会因为数据来源、数据量、访问模式等因素对数据进行拆分,以提升查询性能和管理效率。
索引的作用¶
- 性能优化:通过索引将数据分散存储,减少单次查询的数据扫描量
- 数据隔离:不同业务、环境或时间范围的数据可以存储在不同的索引中
- 权限管理:可以为不同索引设置不同的访问权限
- 生命周期管理:不同索引可以设置不同的数据保留策略
索引的命名规则¶
- 索引名称必须显式声明,不支持使用通配符或正则表达式进行匹配
- 索引名称通常体现数据的业务属性,如:
production
、staging
、web-logs
、api-logs
- 默认索引名为
default
,当不显式指定索引时使用
基本语法¶
// 使用默认索引(当不指定时自动使用 default)
M::cpu // 等价于 M("default")::cpu
L::nginx // 等价于 L("default")::nginx
// 指定单个索引
M("production")::cpu // 查询 production 索引中的 CPU 指标
L("web-logs")::nginx // 查询 web-logs 索引中的 Nginx 日志
// 多索引查询(同时查询多个索引的数据)
M("production", "staging")::cpu // 查询生产和测试环境的 CPU 指标
L("web-logs", "api-logs")::nginx // 查询 Web 和 API 日志
索引与性能¶
合理使用索引可以显著提升查询性能:
- 精确索引:明确知道数据在哪个索引中时,直接指定该索引
- 多索引查询:当需要跨多个索引查询时,使用多索引语法而不是使用通配符
- 避免全索引扫描:尽量通过索引和 WHERE 条件的组合来减少数据扫描范围
兼容语法(不推荐)¶
由于历史原因,DQL 还支持在 where 子句中指定索引,但不推荐使用:
应用示例¶
// 查询生产环境的 CPU 使用率
M("production")::cpu:(avg(usage)) [1h] BY host
// 查询生产和测试环境的对比
M("production", "staging")::cpu:(avg(usage)) [1h] BY index, host
// 分析 Web 服务器日志
L("web-logs")::nginx:(count(*)) {status >= 400} [1h] BY status
// 对比 Web 和 API 服务器错误率
L("web-logs", "api-logs")::*:(count(*)) {status >= 500} [1h] BY index
数据源(datasource)¶
数据源指定查询的具体数据来源,可以是数据集名称、通配符模式、正则表达式或子查询。
基础数据源¶
不同命名空间中数据源的定义不同:
命名空间 | 数据源类型 | 示例 |
---|---|---|
M | 指标集(measurement) | cpu, memory, network |
L | 数据源(source) | nginx, tomcat, java-app |
O | 基础设施对象分类 | host, container, process |
T | 服务名(service) | user-service, order-service |
R | RUM 数据类型 | session, view, resource, error |
数据源语法¶
指定数据源名称¶
通配符匹配¶
正则表达式匹配¶
M::re('cpu.*'):(usage) // 查询以 cpu 开头的指标
L::re('web.*'):(count(*)) // 查询以 web 开头的日志
T::re('.*-service'):(traces) // 查询以 -service 结尾的服务
子查询数据源¶
子查询是 DQL 中实现复杂分析的重要功能,它允许将一个查询的结果作为另一个查询的数据源。这种嵌套查询机制支持多层级的分析需求。
执行机制¶
子查询的执行遵循以下原则:
- 串行执行:内层子查询先执行,完成后将其结果作为外层查询的数据源
- 结果封装:子查询结果会被封装成临时表结构,供外层查询使用
- 命名空间混合:子查询支持不同命名空间的混合查询,实现跨数据类型分析
- 性能考虑:子查询会增加计算复杂度,需要合理设计查询逻辑
基本语法¶
执行过程¶
以一个典型的子查询为例:
执行过程为:
-
内层子查询:
L::*:(count(*)) {level = 'error'} BY app_id
- 扫描所有日志数据
- 筛选出错误级别的日志
- 按 app_id 分组统计错误数量
- 生成临时表:
app_id | count(*)
-
外层查询:
L::(...):(count_distinct(app_id))
- 以子查询结果为数据源
- 统计有多少个不同的 app_id
- 最终结果:有错误的应用数量
应用示例¶
// 统计有错误的应用数量
L::(L::*:(count(*)) {level = 'error'} BY app_id):(count_distinct(app_id))
// 分析高 CPU 使用率的服务器
M::(M::cpu:(avg(usage)) [1h] BY host {avg(usage) > 80}):(count(host))
// 先找出错误率超过 1% 的服务端点,然后统计受影响的服务数量
M::(M::http_requests:(sum(request_count), sum(error_count)) [1h] BY service, endpoint
{sum(error_count) / sum(request_count) > 0.01}
):(count(service))
Select 子句(select-clause)¶
Select 子句用于指定查询要返回的字段或表达式,是 DQL 查询中最基本且最重要的部分之一。
字段选择¶
基本语法¶
字段名规则¶
字段名有以下几种书写形式:
-
直接写名字:适用于普通标识符
- ✅
message
- ✅
host_name
- ✅
response_time
- ✅
-
反撇号包裹:适用于包含特殊字符或关键字的字段名
- ✅
message
- ✅
limit
- ✅
host-name
- ✅
column with spaces
- ✅
-
避免的写法:单引号和双引号包裹的是字符串,不是字段名
- ❌
'message'
- ❌
"message"
- ❌
JSON 字段提取¶
当数据字段包含 JSON 格式的内容时,可以使用一个 JSON Path 语法子集提取内部字段的数据。
基本语法¶
JSON Path 语法¶
- 点号取对象属性:
.field_name
- 方括号取对象属性:
["key"]
(适用于包含空格或特殊字符的键) - 数组索引取值:
[index]
应用示例¶
假设有以下 JSON 日志数据:
{
"message": "User login attempt",
"request": {
"method": "POST",
"path": "/api/login",
"headers": {
"user-agent": "Mozilla/5.0",
"content-type": "application/json"
},
"body": {
"username": "john.doe",
"password": "***",
"permissions": ["read", "write", "admin"]
}
},
"response": {
"status": 200,
"time": 156,
"data": [
{"id": 1, "name": "user1"},
{"id": 2, "name": "user2"}
]
}
}
// 提取请求方法
L::auth_logs:(message@request.method)
// 提取请求路径
L::auth_logs:(message@request.path)
// 提取用户名
L::auth_logs:(message@request.body.username)
// 提取响应状态
L::auth_logs:(message@response.status)
// 提取 User-Agent(包含连字符,需要用方括号)
L::auth_logs:(message@request.headers["user-agent"])
// 提取权限数组第一个元素
L::auth_logs:(message@request.body.permissions[0])
// 提取响应数据第一个对象的 name
L::auth_logs:(message@response.data[0].name)
// 统计不同请求方法的数量
L::auth_logs:(count(*)) [1h] BY message@request.method
// 分析响应时间分布
L::auth_logs:(avg(message@response.time), max(message@response.time)) [1h] BY message@request.method
// 提取多个字段
L::auth_logs:(
message,
message@request.method as method,
message@request.path as path,
message@response.status as status,
message@response.time as response_time
) {message@response.status >= 400} [1h]
计算字段¶
表达式计算¶
支持基本的算术运算:
// 单位转换(毫秒转秒)
L::nginx:(response_time / 1000) as response_time_seconds
// 计算百分比
M::memory:(used / total * 100) as usage_percentage
// 复合计算
M::network:((bytes_in + bytes_out) / 1024 / 1024) as total_traffic_mb
函数计算¶
支持各种聚合和转换函数:
// 聚合函数
M::cpu:(max(usage), min(usage), avg(usage)) [1h] BY host
// 转换函数
L::logs:(int(response_time) as response_time_seconds)
L::logs:(floor(response_time) as response_time_seconds)
别名¶
为字段或表达式指定别名,使结果更易读和方便后续引用。
基本语法¶
应用示例¶
// 简单别名
M::cpu:(avg(usage) as avg_usage, max(usage) as max_usage) [1h] BY host
// 表达式别名
M::memory:((used / total) * 100 as usage_percent) [1h] BY host
// 函数别名
L::logs:(count(*) as error_count) {level = 'error'} [1h] BY service
// JSON 提取别名
L::api_logs:(
message@request.method as http_method,
message@response.status as http_status,
message@response.time as response_time_ms
) [1h]
使用技巧¶
在 DQL 中,聚合函数的结果可以直接使用原字段名引用,这减少了别名的使用:
M::cpu:(max(usage)) [1h] BY host
// 结果会包含 max(usage) 列,可以直接在后续直接使用 usage 列名拿到子查询结果的 `max(usage)` 列:
M::(M::cpu:(max(usage)) [1h] BY host):(max(usage)) { usage > 80 }
但是当有多个聚合函数使用同一字段时,必须使用别名才能在后续正确区分:
时间表达式(time-expr)¶
时间表达式是 DQL 的核心特性之一,用于指定查询的时间区间、聚合时间窗口和 Rollup 聚合函数。
基本语法¶
时间范围¶
绝对时间戳¶
相对时间¶
支持多种时间单位,可以混合使用:
[1h] // 过去 1 小时到现在
[1h:5m] // 从过去 1 小时到过去 5 分钟
[1h30m] // 过去 1 小时 30 分钟
[2h15m30s] // 过去 2 小时 15 分钟 30 秒
时间单位¶
单位 | 说明 | 示例 |
---|---|---|
s | 秒 | 30s |
m | 分钟 | 5m |
h | 小时 | 2h |
d | 天 | 7d |
w | 周 | 4w |
y | 年 | 1y |
预设时间范围¶
提供常用的时间范围关键字:
关键字 | 说明 | 时间范围 |
---|---|---|
TODAY | 今天 | 从今天 0 点到现在 |
YESTERDAY | 昨天 | 从昨天 0 点到今天 0 点 |
THIS WEEK | 本周 | 从本周一 0 点到现在 |
LAST WEEK | 上周 | 从上周一 0 点到本周一 0 点 |
THIS MONTH | 本月 | 从本月 1 号 0 点到现在 |
LAST MONTH | 上月 | 从上月 1 号 0 点到本月 1 号 0 点 |
[TODAY] // 今天的数据
[YESTERDAY] // 昨天的数据
[THIS WEEK] // 本周的数据
[LAST WEEK] // 上周的数据
[THIS MONTH] // 本月的数据
[LAST MONTH] // 上月的数据
当使用时间范围关键字时请确认空间的时区设置是否正确,需严格按照用户请求的时区进行换算。
时间窗口聚合¶
时间窗口将数据按指定的时间间隔进行分组聚合,返回结果中的 time 列表示每个时间窗口的开始时间。
单个时间窗口¶
整个时间范围聚合为一个值:
查询结果:
时间窗口聚合¶
按时间间隔分组聚合:
查询结果:
{
"columns": ["time", "max(usage_total)"],
"values": [
[1721059200000, 37.46],
[1721058600000, 34.12],
[1721058000000, 33.81],
[1721057400000, 30.92],
[1721058000000, 34.53],
[1721057400000, 36.11]
]
}
Rollup 函数¶
Rollup 函数是 DQL 中一个重要的预处理步骤,它先于分组聚合执行,用于对原始时间序列数据进行预处理。
执行时序¶
Rollup 在查询执行流程中的位置:
执行机制¶
Rollup 的执行过程分为两个阶段:
- 逐时间线处理:对每个独立的时间序列单独应用 Rollup 函数
- 聚合计算:在 Rollup 处理后的结果上执行分组聚合
应用场景¶
Rollup 函数的一个典型应用场景为 Counter 指标处理。
对于 Prometheus 的 Counter 类型指标,直接使用原始值进行聚合是没有意义的,因为 Counter 是单调递增的。需要先逐个时间线计算增长率,然后再进行聚合。
*问题示例:假设有两个服务器的请求计数器:
{
"host": "web-server-01",
"data": [
{"time": "2024-07-15 08:25:00", "request_count": 150},
{"time": "2024-07-15 08:20:00", "request_count": 140},
{"time": "2024-07-15 08:15:00", "request_count": 130},
{"time": "2024-07-15 08:10:00", "request_count": 120},
{"time": "2024-07-15 08:05:00", "request_count": 110},
{"time": "2024-07-15 08:00:00", "request_count": 100}
]
}
{
"host": "web-server-02",
"data": [
{"time": "2024-07-15 08:25:00", "request_count": 250},
{"time": "2024-07-15 08:20:00", "request_count": 240},
{"time": "2024-07-15 08:15:00", "request_count": 230},
{"time": "2024-07-15 08:10:00", "request_count": 220},
{"time": "2024-07-15 08:05:00", "request_count": 210},
{"time": "2024-07-15 08:00:00", "request_count": 200}
]
}
直接聚合的问题:
- web-server-01 的 request_count 起始值是 100
- web-server-02 的 request_count 起始值是 200
- 虽然两个服务器的请求速率相同(都是每 5 分钟 10 个请求),但绝对值不同
使用 Rollup 的解决方案:
执行过程:
-
Rollup 阶段(在每个时间线上分别执行):
- web-server-01: rate([100, 110, 120, 130, 140, 150]) = 2 请求/分钟
- web-server-02: rate([200, 210, 220, 230, 240, 250]) = 2 请求/分钟
-
聚合阶段:
- sum([2, 2]) = 4 请求/分钟
函数类型¶
常见的 Rollup 函数包括:
函数类型 | 说明 | 适用场景 |
---|---|---|
rate() |
计算增长率 | Counter 类型指标 |
increase() |
计算增长量 | Counter 类型指标 |
last() |
取最后一个值 | Gauge 类型指标 |
avg() |
计算平均值 | 数据平滑 |
max() |
取最大值 | 峰值分析 |
min() |
取最小值 | 谷值分析 |
但几乎所有返回单值的聚合函数都可以使用,这里就不再列出全部的函数列表。
默认 Rollup¶
如果没有显式指定 Rollup 函数,DQL 默认不进行 Rollup 计算,PromQL 的默认 Rollup 是 last,所以如果您正在计算的是 Prometheus 指标,请务必理解这个差异,并手动指定 Rollup 函数。
应用示例¶
// 计算所有服务器的总请求速率
M::http_requests:(sum(request_count)) [rate]
// 计算错误率
M::http_requests:(
sum(error_count) as errors,
sum(request_count) as requests
) [rate] BY service
时间窗口灵活语法¶
DQL 支持多种时间窗口的简写格式,使查询的书写更加方便。
简写格式¶
[1h] // 只指定时间范围
[1h::5m] // 时间范围 + 聚合步长
[1h:5m] // 开始时间 + 结束时间
[1h:5m:1m] // 开始 + 结束 + 步长
[1h:5m:1m:avg] // 完整格式
[::5m] // 只指定聚合步长
[:::sum] // 只指定 rollup 函数
[sum] // 只指定 rollup 函数(最简形式)
筛选条件(where-clause)¶
筛选条件用于过滤数据行,只保留满足条件的数据进行后续处理。
基本语法¶
多个条件之间可以用逗号、AND、OR、&&、|| 连接。
比较操作符¶
操作符 | 说明 | 示例 |
---|---|---|
= |
等于 | host = 'web-01' |
!= |
不等于 | status != 200 |
> |
大于 | cpu_usage > 80 |
>= |
大于等于 | memory_usage >= 90 |
< |
小于 | response_time < 1000 |
<= |
小于等于 | disk_usage <= 80 |
模式匹配操作符¶
操作符 | 说明 | 示例 |
---|---|---|
=~ |
正则匹配 | message =~ 'error.*\\d+' |
!~ |
正则不匹配 | message !~ 'debug.*' |
集合操作符¶
操作符 | 说明 | 示例 |
---|---|---|
IN |
在集合中 | status IN [200, 201, 202] |
NOT IN |
不在集合中 | level NOT IN ['debug', 'info'] |
逻辑操作符¶
操作符 | 说明 | 示例 |
---|---|---|
AND 或 && |
逻辑与 | cpu > 80 AND memory > 90 |
OR 或 | | |
逻辑或 | status = 500 OR status = 502 |
NOT |
逻辑非 | NOT status = 200 |
应用示例¶
基础过滤¶
// 单一条件
M::cpu:(usage) {host = 'web-01'} [1h]
// 多个 AND 条件
M::cpu:(usage) {host = 'web-01', usage > 80} [1h]
// 使用 AND 关键字
M::cpu:(usage) {host = 'web-01' AND usage > 80} [1h]
// 混合使用逻辑操作符
M::cpu:(usage) {(host = 'web-01' OR host = 'web-02') AND usage > 80} [1h]
正则表达式匹配¶
// 匹配错误日志
L::logs:(message) {message =~ 'ERROR.*\\d{4}'} [1h]
// 匹配特定格式的日志
L::logs:(message) {message =~ '\\[(ERROR|WARN)\\].*'} [1h]
// 排除调试信息
L::logs:(message) {message !~ 'DEBUG.*'} [1h]
// 主机名模式匹配
M::cpu:(usage) {host =~ 'web-.*\\.prod\\.com'} [1h]
集合操作¶
// 状态码过滤
L::nginx:(count(*)) {status IN [200, 201, 202, 204]} [1h]
// 排除特定状态码
L::nginx:(count(*)) {status NOT IN [404, 500, 502]} [1h]
// 日志级别过滤
L::app_logs:(count(*)) {level IN ['ERROR', 'WARN', 'CRITICAL']} [1h]
数组字段的处理¶
当字段类型为数组时,DQL 支持多种数组匹配操作。
假设有字段 tags = ['web', 'prod', 'api']
:
推荐语法:使用 IN 和 NOT IN¶
// 检查数组是否包含某个值
{tags IN ['web']} // true,因为 tags 包含 'web'
{tags IN ['mobile']} // false,因为 tags 不包含 'mobile'
// 检查数组是否不包含某个值
{tags NOT IN ['mobile']} // true,因为 tags 不包含 'mobile'
{tags NOT IN ['web']} // false,因为 tags 包含 'web'
// 检查数组是否包含所有指定的值
{tags IN ['web', 'api']} // true,包含 'web' 和 'api'
{tags IN ['web', 'api', 'mobile']} // false,不包含 'mobile'
兼容语法(不推荐使用)¶
以下语法为历史兼容目的而保留,不推荐在新查询中使用。这些操作符在数组字段上的语义与普通字段不同,容易造成混淆。
// 历史语法:单值包含检查(等于操作符的重载语义)
{tags = 'web'} // true,因为 tags 包含 'web'
{tags = 'mobile'} // false,因为 tags 不包含 'mobile'
// 历史语法:单值不包含检查(不等于操作符的重载语义)
{tags != 'mobile'} // true,因为 tags 不包含 'mobile'
{tags != 'web'} // false,因为 tags 包含 'web'
函数筛选¶
任何返回布尔值的函数都可以用作筛选条件。
// 字符串匹配
L::logs:(message) { match(message, 'error') }
L::logs:(message) { wildcard(message, 'error*') }
// 查询响应时间异常的请求
L::access_logs:(*) {
response_time > 1000 AND
match(message, 'timeout')
}
// 查询内存使用率异常的主机
M::memory:(usage) {
(usage > 90 OR usage < 10) AND
host =~ 'prod-.*' AND
tags IN ['critical', 'important']
}
WHERE 子查询¶
WHERE 子查询是 DQL 中实现动态筛选的强大功能,它允许将一个查询的结果作为另一个查询的筛选条件。这种机制支持基于数据分析结果的动态筛选。
查询特点¶
- 动态筛选:筛选条件不是固定的值,而是通过查询动态计算得出
- 命名空间混合:支持跨命名空间的查询,实现不同数据类型的关联分析
- 串行执行:子查询先执行,其结果用于主查询的筛选
- 数组结果:子查询结果会被封装为数组,因此只支持
IN
和NOT IN
操作符
执行流程¶
以一个典型的 WHERE 子查询为例:
执行过程:
-
子查询执行:
O::HOST:(hostname) {provider = 'cloud-a'}
- 查询所有基础设施对象
- 筛选出 provider 为 'cloud-a' 的主机
- 返回主机名列表:
['host-01', 'host-02', 'host-03']
-
主查询执行:
M::cpu:(avg(usage)) [1h] BY host {host IN [...]}
- 查询 CPU 使用率数据
- 只统计子查询返回的主机
- 按主机分组计算平均使用率
应用示例¶
// 监控特定云服务商的服务器
M::cpu:(avg(usage)) [1h] BY host
{host IN (O::HOST:(hostname) {provider = 'cloud-a'})}
// 对比不同云服务商的性能
M::memory:(avg(used / total * 100)) [1h] BY host
{host IN (O::HOST:(hostname) {provider IN ['cloud-a', 'cloud-b']})}
// 分析特定业务的日志
L::app_logs:(count(*)) [1h] BY level
{service IN (T::services:(service_name) {business_unit = 'ecommerce'})}
// 监控关键业务的应用性能
M::response_time:(avg(response_time)) [1h] BY service
{service IN (T::services:(service_name) {criticality = 'high'})}
分组(group-by-clause)¶
分组是数据分析的核心功能,用于将数据按指定维度进行分组聚合。
基本语法¶
分组类型¶
字段分组¶
// 单字段分组
M::cpu:(avg(usage)) [1h] BY host
// 多字段分组
M::cpu:(avg(usage)) [1h] BY host, env
// 嵌套分组
M::cpu:(avg(usage)) [1h] BY datacenter, rack, host
表达式分组¶
// 复合数学表达式
M::memory:(avg(used)) [1h] BY ((used / total) * 100) as
usage_percent
// 多字段数学运算
M::performance:(avg(response_time)) [1h] BY
(response_time / 1000) as response_seconds
函数分组¶
// Drain 聚类算法
L::logs:(count(*)) BY drain(message, 0.7) as sample
// 正则提取分组
L::logs:(count(*)) [1h] BY regexp_extract(message, 'error_code: (\\d+)', 1)
分组结果处理¶
当查询同时包含分组和时间窗口时,会产生二维数据结构。Group By 可以跟时间窗口混合使用,这样的查询结果将会是一个二维数组。这个二维数组第一层是由分组键区分的多个分组本身,第二维是单个分组内的多时间区间数据。
二维数据结构示例:
查询:M::cpu:(max(usage_total)) [1h::10m] by host
查询结果结构:
{
"series": [
{
"columns": ["time", "max(usage_total)"],
"name": "cpu",
"tags": {"host": "web-server-01"},
"values": [
[1721059200000, 78.5],
[1721058600000, 82.3],
[1721058000000, 75.8],
[1721057400000, 88.2]
]
},
{
"columns": ["time", "max(usage_total)"],
"name": "cpu",
"tags": {"host": "web-server-02"},
"values": [
[1721059200000, 45.2],
[1721058600000, 52.8],
[1721058000000, 48.5],
[1721057400000, 61.3]
]
},
{
"columns": ["time", "max(usage_total)"],
"name": "cpu",
"tags": {"host": "web-server-03"},
"values": [
[1721059200000, 92.1],
[1721058600000, 95.7],
[1721058000000, 89.4],
[1721057400000, 97.6]
]
}
]
}
对这个二维数组再次加工:
- 如果要筛选这个二维数组的结果,使用 Having 子句
- 如果要对二维数组中的单组内数据进行排序或分页,使用 order by、limit、offset 系列语句
- 如果要对二维数组的分组组别进行排序或分页,使用 sorder by、slimit、soffset 系列语句
关于二维数据结构的详细说明和排序分页功能,请参考排序和分页。
Having 子句(having-clause){#having}¶
Having 子句用于对分组聚合后的结果进行筛选,类似于 WHERE 子句,但作用于聚合后的数据。
基本语法¶
与 WHERE 的区别¶
WHERE 和 HAVING 都是用于筛选数据的子句,但它们在查询执行的不同阶段起作用,处理的筛选条件也有所不同。
执行时序的区别¶
-
WHERE 子句:
- 在分组聚合之前执行
- 作用于原始数据行
- 过滤不满足条件的数据行,减少后续处理的数据量
-
HAVING 子句:
- 在分组聚合之后执行
- 作用于聚合后的结果
- 基于聚合函数的结果进行筛选
应用场景¶
HAVING 子句适用于对聚合结果进行筛选:
// 基于聚合函数值的筛选
M::cpu:(avg(usage) as avg_usage) [1h] BY host HAVING avg_usage > 80
// 基于多个聚合条件的筛选
M::http_requests:(
sum(request_count) as total,
sum(error_count) as errors
) [1h] BY service, endpoint
HAVING errors / total > 0.01 AND total > 1000
// 基于分组统计的筛选
L::logs:(count(*) as count) [1h] BY service HAVING count > 100
// 基于复合聚合条件的筛选
M::response_time:(
avg(response_time) as avg_time,
max(response_time) as max_time,
min(response_time) as min_time
) [1h] BY endpoint
HAVING avg_time > 1000 AND max_time > 5000 AND (max_time - min_time) > 2000
排序和分页¶
DQL 中的排序和分页是一个非常重要且独特的功能,它针对时序数据的特点设计了双重排序机制:组内排序和组间排序。这种设计使得 DQL 能够高效处理复杂的多维度时序数据分析需求。
理解 DQL 的数据结构¶
在深入排序和分页之前,先来理解 DQL 查询结果的二维数据结构。当查询同时包含分组(BY)和时间窗口时,会产生一个二维数组:
- 第一维(分组维度):由分组键区分的多个分组
- 第二维(时间维度):每个分组内按时间窗口聚合的数据
关于二维数据结构的详细说明和 JSON 格式示例,请参考 分组结果的处理。这个二维结构是 DQL 排序和分页功能的基础,理解这个结构对于掌握 DQL 的排序机制至关重要。
组内排序和分页(ORDER BY、LIMIT、OFFSET)¶
组内排序和分页作用于二维结构中的第二维,即每个分组内部的数据。这种排序在每个分组内独立执行,不会影响其他分组的数据。
基本语法¶
执行机制¶
组内排序的执行过程:
- 分组处理:对每个分组独立执行排序操作
- 排序依据:可以使用时间字段、聚合函数结果或计算表达式
- 分页限制:LIMIT 限制每个分组返回的数据行数
- 偏移处理:OFFSET 跳过每个分组的前 N 行数据
应用示例¶
基础时间排序¶
// 按时间降序排列,显示每个主机的最新数据
M::cpu:(max(usage_total)) [1h::10m] BY host ORDER BY time DESC
// 按时间升序排列,显示历史趋势
M::cpu:(max(usage_total)) [1h::10m] BY host ORDER BY time ASC
执行结果(ORDER BY time DESC):
{
"series": [
{
"columns": ["time", "max(usage_total)"],
"name": "cpu",
"tags": {"host": "web-server-01"},
"values": [
[1721059200000, 78.5], // 12:00:00
[1721058600000, 82.3], // 11:50:00
[1721058000000, 75.8], // 11:40:00
[1721057400000, 88.2], // 11:30:00
[1721056800000, 72.1], // 11:20:00
[1721056200000, 69.4] // 11:10:00
]
},
{
"columns": ["time", "max(usage_total)"],
"name": "cpu",
"tags": {"host": "web-server-02"},
"values": [
[1721059200000, 45.2], // 12:00:00
[1721058600000, 52.8], // 11:50:00
[1721058000000, 48.5], // 11:40:00
[1721057400000, 61.3], // 11:30:00
[1721056800000, 55.7], // 11:20:00
[1721056200000, 58.9] // 11:10:00
]
}
]
}
基于数值的排序¶
// 按CPU使用率降序排列,找出每个主机的峰值时段
M::cpu:(max(usage_total) as max_usage_total) [1h::10m] BY host ORDER BY max_usage_total DESC
// 按响应时间升序排列,找出性能最好的时段
M::response_time:(avg(response_time) as avg_response_time) [1h::5m] BY endpoint ORDER BY avg_response_time ASC
组内分页¶
// 每个主机只显示最新的3个数据点
M::cpu:(max(usage_total)) [1h::10m] BY host ORDER BY time DESC LIMIT 3
// 跳过最新的2个数据点,显示接下来的3个
M::cpu:(max(usage_total)) [1h::10m] BY host ORDER BY time DESC LIMIT 3 OFFSET 2
执行结果(LIMIT 3):
{
"series": [
{
"columns": ["time", "max(usage_total)"],
"name": "cpu",
"tags": {"host": "web-server-01"},
"values": [
[1721059200000, 78.5], // 12:00:00 - 最新
[1721058600000, 82.3], // 11:50:00
[1721058000000, 75.8] // 11:40:00
// 只返回前3条数据
]
},
{
"columns": ["time", "max(usage_total)"],
"name": "cpu",
"tags": {"host": "web-server-02"},
"values": [
[1721059200000, 45.2], // 12:00:00 - 最新
[1721058600000, 52.8], // 11:50:00
[1721058000000, 48.5] // 11:40:00
// 只返回前3条数据
]
}
]
}
组间排序和分页(SORDER BY、SLIMIT、SOFFSET)¶
组间排序和分页是 DQL 的特色功能,它作用于二维结构中的第一维,即对分组本身进行排序。这种排序需要将每个分组的数据降维为单个值,然后比较不同分组之间的这个值。
基本语法¶
执行机制¶
组间排序的执行过程:
- 降维计算:对每个分组应用聚合函数,计算出一个代表性数值
- 分组排序:根据降维后的值对所有分组进行排序
- 分组分页:SLIMIT 限制返回的分组数量,SOFFSET 跳过前 N 个分组
降维函数的选择¶
组间排序必须使用聚合函数进行降维,当不指定聚合函数时,默认使用的聚合函数时 last
。
常用的降维函数包括:
函数 | 说明 | 适用场景 |
---|---|---|
last() |
取最后一个值 | 适用于时间序列的当前状态 |
max() |
取最大值 | 适用于峰值分析 |
min() |
取最小值 | 适用于谷值分析 |
avg() |
取平均值 | 适用于整体趋势分析 |
sum() |
求和 | 适用于总量统计 |
count() |
计数 | 适用于频率分析 |
但几乎所有返回单值的聚合函数都可以使用,此处列出全不再部的函数列表。
应用示例¶
基于平均值的排序¶
// 按平均CPU使用率降序排列,找出负载最高的主机
M::cpu:(avg(usage)) [1h::10m] BY host SORDER BY avg(usage) DESC SLIMIT 5
// 按平均响应时间升序排列,找出性能最好的服务
M::response_time:(avg(response_time)) [1h] BY service SORDER BY avg(response_time) ASC SLIMIT 10
执行过程分析:
-
降维计算:
- web-server-01: avg(usage) = 77.7
- web-server-02: avg(usage) = 53.7
- web-server-03: avg(usage) = 90.9
-
分组排序(按 avg(usage) DESC):
- web-server-03: 90.9
- web-server-01: 77.7
- web-server-02: 53.7
-
最终结果(SLIMIT 2):
{
"series": [
{
"columns": ["time", "avg(usage)"],
"name": "cpu",
"tags": {"host": "web-server-03"}, // 平均使用率: 90.9 - 排名第一
"values": [
[1721059200000, 92.1],
[1721058600000, 95.7],
[1721058000000, 89.4]
]
},
{
"columns": ["time", "avg(usage)"],
"name": "cpu",
"tags": {"host": "web-server-01"}, // 平均使用率: 77.7 - 排名第二
"values": [
[1721059200000, 78.5],
[1721058600000, 82.3],
[1721058000000, 75.8]
]
}
// web-server-02 (avg: 53.7) 被过滤掉,因为 SLIMIT 2
]
}
应用示例¶
// 按最大CPU使用率排序,找出有异常峰值的主机
M::cpu:(max(usage_total)) [1h::10m] BY host SORDER BY max(usage_total) DESC SLIMIT 10
// 按最小内存使用率排序,找出资源利用率最低的主机
M::memory:(min(usage_percent)) [24h::1h] BY host SORDER BY min(usage_percent) ASC SLIMIT 5
// 按最新的CPU使用率排序,找出当前负载最高的主机
M::cpu:(usage) [1h::10m] BY host SORDER BY last(usage) DESC SLIMIT 10
// 按最新的错误率排序,找出当前问题最多的服务
L::logs:(count(*) as error_count) {level = 'error'} [1h] BY service SORDER BY error_count DESC SLIMIT 5
双重排序和分页的组合使用¶
在实际应用中,组内排序和组间排序经常结合使用,实现复杂的数据展示需求。这种组合可以同时控制分组的顺序和分组内数据的顺序。
执行顺序¶
双重排序的执行顺序:
- 组间排序:先对所有分组进行排序和分页
- 组内排序:对选中的分组进行内部排序和分页
应用示例¶
监控仪表板场景¶
// 找出CPU使用率最高的10台服务器,每台显示最新的5个数据点
M::cpu:(avg(usage)) [1h::10m] BY host
SORDER BY avg(usage) DESC SLIMIT 10 // 组间排序:找出使用率最高的10台
ORDER BY time DESC LIMIT 5 // 组内排序:每台显示最新的5个点
执行结果:
{
"series": [
{
"columns": ["time", "avg(usage)"],
"name": "cpu",
"tags": {"host": "web-server-03"}, // 平均使用率: 90.9 - 排名第一
"values": [
[1721059200000, 92.1], // 12:00:00 - 最新
[1721058600000, 95.7], // 11:50:00
[1721058000000, 89.4], // 11:40:00
[1721057400000, 97.6], // 11:30:00
[1721056800000, 87.3] // 11:20:00
// 只返回最新的5个数据点(LIMIT 5)
]
},
{
"columns": ["time", "avg(usage)"],
"name": "cpu",
"tags": {"host": "web-server-01"}, // 平均使用率: 77.7 - 排名第二
"values": [
[1721059200000, 78.5], // 12:00:00 - 最新
[1721058600000, 82.3], // 11:50:00
[1721058000000, 75.8], // 11:40:00
[1721057400000, 88.2], // 11:30:00
[1721056800000, 72.1] // 11:20:00
// 只返回最新的5个数据点(LIMIT 5)
]
}
// 其他主机被过滤掉,因为 SLIMIT 10 只返回使用率最高的10台
]
}
通过掌握 DQL 的排序和分页功能,您可以构建强大的监控仪表板、性能分析工具和业务洞察系统。
注意
合理使用组内排序和组间排序的组合,可以大大提升数据分析的效率和效果。