Skip to content

DQL


DQL (Debug Query Language) is the core query language of the Guance platform, designed for efficiently querying and analyzing time series data, log data, event data, etc. DQL combines the semantic expression of SQL with the syntactic structure of PromQL, aiming to provide users with a flexible and powerful query tool.

This document will help you quickly understand the basic syntax and design concepts of DQL, and demonstrate how to write DQL queries through examples.

Basic Query Structure

The basic query structure of DQL is as follows:

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]

Where index can be omitted; when omitted, the default index is used.

Execution Order

The execution order of a DQL query is very important, as it determines the semantics and performance of the query.

  1. Data Filtering: Filter data based on namespace::datasource, where-clause, and time-expr

    • Determine the data source
    • Apply WHERE conditions to filter raw data rows
    • Apply time range filtering
    • Filter data as early as possible in this stage to improve subsequent processing efficiency
  2. Time Aggregation: If time-expr includes rollup, execute the rollup logic first

    • Rollup functions preprocess data on the time dimension
    • For Counter type metrics, the rate or increment is usually calculated instead of using the raw value directly
    • For Gauge type metrics, aggregation functions such as last, avg, etc. may be used
  3. Group Aggregation: Execute group-by-clause to group data, and execute aggregation functions in select-clause within each group

    • Group data based on the expressions in the BY clause
    • Calculate aggregation functions (sum, count, avg, max, min, etc.) within each group
    • If there is also a time window, a two-dimensional data structure is formed
  4. Group Filtering: Execute having-clause to filter aggregated groups

    • The HAVING clause acts on the aggregated results
    • It can use the results of aggregation functions for filtering
    • This is the key difference from the WHERE clause
  5. Non-Aggregation Functions: Execute non-aggregation functions in select-clause

    • Process expressions and functions that do not require aggregation
    • Perform further calculations on the aggregation results
  6. Intra-Group Sorting: Execute order-by-clause and limit-clause to sort and paginate data within groups

    • ORDER BY is executed independently within each group
    • LIMIT limits the number of data rows returned per group
  7. Inter-Group Sorting: Execute sorder-by-clause, slimit-clause, and soffset-clause to sort and paginate groups

    • SORDER BY sorts the groups themselves
    • Requires dimensionality reduction of group results (e.g., using functions like max, avg, last)
    • SLIMIT limits the number of groups returned

Complete Example

Let's understand the structure of DQL through a complete example:

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

The meaning of this query is:

  • Namespace: M (Metrics data)
  • Index: production
  • Datasource: cpu
  • Select Fields: Average and maximum of the usage field
  • Time Range: Past 1 hour, aggregated every 5 minutes
  • Filter Conditions: Hostname starts with web and CPU usage greater than 50%
  • Group By: Group by hostname and environment
  • Group Filter: Average CPU usage greater than 60%
  • Sorting: Descending order by time, up to 100 entries per group
  • Inter-Group Sorting: Descending order by average usage, up to 10 groups

Namespace (namespace)

Namespaces are used to distinguish different types of data, each with its specific query methods and storage strategies. DQL supports querying various business data types:

Namespace Description Typical Use Cases
M Metrics, time series metric data CPU usage, memory usage, request count, etc.
L Logging, log data Application logs, system logs, error logs, etc.
O Object, infrastructure object data Server information, container information, network devices, etc.
OH History object, object history data Server configuration change history, performance metrics history, etc.
CO Custom object, custom object data Business-specific custom object information
COH History custom object, custom object history data Historical change records of custom objects
N Network, network data Network traffic, DNS queries, HTTP requests, etc.
T Trace, trace data Distributed tracing, call chain analysis, etc.
P Profile, profiling data Performance profiling, CPU flame graphs, etc.
R RUM, user access data Front-end performance, user behavior analysis, etc.
E Event, event data Alert events, deployment events, system events, etc.
UE Unrecovered Event, unrecovered event data Unresolved alerts and events
B Cloud billing, cloud billing

Index (index)

Index is an important mechanism for DQL query optimization, which can be understood as tables or partitions in traditional databases. Within a single namespace, the system may split data based on factors such as data source, data volume, and access patterns to improve query performance and management efficiency.

Role of Index

  • Performance Optimization: Distribute data storage through indexes, reducing the amount of data scanned per query
  • Data Isolation: Data from different businesses, environments, or time ranges can be stored in different indexes
  • Permission Management: Different access permissions can be set for different indexes
  • Lifecycle Management: Different data retention policies can be set for different indexes

Index Naming Rules

  • Index names can be explicitly declared; when not explicitly declared, the default default is used
  • When explicitly declaring an index name, wildcards or regular expressions are not supported for matching
  • Index names usually reflect the business attributes of the data, e.g., production, staging, web-logs, api-logs

Basic Syntax

// Use the default index (automatically uses default when not specified)
M::cpu           // Equivalent to M("default")::cpu
L::nginx         // Equivalent to L("default")::nginx

// Specify a single index
M("production")::cpu     // Query CPU metrics in the production index
L("web-logs")::nginx     // Query Nginx logs in the web-logs index

// Multi-index query (query data from multiple indexes simultaneously)
M("production", "staging")::cpu     // Query CPU metrics for production and staging environments
L("web-logs", "api-logs")::nginx     // Query Web and API logs

Index and Performance

