# DQL

---


DQL（Debug Query Language）是<<< custom_key.brand_name >>>平台的核心查询语言，专为高效查询和分析时间序列数据、日志数据、事件数据等设计。DQL 结合了 SQL 的语义表达和 PromQL 的语法结构，旨在为用户提供一种灵活且强大的查询工具。

本文档将帮助您快速理解 DQL 的基本语法和设计思路，并通过示例展示如何编写 DQL 查询。

## 基本查询结构

DQL 的基本查询结构如下：

```plaintext
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]
```

其中 `index` 可省略；省略时默认使用 `default` 索引。

### 执行顺序

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::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       | Unrecovered Event，未恢复事件数据         | 未解决的告警和事件                 |


### 索引（index）

索引是 DQL 查询优化的重要机制，可以理解为传统数据库中的表或分区。在单个命名空间下，系统可能会因为数据来源、数据量、访问模式等因素对数据进行拆分，以提升查询性能和管理效率。

#### 索引的作用

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

#### 索引的命名规则

- 索引名称可以显式声明；当不显式声明时，默认使用 `default`
- 显式声明索引名称时，不支持使用通配符或正则表达式进行匹配
- 索引名称通常体现数据的业务属性，如：`production`、`staging`、`web-logs`、`api-logs`

#### 基本语法

```
// 使用默认索引（当不指定时自动使用 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::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 日志数据：*

```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)
```

#### 条件表达式

`CASE WHEN` 用于在查询中按条件选择不同的值。它通常和聚合函数一起使用，用来做条件计数、条件求和或按条件归一化字段。

#### 基本语法

```
CASE
  WHEN condition THEN value
  [WHEN condition THEN value ...]
  [ELSE default_value]
END
```

也可以使用简单 CASE 语法，对同一个字段做多值匹配：

```
CASE field
  WHEN value1 THEN result1
  WHEN value2 THEN result2
  ELSE default_value
END
```

`WHEN` 会按书写顺序依次匹配，命中第一个条件后返回对应的 `THEN` 值；如果都不命中，则返回 `ELSE` 值。没有显式写 `ELSE` 时，默认返回 `nil`。

#### 应用示例

**条件求和：只统计 5xx 请求的流量**

```
L::nginx_access:(
    sum(CASE WHEN status >= 500 THEN bytes ELSE 0 END) as error_bytes
) [1h] BY host
```

**条件计数：统计错误请求数量**

```
L::nginx_access:(
    count(CASE WHEN status >= 500 THEN 1 ELSE nil END) as error_count
) [1h] BY service
```

> 注意：`count(expr)` 统计的是非 `nil` 值。`0` 也是非 `nil`，因此 `count(CASE WHEN condition THEN 1 ELSE 0 END)` 会统计所有行，而不是只统计命中条件的行。做条件计数时推荐使用 `ELSE nil`，或使用 `sum(CASE WHEN condition THEN 1 ELSE 0 END)`。

**多分支分类：按状态码生成分级**

```
L::nginx_access:(
    max(CASE
        WHEN status >= 500 THEN 3
        WHEN status >= 400 THEN 2
        WHEN status >= 300 THEN 1
        ELSE 0
    END) as status_level
) [1h] BY service
```

**字段清洗后再判断：忽略大小写统计错误日志**

```
L::app_logs:(
    sum(CASE WHEN lower(level) = "error" THEN 1 ELSE 0 END) as error_count
) [1h] BY service
```

**类型转换后再聚合：字段是字符串时转为数值参与求和**

```
L::nginx_access:(
    sum(CASE WHEN status >= 500 THEN int(bytes) ELSE 0 END) as error_bytes
) [1h] BY host
```

#### 支持范围

`CASE WHEN` 当前是受限的行级条件表达式，设计目标是支持高性能下推执行。它可以放在聚合函数内部，例如 `sum(CASE ...)`、`count(CASE ...)`、`max(CASE ...)`。

`CASE` 内部支持：

- 字段
- 字面量和 `nil`
- 布尔条件
- 以下标量函数：`int`、`float`、`string`、`md5`、`lower`、`upper`、`trim`、`ltrim`、`rtrim`、`length`

不支持在 `CASE` 内使用聚合函数。聚合函数应该包在 `CASE` 外面：

```
// 推荐：先逐行计算 CASE，再聚合
L::nginx_access:(
    sum(CASE WHEN status >= 500 THEN bytes ELSE 0 END) as error_bytes
) [1h] BY host

// 不支持：CASE 内部使用聚合函数
L::nginx_access:(
    CASE WHEN sum(bytes) > 0 THEN "has_bytes" ELSE "empty" END
) [1h] BY host
```

也不支持在 `CASE` 内使用未列入支持范围的复杂标量函数，例如 `regexp_extract`。如果需要这类复杂处理，建议先通过查询条件、字段清洗或子查询拆分逻辑，避免在 `CASE` 中触发大范围明细扫描。

### 别名

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

#### 基本语法

```
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-clause）

