跳转至

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 查询的执行顺序非常重要,它决定了查询的语义和性能。

  1. 数据筛选:根据 namespace::datasource、where-clause、time-expr 筛选数据

    • 确定数据源
    • 应用 WHERE 条件过滤原始数据行
    • 应用时间范围筛选
    • 在此阶段尽早过滤数据,提高后续处理效率
  2. 时间聚合:如果 time-expr 包含 rollup,先执行 rollup 逻辑

    • Rollup 函数在时间维度上对数据进行预处理
    • 对于 Counter 类型指标,通常会计算速率或增量而不是直接使用原始值
    • 对于 Gauge 类型指标,可能使用 last、avg 等聚合函数
  3. 分组聚合:执行 group-by-clause 分组,并在分组内执行 select-clause 中的聚合函数

    • 根据 BY 子句的表达式将数据分组
    • 在每个分组内计算聚合函数(sum、count、avg、max、min 等)
    • 如果同时有时间窗口,会形成二维数据结构
  4. 分组筛选:执行 having-clause 筛选聚合分组

    • HAVING 子句作用于聚合后的结果
    • 可以使用聚合函数的结果进行筛选
    • 这是与 WHERE 子句的关键区别
  5. 非聚合函数:执行 select-clause 中的非聚合函数

    • 处理不需要聚合的表达式和函数
    • 对聚合结果进行进一步计算
  6. 组内排序:执行 order-by-clause、limit-clause 对分组内数据排序分页

    • ORDER BY 在每个分组内部独立执行
    • LIMIT 限制每个分组返回的数据行数
  7. 组间排序:执行 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 查询优化的重要机制,可以理解为传统数据库中的表或分区。在单个命名空间下,系统可能会因为数据来源、数据量、访问模式等因素对数据进行拆分,以提升查询性能和管理效率。

索引的作用

  • 性能优化:通过索引将数据分散存储,减少单次查询的数据扫描量
  • 数据隔离:不同业务、环境或时间范围的数据可以存储在不同的索引中
  • 权限管理:可以为不同索引设置不同的访问权限
  • 生命周期管理:不同索引可以设置不同的数据保留策略

索引的命名规则

  • 索引名称必须显式声明,不支持使用通配符或正则表达式进行匹配
  • 索引名称通常体现数据的业务属性,如:productionstagingweb-logsapi-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 子句中指定索引,但不推荐使用:

// 旧语法,不推荐
L::nginx { index = "web-logs" }
L::nginx { index IN ["web-logs", "api-logs"] }

应用示例

// 查询生产环境的 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::cpu:(usage)                    // 查询 CPU 指标
L::nginx:(count(*))                // 查询 Nginx 日志
T::user-service:(traces)           // 查询用户服务追踪

通配符匹配

M::*:(usage)                      // 查询所有指标

正则表达式匹配

M::re('cpu.*'):(usage)            // 查询以 cpu 开头的指标
L::re('web.*'):(count(*))          // 查询以 web 开头的日志
T::re('.*-service'):(traces)      // 查询以 -service 结尾的服务

子查询数据源

子查询是 DQL 中实现复杂分析的重要功能,它允许将一个查询的结果作为另一个查询的数据源。这种嵌套查询机制支持多层级的分析需求。

执行机制

子查询的执行遵循以下原则:

  1. 串行执行:内层子查询先执行,完成后将其结果作为外层查询的数据源
  2. 结果封装:子查询结果会被封装成临时表结构,供外层查询使用
  3. 命名空间混合:子查询支持不同命名空间的混合查询,实现跨数据类型分析
  4. 性能考虑:子查询会增加计算复杂度,需要合理设计查询逻辑

基本语法

namespace::(subquery):(projections)

执行过程

以一个典型的子查询为例:

L::(L::*:(count(*)) {level = 'error'} BY app_id):(count_distinct(app_id))

执行过程为:

  1. 内层子查询L::*:(count(*)) {level = 'error'} BY app_id

    • 扫描所有日志数据
    • 筛选出错误级别的日志
    • 按 app_id 分组统计错误数量
    • 生成临时表:app_id | count(*)
  2. 外层查询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 查询中最基本且最重要的部分之一。

字段选择

基本语法

// 选择单个字段
M::cpu:(usage)

// 选择多个字段
M::cpu:(usage, system, user)

// 选择所有字段
M::cpu:(*)

字段名规则

字段名有以下几种书写形式:

  1. 直接写名字:适用于普通标识符

    • message
    • host_name
    • response_time
  2. 反撇号包裹:适用于包含特殊字符或关键字的字段名

    • message
    • limit
    • host-name
    • column with spaces
  3. 避免的写法:单引号和双引号包裹的是字符串,不是字段名

    • 'message'
    • "message"

JSON 字段提取

当数据字段包含 JSON 格式的内容时,可以使用一个 JSON Path 语法子集提取内部字段的数据。

基本语法

field-name@json-path
@json-path                    // 默认使用 message 字段

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)

别名

为字段或表达式指定别名,使结果更易读和方便后续引用。

基本语法

expression as alias_name

应用示例

// 简单别名
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 }

但是当有多个聚合函数使用同一字段时,必须使用别名才能在后续正确区分:

// 必须使用别名的情况
M::cpu:(max(usage) as max_usage, min(usage) as min_usage) [1h] BY host

时间表达式(time-expr)

时间表达式是 DQL 的核心特性之一,用于指定查询的时间区间、聚合时间窗口和 Rollup 聚合函数。

基本语法

[start_time:end_time:interval:rollup]

时间范围

绝对时间戳

[1672502400000:1672588800000]     // 毫秒级时间戳
[1672502400:1672588800]           // 秒级时间戳

相对时间