Proper use of indexes can significantly improve query performance:

  • Precise Index: When you know exactly which index the data is in, specify that index directly
  • Multi-Index Query: When you need to query across multiple indexes, use the multi-index syntax instead of using wildcards
  • Avoid Full Index Scans: Try to reduce the data scan range through a combination of indexes and WHERE conditions

For historical reasons, DQL also supports specifying the index in the WHERE clause, but it is not recommended:

// Old syntax, not recommended
L::nginx { index = "web-logs" }
L::nginx { index IN ["web-logs", "api-logs"] }

Application Examples

// Query CPU usage for the production environment
M::cpu:(avg(usage)) [1h] BY host

// Compare production and staging environments
M("production", "staging")::cpu:(avg(usage)) [1h] BY index, host

// Analyze web server logs
L("web-logs")::nginx:(count(*)) {status >= 400} [1h] BY status

// Compare error rates for Web and API servers
L("web-logs", "api-logs")::*:(count(*)) {status >= 500} [1h] BY index

Datasource (datasource)

The datasource specifies the specific data source for the query, which can be a dataset name, wildcard pattern, regular expression, or subquery.

Basic Datasources

The definition of datasource differs across namespaces:

Namespace Datasource Type Example
M Measurement cpu, memory, network
L Source nginx, tomcat, java-app
O Infrastructure object classification host, container, process
T Service name user-service, order-service
R RUM data type session, view, resource, error

Datasource Syntax

Specify Datasource Name

M::cpu:(usage)                    // Query CPU metrics
L::nginx:(count(*))                // Query Nginx logs
T::user-service:(traces)           // Query user service traces

Wildcard Matching

M::*:(usage)                      // Query all metrics

Regular Expression Matching

M::re('cpu.*'):(usage)            // Query metrics starting with cpu
L::re('web.*'):(count(*))          // Query logs starting with web
T::re('.*-service'):(traces)      // Query services ending with -service

Subquery Datasource

Subquery is an important feature in DQL for implementing complex analysis, allowing the result of one query to be used as the datasource for another query. This nested query mechanism supports multi-level analysis requirements.

Execution Mechanism

The execution of subqueries follows these principles:

  1. Serial Execution: The inner subquery executes first, and its results are used as the datasource for the outer query
  2. Result Encapsulation: The subquery result is encapsulated into a temporary table structure for use by the outer query
  3. Namespace Mixing: Subqueries support mixed queries across different namespaces, enabling cross-data type analysis
  4. Performance Considerations: Subqueries increase computational complexity, so query logic needs to be designed reasonably

Basic Syntax

namespace::(subquery):(projections)

Execution Process

Take a typical subquery as an example:

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

The execution process is:

  1. Inner Subquery: L::*:(count(*)) {level = 'error'} BY app_id

    • Scan all log data
    • Filter out logs with error level
    • Count the number of errors grouped by app_id
    • Generate temporary table: app_id | count(*)
  2. Outer Query: L::(...):(count_distinct(app_id))

    • Use the subquery result as the datasource
    • Count how many distinct app_ids exist
    • Final result: the number of applications with errors

Application Examples

// Count the number of applications with errors
L::(L::*:(count(*)) {level = 'error'} BY app_id):(count_distinct(app_id))

// Analyze servers with high CPU usage
M::(M::cpu:(avg(usage)) [1h] BY host {avg(usage) > 80}):(count(host))

// First find service endpoints with error rate exceeding 1%, then count the number of affected services
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 Clause (select-clause)

The Select clause is used to specify the fields or expressions to be returned by the query, and is one of the most basic and important parts of a DQL query.

Field Selection

Basic Syntax

// Select a single field
M::cpu:(usage)

// Select multiple fields
M::cpu:(usage, system, user)

// Select all fields
M::cpu:(*)

Field Name Rules

Field names can be written in the following forms:

  1. Direct Name: Suitable for ordinary identifiers

    • message
    • host_name
    • response_time
  2. Backtick Enclosed: Suitable for field names containing special characters or keywords

    • message
    • limit
    • host-name
    • column with spaces
  3. Avoided Writing: Single quotes and double quotes enclose strings, not field names

    • 'message'
    • "message"

JSON Field Extraction

When a data field contains JSON-formatted content, a subset of JSON Path syntax can be used to extract internal field data.

Basic Syntax

field-name@json-path
@json-path                    // Use the message field by default

JSON Path Syntax

  • Dot for object property: .field_name
  • Square brackets for object property: ["key"] (suitable for keys containing spaces or special characters)
  • Array index access: [index]

Application Examples

Assume the following JSON log data:

{
  "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"}
    ]
  }
}
// Extract request method
L::auth_logs:(message@request.method)

// Extract request path
L::auth_logs:(message@request.path)

// Extract username
L::auth_logs:(message@request.body.username)

// Extract response status
L::auth_logs:(message@response.status)

// Extract User-Agent (contains hyphen, need square brackets)
L::auth_logs:(message@request.headers["user-agent"])

// Extract the first element of the permissions array
L::auth_logs:(message@request.body.permissions[0])

// Extract the name of the first object in response data
L::auth_logs:(message@response.data[0].name)

// Count the number of different request methods
L::auth_logs:(count(*)) [1h] BY message@request.method

// Analyze response time distribution
L::auth_logs:(avg(message@response.time), max(message@response.time)) [1h] BY message@request.method