时间子句是 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   |

时长表达式用在时间子句的时候表示从当前时间往前的偏移量，当用在 Select 子句或 Where 子句等地方的时候会被当做毫秒整型参与计算。

当用在聚合查询中时，还额外支持两种时长单位：

| 单位  | 说明                                 | 示例    |
| ----- | ------------------------------------ | ------- |
| i、is | 聚合时间窗口的倍数，返回值为浮点型秒 | 1i、1is |
| ims   | 聚合时间窗口的倍数，返回值为整型毫秒 | 1ims    |

```
O::HOST:(count(*)){ `last_update_time` > (now()-10m) } // 10m 被当做 600,000 整型参与计算
L::*:( count(*) / 1i ) [::1m] // 除以时间窗口大小 (1m) 的秒数，计算日志写入的 QPS
```

#### 预设时间范围

提供常用的时间范围关键字：

| 关键字     | 说明 | 时间范围                         |
| ---------- | ---- | -------------------------------- |
| 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]
```

**查询结果：**

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

#### 时间窗口聚合

按时间间隔分组聚合：

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

**查询结果：**

```json
{
  "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 是单调递增的。需要先逐个时间线计算增长率，然后再进行聚合。

**问题示例：假设有两个服务器的请求计数器：*

```json
{
  "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}
  ]
}
```

```json
{
  "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`         |

> **提示**：`OR` 运算符除了执行逻辑或以外，还提供空值兜底语义——当左侧表达式返回 `NULL` 时，直接返回右侧表达式的结果，可用来实现优先级回退逻辑，例如 `status OR backup_status`。

### 应用示例

#### 基础过滤

```
// 单一条件
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}

WHERE 子查询是 DQL 中实现动态筛选的强大功能，它允许将一个查询的结果作为另一个查询的筛选条件。这种机制支持基于数据分析结果的动态筛选。

#### 查询特点

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

#### 执行流程

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

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

执行过程：

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)) { host IN (O::HOST:(hostname) {provider = 'cloud-a'}) } [1h] BY host


// 对比不同云服务商的性能
M::memory:(avg(used / total * 100)) { host IN (O::HOST:(hostname) {provider IN ['cloud-a', 'cloud-b']}) } [1h] BY host


// 分析特定业务的日志
L::app_logs:(count(*)) { service IN (T::services:(service_name) {business_unit = 'ecommerce'}) } [1h] BY level


// 监控关键业务的应用性能
M::response_time:(avg(response_time)) { service IN (T::services:(service_name) {criticality = 'high'}) } [1h] BY service
```

## 分组（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)
```

### 分组结果处理 {#result}

当查询同时包含分组和时间窗口时，会产生二维数据结构。Group By 可以跟时间窗口混合使用，这样的查询结果将会是一个二维数组。这个二维数组第一层是由分组键区分的多个分组本身，第二维是单个分组内的多时间区间数据。

*二维数据结构示例：*

查询：`M::cpu:(max(usage_total)) [1h::10m] by host`

**查询结果结构：**

```json
{
  "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 系列语句

> 关于二维数据结构的详细说明和排序分页功能，请参考[排序和分页](#sorting)。

## Having 子句（having-clause）{#having}

Having 子句用于对分组聚合后的结果进行筛选，类似于 [WHERE 子句](#where)，但作用于聚合后的数据。

### 基本语法

```
HAVING condition
```

### 与 WHERE 的区别

[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
```

## 排序和分页 {#sorting}

DQL 中的排序和分页是一个非常重要且独特的功能，它针对时序数据的特点设计了双重排序机制：组内排序和组间排序。这种设计使得 DQL 能够高效处理复杂的多维度时序数据分析需求。

### 理解 DQL 的数据结构

在深入排序和分页之前，先来理解 DQL 查询结果的二维数据结构。当查询同时包含分组（BY）和时间窗口时，会产生一个二维数组：

- 第一维（分组维度）：由分组键区分的多个分组
- 第二维（时间维度）：每个分组内按时间窗口聚合的数据

> 关于二维数据结构的详细说明和 JSON 格式示例，请参考 [分组结果的处理](#result)。这个二维结构是 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）：**

```json
{
  "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）：**

```json
{
  "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 的特色功能，它作用于二维结构中的**第一维**，即对分组本身进行排序。这种排序需要将每个分组的数据降维为单个值，然后比较不同分组之间的这个值。

当查询未使用 `BY` 时，结果通常只有一个分组。此时 `SORDER BY` 在排序上通常没有可见效果，但 `SLIMIT` / `SOFFSET` 仍按“分组数量”语义生效（即针对该单一分组）。

#### 基本语法

```
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）：

```json
{
  "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个点
```

**执行结果：**

```json
{
  "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 的排序和分页功能，您可以构建强大的监控仪表板、性能分析工具和业务洞察系统。

???+ warning "注意"

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