支持多种时间单位,可以混合使用:

[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 列表示每个时间窗口的开始时间。

单个时间窗口

整个时间范围聚合为一个值:

M::cpu:(max(usage_total)) [1h]

查询结果:

{
  "columns": ["time", "max(usage_total)"],
  "values": [
    [1721059200000, 37.46]
  ]
}

时间窗口聚合

按时间间隔分组聚合:

M::cpu:(max(usage_total)) [1h::10m]

查询结果:

{
  "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 在查询执行流程中的位置:

原始数据 → WHERE 筛选 → **Rollup 预处理** → 分组聚合 → HAVING 筛选 → 最终结果

执行机制

Rollup 的执行过程分为两个阶段:

  1. 逐时间线处理:对每个独立的时间序列单独应用 Rollup 函数
  2. 聚合计算:在 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 的解决方案:

M::http:(sum(request_count)) [rate]

执行过程:

  1. Rollup 阶段(在每个时间线上分别执行):

    • web-server-01: rate([100, 110, 120, 130, 140, 150]) = 2 请求/分钟
    • web-server-02: rate([200, 210, 220, 230, 240, 250]) = 2 请求/分钟
  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)

筛选条件用于过滤数据行,只保留满足条件的数据进行后续处理。

基本语法

{condition1, condition2, condition3}

多个条件之间可以用逗号、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 中实现动态筛选的强大功能,它允许将一个查询的结果作为另一个查询的筛选条件。这种机制支持基于数据分析结果的动态筛选。

查询特点

  • 动态筛选:筛选条件不是固定的值,而是通过查询动态计算得出
  • 命名空间混合:支持跨命名空间的查询,实现不同数据类型的关联分析
  • 串行执行:子查询先执行,其结果用于主查询的筛选
  • 数组结果:子查询结果会被封装为数组,因此只支持 INNOT IN 操作符

执行流程

以一个典型的 WHERE 子查询为例:

M::cpu:(avg(usage)) [1h] BY host
{host IN (O::HOST:(hostname) {provider = 'cloud-a'})}

执行过程:

  1. 子查询执行O::HOST:(hostname) {provider = 'cloud-a'}

    • 查询所有基础设施对象
    • 筛选出 provider 为 'cloud-a' 的主机
    • 返回主机名列表:['host-01', 'host-02', 'host-03']
  2. 主查询执行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)

分组是数据分析的核心功能,用于将数据按指定维度进行分组聚合。

基本语法

BY expression1, expression2, ...

分组类型

字段分组

// 单字段分组
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]
      ]
    }
  ]
}

对这个二维数组再次加工:

  1. 如果要筛选这个二维数组的结果,使用 Having 子句
  2. 如果要对二维数组中的单组内数据进行排序或分页,使用 order by、limit、offset 系列语句
  3. 如果要对二维数组的分组组别进行排序或分页,使用 sorder by、slimit、soffset 系列语句

关于二维数据结构的详细说明和排序分页功能,请参考排序和分页

Having 子句(having-clause){#having}

Having 子句用于对分组聚合后的结果进行筛选,类似于 WHERE 子句,但作用于聚合后的数据。

基本语法

HAVING condition

与 WHERE 的区别

WHERE 和 HAVING 都是用于筛选数据的子句,但它们在查询执行的不同阶段起作用,处理的筛选条件也有所不同。

执行时序的区别

原始数据 → WHERE 筛选 → 分组聚合 → HAVING 筛选 → 最终结果
  1. WHERE 子句

    • 在分组聚合之前执行
    • 作用于原始数据行
    • 过滤不满足条件的数据行,减少后续处理的数据量
  2. 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)

组内排序和分页作用于二维结构中的第二维,即每个分组内部的数据。这种排序在每个分组内独立执行,不会影响其他分组的数据。

基本语法

ORDER BY expression [ASC|DESC]
LIMIT row_count
OFFSET row_offset

执行机制

组内排序的执行过程:

  1. 分组处理:对每个分组独立执行排序操作
  2. 排序依据:可以使用时间字段、聚合函数结果或计算表达式
  3. 分页限制:LIMIT 限制每个分组返回的数据行数
  4. 偏移处理: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 的特色功能,它作用于二维结构中的第一维,即对分组本身进行排序。这种排序需要将每个分组的数据降维为单个值,然后比较不同分组之间的这个值。

基本语法

SORDER BY aggregate_function(expression) [ASC|DESC]
SLIMIT group_count
SOFFSET group_offset

执行机制

组间排序的执行过程:

  1. 降维计算:对每个分组应用聚合函数,计算出一个代表性数值
  2. 分组排序:根据降维后的值对所有分组进行排序
  3. 分组分页: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

执行过程分析:

  1. 降维计算

    • web-server-01: avg(usage) = 77.7
    • web-server-02: avg(usage) = 53.7
    • web-server-03: avg(usage) = 90.9
  2. 分组排序(按 avg(usage) DESC):

    • web-server-03: 90.9
    • web-server-01: 77.7
    • web-server-02: 53.7
  3. 最终结果(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

双重排序和分页的组合使用

在实际应用中,组内排序和组间排序经常结合使用,实现复杂的数据展示需求。这种组合可以同时控制分组的顺序和分组内数据的顺序。

执行顺序

双重排序的执行顺序:

  1. 组间排序:先对所有分组进行排序和分页
  2. 组内排序:对选中的分组进行内部排序和分页

应用示例

监控仪表板场景
// 找出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 的排序和分页功能,您可以构建强大的监控仪表板、性能分析工具和业务洞察系统。

注意

合理使用组内排序和组间排序的组合,可以大大提升数据分析的效率和效果。

文档评价

文档内容是否对您有帮助? ×