// Extract multiple fields
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]

Calculated Fields

Expression Calculation

Supports basic arithmetic operations:

// Unit conversion (milliseconds to seconds)
L::nginx:(response_time / 1000) as response_time_seconds

// Calculate percentage
M::memory:(used / total * 100) as usage_percentage

// Compound calculation
M::network:((bytes_in + bytes_out) / 1024 / 1024) as total_traffic_mb

Function Calculation

Supports various aggregation and transformation functions:

// Aggregation functions
M::cpu:(max(usage), min(usage), avg(usage)) [1h] BY host

// Transformation functions
L::logs:(int(response_time) as response_time_seconds)
L::logs:(floor(response_time) as response_time_seconds)

Conditional Expressions

CASE WHEN is used to select different values based on conditions within a query. It is often used together with aggregation functions for conditional counting, conditional summation, or normalizing fields by condition.

Basic Syntax

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

A simple CASE syntax can also be used for multi-value matching on the same field:

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

WHEN clauses are matched in the order they are written; the first matching condition returns its corresponding THEN value. If none match, the ELSE value is returned. If ELSE is not explicitly written, nil is returned by default.

Application Examples

Conditional Summation: Only count traffic from 5xx requests

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

Conditional Counting: Count the number of error requests

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

Note: count(expr) counts non-nil values. 0 is also non-nil, so count(CASE WHEN condition THEN 1 ELSE 0 END) counts all rows, not just the rows that match the condition. For conditional counting, it is recommended to use ELSE nil, or use sum(CASE WHEN condition THEN 1 ELSE 0 END).

Multi-Branch Classification: Generate a level based on status code

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

Clean Field Before Evaluating: Count error logs case-insensitively

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

Aggregate After Type Conversion: When the field is a string, convert to numeric for summation

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

Supported Scope

CASE WHEN is currently a restricted row-level conditional expression, designed to support high-performance pushdown execution. It can be placed inside aggregation functions, such as sum(CASE ...), count(CASE ...), max(CASE ...).

Inside CASE, the following are supported:

  • Fields
  • Literals and nil
  • Boolean conditions
  • The following scalar functions: int, float, string, md5, lower, upper, trim, ltrim, rtrim, length, regexp_replace

Using aggregation functions inside CASE is not supported. Aggregation functions should be wrapped outside CASE:

// Recommended: Calculate CASE row by row, then aggregate
L::nginx_access:(
    sum(CASE WHEN status >= 500 THEN bytes ELSE 0 END) as error_bytes
) [1h] BY host

// Not supported: Using aggregation functions inside CASE
L::nginx_access:(
    CASE WHEN sum(bytes) > 0 THEN "has_bytes" ELSE "empty" END
) [1h] BY host

Using complex scalar functions not listed in the supported scope, such as regexp_extract, inside CASE is also not supported. If other complex processing is needed, it is recommended to split the logic through query conditions, field cleaning, or subqueries to avoid triggering large-scale detail scans in CASE.

Aliases

Specify an alias for a field or expression to make the result more readable and easier to reference later.

Basic Syntax

expression as alias_name

Application Examples

// Simple alias
M::cpu:(avg(usage) as avg_usage, max(usage) as max_usage) [1h] BY host

// Expression alias
M::memory:((used / total) * 100 as usage_percent) [1h] BY host

// Function alias
L::logs:(count(*) as error_count) {level = 'error'} [1h] BY service

// JSON extraction alias
L::api_logs:(
    message@request.method as http_method,
    message@response.status as http_status,
    message@response.time as response_time_ms
) [1h]

Usage Tips

In DQL, the result of an aggregation function can be referenced directly using the original field name, reducing the need for aliases:

M::cpu:(max(usage)) [1h] BY host

// The result will include a max(usage) column, which can be directly referenced later using the usage column name to get the `max(usage)` column of the subquery result:
M::(M::cpu:(max(usage)) [1h] BY host):(max(usage)) { usage > 80 }

However, when multiple aggregation functions use the same field, an alias must be used to distinguish them correctly later:

// Must use aliases in this case
M::cpu:(max(usage) as max_usage, min(usage) as min_usage) [1h] BY host

Time Clause (time-clause)

The time clause is a core feature of DQL, used to specify the query time range, aggregation time window, and Rollup aggregation function.

Basic Syntax

[start_time:end_time:interval:rollup]

Time Range

Absolute Timestamps

[1672502400000:1672588800000]     // Millisecond timestamps
[1672502400:1672588800]           // Second timestamps

Relative Time

Supports multiple duration units, which can be mixed:

[1h]                             // Past 1 hour to now
[1h:5m]                          // From past 1 hour to past 5 minutes
[1h30m]                          // Past 1 hour 30 minutes
[2h15m30s]                       // Past 2 hours 15 minutes 30 seconds

Duration Expression Description

Unit Description Example
s Second 30s
m Minute 5m
h Hour 2h
d Day 7d
w Week 4w
y Year 1y

When used in the time clause, the duration expression represents the offset backward from the current time. When used in the Select clause or Where clause, it is treated as a millisecond integer for calculation.

When used in aggregation queries, two additional duration units are supported:

Unit Description Example
i、is Multiple of the aggregation time window, return value is floating-point seconds 1i、1is
ims Multiple of the aggregation time window, return value is integer milliseconds 1ims
O::HOST:(count(*)){ `last_update_time` > (now()-10m) } // 10m is treated as integer 600,000 for calculation
L::*:( count(*) / 1i ) [::1m] // Divide by the time window size (1m) in seconds to calculate the log write QPS

Preset Time Ranges

Provides commonly used time range keywords:

Keyword Description Time Range
TODAY Today From 00:00 today to now
YESTERDAY Yesterday From 00:00 yesterday to 00:00 today
THIS WEEK This week From 00:00 this Monday to now
LAST WEEK Last week From 00:00 last Monday to 00:00 this Monday
THIS MONTH This month From 00:00 on the 1st of this month to now
LAST MONTH Last month From 00:00 on the 1st of last month to 00:00 on the 1st of this month
[TODAY]                          // Today's data
[YESTERDAY]                      // Yesterday's data
[THIS WEEK]                      // This week's data
[LAST WEEK]                      // Last week's data
[THIS MONTH]                     // This month's data
[LAST MONTH]                     // Last month's data

When using time range keywords, please ensure the time zone setting of the workspace is correct, and strictly convert according to the time zone of the user's request.

Time Window Aggregation

The time window groups data by the specified time interval for aggregation. The time column in the returned result represents the start time of each time window.

Single Time Window

The entire time range is aggregated into a single value:

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

Query Result:

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

Time Window Aggregation

Grouped aggregation by time interval:

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

Query Result:

{
  "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 Functions

Rollup functions are an important preprocessing step in DQL, executed before group aggregation, used to preprocess raw time series data.

Execution Sequence

The position of Rollup in the query execution flow:

Raw Data → WHERE Filtering → **Rollup Preprocessing** → Group Aggregation → HAVING Filtering → Final Result

Execution Mechanism

The execution of Rollup is divided into two phases:

  1. Per-Time Series Processing: Apply the Rollup function individually to each independent time series
  2. Aggregation Calculation: Perform group aggregation on the result after Rollup processing

Rollup Shorthand, Rollup Function Call, and Explicit Aggregation Call

There are three easily confused timing function writing styles in DQL:

  1. Rollup Shorthand: Only the function name is written in the time clause, e.g., [rate], [1h::5m:slope].
  2. Rollup Function Call: Algorithm parameters are passed to the Rollup function in the time clause, e.g., [1h::1m:ewma(0.3)], [1h::1m:moving_average(5)], [1h::1m:percentile(95)].
  3. Explicit Aggregation Call: Full function call written in the Select clause, e.g., rate(request_count), ewma(usage, 0.3), corr(cpu_usage, request_count).

The main differences between the three writing styles are the execution phase and parameter capabilities:

Writing Style Example Execution Phase Applicable Scenario
Rollup Shorthand [1h::5m:rate] Before group aggregation, executed on each original time series Preprocess each time series first, then perform group aggregation
Rollup Function Call [1h::1m:ewma(0.3)] Before group aggregation, executed on each original time series Rollup function requires additional algorithm parameters
Explicit Aggregation Call ewma(usage, 0.3) Select clause aggregation phase Need to specify fields, additional parameters, or multiple input fields

The Rollup input fields in the time clause are determined by the Select field, time window, and original time series. Therefore, only additional algorithm parameters are passed in the Rollup function call, not field names, in the time clause. Single-input timing functions without additional parameters can usually support Rollup shorthand; single-input functions requiring additional algorithm parameters can support Rollup function calls; functions requiring multiple input fields should use explicit aggregation calls.

Example: Counter Metrics First Rollup, Then Aggregation

Counter metrics should first calculate the growth rate on each original time series, then sum by business dimension:

// Recommended: First calculate rate on each time series, then sum by service
M::http_requests:(sum(request_count)) [1h::5m:rate] BY service

// Not recommended: Directly aggregate Counter raw cumulative values, the result is not QPS
M::http_requests:(sum(request_count)) [1h::5m] BY service

Example: Use Rollup Function Call When Additional Algorithm Parameters Are Needed

ewma requires an explicit smoothing coefficient alpha. If you want to apply EWMA to each original time series first, then enter group aggregation, you can write it in the time clause:

// Rollup function call: First apply EWMA to each time series, then average by host
M::cpu:(avg(usage)) [1h::1m:ewma(0.3)] BY host

// Error: ewma requires alpha, cannot just write the function name
M::cpu:(avg(usage)) [1h::1m:ewma] BY host

// Error: The Rollup parameter in the time clause only writes algorithm parameters, not field names
M::cpu:(avg(usage)) [1h::1m:ewma(usage, 0.3)] BY host

If you want to calculate EWMA in the Select aggregation phase, you can also use an explicit aggregation call:

// Explicit aggregation call: Calculate EWMA on usage in the Select aggregation phase
M::cpu:(ewma(usage, 0.3)) [1h::1m] BY host

Other single-value parameterized functions also suitable for time clause Rollup include:

// First apply a 5-point moving average to each time series, then average by host
M::cpu:(avg(usage)) [1h::1m:moving_average(5)] BY host

// First take P95 of each time series, then average by service
M::response_time:(avg(duration)) [1h::5m:percentile(95)] BY service

Example: Use Explicit Aggregation Call When Multiple Input Fields Are Needed

corr requires two input fields, so it cannot be written in the time clause Rollup:

// Correct: Explicitly specify two fields in the Select clause
M::service_metric:(corr(cpu_usage, request_count)) [1h::5m] BY service

// Error: Time clause Rollup cannot express two input fields
M::service_metric:(avg(cpu_usage)) [1h::5m:corr] BY service

Example: Different Execution Phases for Functions with the Same Name

When a function supports both Rollup shorthand and explicit aggregation call, the two writing styles represent different execution phases and should not be assumed to be completely equivalent:

// Rollup shorthand: First calculate zscore on each original time series, then enter group aggregation
M::cpu:(max(usage)) [1h::5m:zscore] BY host

// Explicit aggregation call: Calculate zscore on usage in the Select aggregation phase
M::cpu:(zscore(usage)) [1h::5m] BY host

Application Scenarios

A typical application scenario of Rollup functions is processing Counter metrics.

For Prometheus Counter type metrics, aggregating raw values directly is meaningless because Counter is monotonically increasing. It is necessary to first calculate the growth rate for each time series, then perform aggregation.

*Problem Example: Assume two servers with request counters:

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

Problem with Direct Aggregation:

  • web-server-01's request_count starts at 100
  • web-server-02's request_count starts at 200
  • Although both servers have the same request rate (10 requests per 5 minutes), the absolute values differ

Solution Using Rollup:

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

Execution Process:

  1. Rollup Phase (executed on each time series separately):

    • web-server-01: rate([100, 110, 120, 130, 140, 150]) = 2 requests/minute
    • web-server-02: rate([200, 210, 220, 230, 240, 250]) = 2 requests/minute
  2. Aggregation Phase:

    • sum([2, 2]) = 4 requests/minute

Function Types

Common Rollup functions include:

Function Type Description Applicable Scenario
rate() Calculate growth rate Counter type metrics
increase() Calculate increase amount Counter type metrics
last() Get the last value Gauge type metrics
avg() Calculate average Data smoothing
max() Get the maximum value Peak analysis
min() Get the minimum value Valley analysis

But almost all aggregation functions that return a single value can be used, so the full list of functions is not listed here.

Default Rollup

If no Rollup function is explicitly specified, DQL does not perform Rollup calculation by default. PromQL's default Rollup is last, so if you are calculating Prometheus metrics, please understand this difference and manually specify the Rollup function.

Application Examples

// Calculate the total request rate for all servers
M::http_requests:(sum(request_count)) [rate]

// Calculate error rate
M::http_requests:(
    sum(error_count) as errors,
    sum(request_count) as requests
) [rate] BY service

Flexible Time Window Syntax

DQL supports various shorthand formats for time windows, making query writing more convenient.

Shorthand Formats

[1h]                             // Only specify time range
[1h::5m]                         // Time range + aggregation step
[1h:5m]                          // Start time + end time
[1h:5m:1m]                       // Start + end + step
[1h:5m:1m:avg]                   // Full format
[::5m]                           // Only specify aggregation step
[:::sum]                         // Only specify rollup function
[sum]                            // Only specify rollup function (simplest form)

Filter Conditions (where-clause)

Filter conditions are used to filter data rows, retaining only the data that meets the conditions for subsequent processing.

Basic Syntax

{condition1, condition2, condition3}

Multiple conditions can be connected using commas, AND, OR, &&, ||.

Comparison Operators

Operator Description Example
= Equal to host = 'web-01'
!= Not equal to status != 200
> Greater than cpu_usage > 80
>= Greater than or equal to memory_usage >= 90
< Less than response_time < 1000
<= Less than or equal to disk_usage <= 80

Pattern Matching Operators

Operator Description Example
=~ Regex match message =~ 'error.*\\d+'
!~ Regex not match message !~ 'debug.*'

Set Operators

Operator Description Example
IN In the set status IN [200, 201, 202]
NOT IN Not in the set level NOT IN ['debug', 'info']

Logical Operators

Operator Description Example
AND or && Logical AND cpu > 80 AND memory > 90
OR or | | Logical OR status = 500 OR status = 502
NOT Logical NOT NOT status = 200

Tip: In addition to performing logical OR, the OR operator also provides a null value fallback semantics - when the left expression returns NULL, it directly returns the result of the right expression, which can be used to implement priority fallback logic, such as status OR backup_status.

Application Examples

Basic Filtering

// Single condition
M::cpu:(usage) {host = 'web-01'} [1h]

// Multiple AND conditions
M::cpu:(usage) {host = 'web-01', usage > 80} [1h]

// Using AND keyword
M::cpu:(usage) {host = 'web-01' AND usage > 80} [1h]

// Mixing logical operators
M::cpu:(usage) {(host = 'web-01' OR host = 'web-02') AND usage > 80} [1h]

Regular Expression Matching

// Match error logs
L::logs:(message) {message =~ 'ERROR.*\\d{4}'} [1h]

// Match logs with specific format
L::logs:(message) {message =~ '\\[(ERROR|WARN)\\].*'} [1h]

// Exclude debug information
L::logs:(message) {message !~ 'DEBUG.*'} [1h]

// Hostname pattern matching
M::cpu:(usage) {host =~ 'web-.*\\.prod\\.com'} [1h]

Set Operations

// Status code filtering
L::nginx:(count(*)) {status IN [200, 201, 202, 204]} [1h]

// Exclude specific status codes
L::nginx:(count(*)) {status NOT IN [404, 500, 502]} [1h]

// Log level filtering
L::app_logs:(count(*)) {level IN ['ERROR', 'WARN', 'CRITICAL']} [1h]

Array Field Handling

When the field type is an array, DQL supports multiple array matching operations.

Assume field tags = ['web', 'prod', 'api']:

// Check if the array contains a certain value
{tags IN ['web']}           // true, because tags contains 'web'
{tags IN ['mobile']}        // false, because tags does not contain 'mobile'

// Check if the array does not contain a certain value
{tags NOT IN ['mobile']}    // true, because tags does not contain 'mobile'
{tags NOT IN ['web']}       // false, because tags contains 'web'

// Check if the array contains all specified values
{tags IN ['web', 'api']}           // true, contains 'web' and 'api'
{tags IN ['web', 'api', 'mobile']} // false, does not contain 'mobile'

The following syntax is retained for historical compatibility and is not recommended for use in new queries. The semantics of these operators on array fields differ from regular fields and can easily cause confusion.

// Historical syntax: Single value contains check (overloaded semantics of the equals operator)
{tags = 'web'}           // true, because tags contains 'web'
{tags = 'mobile'}        // false, because tags does not contain 'mobile'

// Historical syntax: Single value does not contain check (overloaded semantics of the not equals operator)
{tags != 'mobile'}       // true, because tags does not contain 'mobile'
{tags != 'web'}          // false, because tags contains 'web'

Function Filtering

Any function that returns a boolean value can be used as a filter condition.

// String matching
L::logs:(message) { match(message, 'error') }
L::logs:(message) { wildcard(message, 'error*') }

// Query requests with abnormal response times
L::access_logs:(*) {
    response_time > 1000 AND
    match(message, 'timeout')
}

// Query hosts with abnormal memory usage
M::memory:(usage) {
    (usage > 90 OR usage < 10) AND
    host =~ 'prod-.*' AND
    tags IN ['critical', 'important']
}

WHERE Subquery

WHERE subquery is a powerful feature in DQL for implementing dynamic filtering, allowing the result of one query to be used as the filter condition for another query. This mechanism supports dynamic filtering based on data analysis results.

Query Characteristics

  • Dynamic Filtering: The filter condition is not a fixed value, but is dynamically calculated through a query
  • Namespace Mixing: Supports cross-namespace queries, enabling correlation analysis of different data types
  • Serial Execution: The subquery executes first, and its results are used for filtering in the main query
  • Array Result: The subquery result is encapsulated as an array, so only IN and NOT IN operators are supported

Execution Flow

Take a typical WHERE subquery as an example:

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

Execution process:

  1. Subquery Execution: O::HOST:(hostname) {provider = 'cloud-a'}

    • Query all infrastructure objects
    • Filter out hosts with provider 'cloud-a'
    • Return hostname list: ['host-01', 'host-02', 'host-03']
  2. Main Query Execution: M::cpu:(avg(usage)) [1h] BY host {host IN [...]}

    • Query CPU usage data
    • Only count hosts returned by the subquery
    • Calculate average usage grouped by host

Application Examples

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


// Compare performance of different cloud providers
M::memory:(avg(used / total * 100)) { host IN (O::HOST:(hostname) {provider IN ['cloud-a', 'cloud-b']}) } [1h] BY host


// Analyze logs for a specific business
L::app_logs:(count(*)) { service IN (T::services:(service_name) {business_unit = 'ecommerce'}) } [1h] BY level


// Monitor application performance for critical business services
M::response_time:(avg(response_time)) { service IN (T::services:(service_name) {criticality = 'high'}) } [1h] BY service

Group By (group-by-clause)

Grouping is a core function of data analysis, used to group and aggregate data by specified dimensions.

Basic Syntax

BY expression1, expression2, ...

Grouping Types

Field Grouping

// Single field grouping
M::cpu:(avg(usage)) [1h] BY host

// Multi-field grouping
M::cpu:(avg(usage)) [1h] BY host, env

// Nested grouping
M::cpu:(avg(usage)) [1h] BY datacenter, rack, host

Expression Grouping

// Compound mathematical expression
M::memory:(avg(used)) [1h] BY ((used / total) * 100) as
usage_percent

// Multi-field mathematical operation
M::performance:(avg(response_time)) [1h] BY
(response_time / 1000) as response_seconds

Function Grouping

// Drain clustering algorithm
L::logs:(count(*)) BY drain(message, 0.7) as sample

// Regex extraction grouping
L::logs:(count(*)) [1h] BY regexp_extract(message, 'error_code: (\\d+)', 1)

Group Result Handling

When the query includes both grouping and time windows, a two-dimensional data structure is generated. Group By can be used together with time windows, and the query result will be a two-dimensional array. The first dimension of this two-dimensional array is multiple groups differentiated by the group key, and the second dimension is the multi-time interval data within a single group.

Two-dimensional data structure example:

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

Query Result Structure:

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

Further processing of this two-dimensional array:

  1. To filter the results of this two-dimensional array, use the Having clause
  2. To sort or paginate data within a single group of the two-dimensional array, use the order by, limit, offset series of statements
  3. To sort or paginate the groups of the two-dimensional array, use the sorder by, slimit, soffset series of statements

For detailed descriptions of the two-dimensional data structure and sorting/pagination functionality, please refer to Sorting and Pagination.

Having Clause (having-clause)

The Having clause is used to filter the results after group aggregation, similar to the WHERE clause, but acts on the aggregated data.

Basic Syntax

HAVING condition

Difference from WHERE

WHERE and HAVING are both clauses used for filtering data, but they act at different stages of query execution and handle different types of filter conditions.

Execution Sequence Difference

Raw Data → WHERE Filtering → Group Aggregation → HAVING Filtering → Final Result
  1. WHERE Clause:

    • Executed before group aggregation
    • Acts on raw data rows
    • Filters out data rows that do not meet the conditions, reducing the amount of data for subsequent processing
  2. HAVING Clause:

    • Executed after group aggregation
    • Acts on the aggregated results
    • Filters based on the results of aggregation functions

Application Scenarios

The HAVING clause is suitable for filtering aggregation results:

// Filter based on aggregation function value
M::cpu:(avg(usage) as avg_usage) [1h] BY host HAVING avg_usage > 80

// Filter based on multiple aggregation conditions
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

// Filter based on group statistics
L::logs:(count(*) as count) [1h] BY service HAVING count > 100

// Filter based on compound aggregation conditions
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 and Pagination

Sorting and pagination in DQL is a very important and unique feature, designed with a dual sorting mechanism for time series data: intra-group sorting and inter-group sorting. This design allows DQL to efficiently handle complex multi-dimensional time series data analysis needs.

Understanding DQL's Data Structure

Before delving into sorting and pagination, let's first understand the two-dimensional data structure of DQL query results. When a query includes both grouping (BY) and time windows, a two-dimensional array is generated:

  • First Dimension (Group Dimension): Multiple groups differentiated by the group key
  • Second Dimension (Time Dimension): Data aggregated by time window within each group

For detailed descriptions of the two-dimensional data structure and JSON format examples, please refer to Group Result Handling. This two-dimensional structure is the foundation of DQL's sorting and pagination functionality. Understanding this structure is crucial for mastering DQL's sorting mechanism.

Intra-Group Sorting and Pagination (ORDER BY, LIMIT, OFFSET)

Intra-group sorting and pagination act on the second dimension of the two-dimensional structure, i.e., the data within each group. This sorting is executed independently within each group and does not affect data in other groups.

Basic Syntax

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

Execution Mechanism

The execution process of intra-group sorting:

  1. Group Processing: Execute the sorting operation independently for each group
  2. Sorting Basis: Can use time fields, aggregation function results, or calculated expressions
  3. Pagination Limit: LIMIT limits the number of data rows returned per group
  4. Offset Handling: OFFSET skips the first N rows of data in each group

Application Examples

Basic Time Sorting
// Sort by time in descending order, showing the latest data for each host
M::cpu:(max(usage_total)) [1h::10m] BY host ORDER BY time DESC

// Sort by time in ascending order, showing historical trends
M::cpu:(max(usage_total)) [1h::10m] BY host ORDER BY time ASC

Execution Result (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
      ]
    }
  ]
}
Numeric-Based Sorting
// Sort by CPU usage in descending order, find the peak period for each host
M::cpu:(max(usage_total) as max_usage_total) [1h::10m] BY host ORDER BY max_usage_total DESC

// Sort by response time in ascending order, find the best performance period
M::response_time:(avg(response_time) as avg_response_time) [1h::5m] BY endpoint ORDER BY avg_response_time ASC
Intra-Group Pagination
// Show only the latest 3 data points per host
M::cpu:(max(usage_total)) [1h::10m] BY host ORDER BY time DESC LIMIT 3

// Skip the latest 2 data points, show the next 3
M::cpu:(max(usage_total)) [1h::10m] BY host ORDER BY time DESC LIMIT 3 OFFSET 2

Execution Result (LIMIT 3):

{
  "series": [
    {
      "columns": ["time", "max(usage_total)"],
      "name": "cpu",
      "tags": {"host": "web-server-01"},
      "values": [
        [1721059200000, 78.5],  // 12:00:00 - Latest
        [1721058600000, 82.3],  // 11:50:00
        [1721058000000, 75.8]   // 11:40:00
        // Only returns the first 3 data points
      ]
    },
    {
      "columns": ["time", "max(usage_total)"],
      "name": "cpu",
      "tags": {"host": "web-server-02"},
      "values": [
        [1721059200000, 45.2],  // 12:00:00 - Latest
        [1721058600000, 52.8],  // 11:50:00
        [1721058000000, 48.5]   // 11:40:00
        // Only returns the first 3 data points
      ]
    }
  ]
}

Inter-Group Sorting and Pagination (SORDER BY, SLIMIT, SOFFSET)

Inter-group sorting and pagination are unique features of DQL, acting on the first dimension of the two-dimensional structure, i.e., sorting the groups themselves. This sorting requires reducing each group's data to a single value, then comparing these values across different groups.

When the query does not use BY, the result usually has only one group. In this case, SORDER BY typically has no visible effect on sorting, but SLIMIT / SOFFSET still operate according to the "group count" semantics (i.e., for that single group).

Basic Syntax

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

Execution Mechanism

The execution process of inter-group sorting:

  1. Dimensionality Reduction Calculation: Apply an aggregation function to each group to calculate a representative value
  2. Group Sorting: Sort all groups based on the reduced value
  3. Group Pagination: SLIMIT limits the number of groups returned, SOFFSET skips the first N groups

Selecting Dimensionality Reduction Functions

Inter-group sorting must use an aggregation function for dimensionality reduction. When no aggregation function is specified, the default aggregation function used is last.

Commonly used dimensionality reduction functions include:

Function Description Applicable Scenario
last() Get the last value Suitable for the current state of a time series
max() Get the maximum value Suitable for peak analysis
min() Get the minimum value Suitable for valley analysis
avg() Calculate the average value Suitable for overall trend analysis
sum() Summation Suitable for total statistics
count() Count Suitable for frequency analysis

But almost all aggregation functions that return a single value can be used, so the full list of functions is not listed here.

Application Examples

Average-Based Sorting
// Sort by average CPU usage in descending order, find the hosts with the highest load
M::cpu:(avg(usage)) [1h::10m] BY host SORDER BY avg(usage) DESC SLIMIT 5

// Sort by average response time in ascending order, find the services with the best performance
M::response_time:(avg(response_time)) [1h] BY service SORDER BY avg(response_time) ASC SLIMIT 10

Execution Process Analysis:

  1. Dimensionality Reduction Calculation:

    • web-server-01: avg(usage) = 77.7
    • web-server-02: avg(usage) = 53.7
    • web-server-03: avg(usage) = 90.9
  2. Group Sorting (by avg(usage) DESC):

    • web-server-03: 90.9
    • web-server-01: 77.7
    • web-server-02: 53.7
  3. Final Result (SLIMIT 2):

{
  "series": [
    {
      "columns": ["time", "avg(usage)"],
      "name": "cpu",
      "tags": {"host": "web-server-03"},  // Average usage: 90.9 - Ranked first
      "values": [
        [1721059200000, 92.1],
        [1721058600000, 95.7],
        [1721058000000, 89.4]
      ]
    },
    {
      "columns": ["time", "avg(usage)"],
      "name": "cpu",
      "tags": {"host": "web-server-01"},  // Average usage: 77.7 - Ranked second
      "values": [
        [1721059200000, 78.5],
        [1721058600000, 82.3],
        [1721058000000, 75.8]
      ]
    }
    // web-server-02 (avg: 53.7) is filtered out because SLIMIT 2
  ]
}
Application Examples
// Sort by maximum CPU usage, find hosts with abnormal peak values
M::cpu:(max(usage_total)) [1h::10m] BY host SORDER BY max(usage_total) DESC SLIMIT 10

// Sort by minimum memory usage, find hosts with the lowest resource utilization
M::memory:(min(usage_percent)) [24h::1h] BY host SORDER BY min(usage_percent) ASC SLIMIT 5

// Sort by the latest CPU usage, find hosts with the highest current load
M::cpu:(usage) [1h::10m] BY host SORDER BY last(usage) DESC SLIMIT 10

// Sort by the latest error rate, find services with the most current problems
L::logs:(count(*) as error_count) {level = 'error'} [1h] BY service SORDER BY error_count DESC SLIMIT 5

Combined Use of Dual Sorting and Pagination

In practical applications, intra-group sorting and inter-group sorting are often used together to achieve complex data display requirements. This combination can control both the order of groups and the order of data within groups.

Execution Order

The execution order of dual sorting:

  1. Inter-Group Sorting: First sort and paginate all groups
  2. Intra-Group Sorting: Then sort and paginate the selected groups internally

Application Examples

Monitoring Dashboard Scenario
// Find the 10 servers with the highest CPU usage, showing the latest 5 data points for each
M::cpu:(avg(usage)) [1h::10m] BY host
SORDER BY avg(usage) DESC SLIMIT 10    // Inter-group sorting: find the top 10 with highest usage
ORDER BY time DESC LIMIT 5             // Intra-group sorting: show the latest 5 points for each

Execution Result:

{
  "series": [
    {
      "columns": ["time", "avg(usage)"],
      "name": "cpu",
      "tags": {"host": "web-server-03"},  // Average usage: 90.9 - Ranked first
      "values": [
        [1721059200000, 92.1],  // 12:00:00 - Latest
        [1721058600000, 95.7],  // 11:50:00
        [1721058000000, 89.4],  // 11:40:00
        [1721057400000, 97.6],  // 11:30:00
        [1721056800000, 87.3]   // 11:20:00
        // Only returns the latest 5 data points (LIMIT 5)
      ]
    },
    {
      "columns": ["time", "avg(usage)"],
      "name": "cpu",
      "tags": {"host": "web-server-01"},  // Average usage: 77.7 - Ranked second
      "values": [
        [1721059200000, 78.5],  // 12:00:00 - Latest
        [1721058600000, 82.3],  // 11:50:00
        [1721058000000, 75.8],  // 11:40:00
        [1721057400000, 88.2],  // 11:30:00
        [1721056800000, 72.1]   // 11:20:00
        // Only returns the latest 5 data points (LIMIT 5)
      ]
    }
    // Other hosts are filtered out because SLIMIT 10 only returns the top 10 with highest usage
  ]
}

By mastering DQL's sorting and pagination features, you can build powerful monitoring dashboards, performance analysis tools, and business insight systems.

Note

Proper use of the combination of intra-group sorting and inter-group sorting can greatly improve the efficiency and effectiveness of data analysis.

Feedback

Is this page helpful? ×