DQL¶
DQL(Debug Query Language)은 Guance 플랫폼의 핵심 쿼리 언어로, 시계열 데이터, 로그 데이터, 이벤트 데이터 등을 효율적으로 쿼리하고 분석하기 위해 설계되었습니다. 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]
index는 생략 가능하며, 생략 시 기본적으로 default 인덱스를 사용합니다.
실행 순서¶
DQL 쿼리의 실행 순서는 쿼리의 의미와 성능을 결정하므로 매우 중요합니다.
-
데이터 필터링: namespace::datasource, where-clause, time-expr을 기반으로 데이터 필터링
- 데이터 소스 확인
- WHERE 조건을 적용하여 원본 데이터 행 필터링
- 시간 범위 필터링 적용
- 이 단계에서 가능한 한 빨리 데이터를 필터링하여 후속 처리 효율성 향상
-
시간 집계: time-expr에 rollup이 포함된 경우 rollup 로직을 먼저 실행
- Rollup 함수는 시간 차원에서 데이터를 사전 처리
- Counter 유형 메트릭의 경우 일반적으로 원시 값 대신 비율 또는 증분을 계산
- Gauge 유형 메트릭의 경우 last, avg 등의 집계 함수를 사용할 수 있음
-
그룹 집계: group-by-clause를 실행하여 그룹화하고, 그룹 내에서 select-clause의 집계 함수 실행
- BY 절의 표현식에 따라 데이터 그룹화
- 각 그룹 내에서 집계 함수(sum, count, avg, max, min 등) 계산
- 시간 창이 함께 있는 경우 2차원 데이터 구조 형성
-
그룹 필터링: having-clause를 실행하여 집계된 그룹 필터링
- HAVING 절은 집계된 결과에 적용
- 집계 함수 결과를 사용하여 필터링 가능
- 이는 WHERE 절과의 주요 차이점
-
비집계 함수: select-clause의 비집계 함수 실행
- 집계가 필요 없는 표현식 및 함수 처리
- 집계 결과에 대한 추가 계산 수행
-
그룹 내 정렬: order-by-clause, limit-clause를 실행하여 그룹 내 데이터 정렬 및 페이지 매김
- ORDER BY는 각 그룹 내에서 독립적으로 실행
- LIMIT은 각 그룹이 반환하는 데이터 행 수를 제한
-
그룹 간 정렬: sorder-by-clause, slimit-clause, soffset-clause를 실행하여 그룹 정렬 및 페이지 매김
- SORDER BY는 그룹 자체를 정렬
- 그룹 결과를 단일 차원으로 축소해야 함(max, avg, last 등 함수 사용)
- SLIMIT은 반환되는 그룹 수를 제한
전체 예제¶
전체 예제를 통해 DQL의 구조를 이해해 보겠습니다.
M::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, 미복구 이벤트 데이터 | 해결되지 않은 알림 및 이벤트 |
| B | Cloud billing, 클라우드 청구 |
인덱스(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 // 웹 및 API 로그 쿼리
인덱스와 성능¶
인덱스를 적절히 사용하면 쿼리 성능을 크게 향상시킬 수 있습니다.
- 정확한 인덱스: 데이터가 어떤 인덱스에 있는지 명확히 알 때 해당 인덱스를 직접 지정
- 다중 인덱스 쿼리: 여러 인덱스에 걸쳐 쿼리해야 할 때 와일드카드 대신 다중 인덱스 구문 사용
- 전체 인덱스 스캔 방지: 인덱스와 WHERE 조건의 조합을 통해 데이터 스캔 범위를 최대한 줄임
호환 구문(권장하지 않음)¶
역사적 이유로 DQL은 where 절에서 인덱스를 지정하는 것도 지원하지만, 권장되지 않습니다.
적용 예제¶
// 프로덕션 환경의 CPU 사용률 쿼리
M::cpu:(avg(usage)) [1h] BY host
// 프로덕션 및 스테이징 환경 비교 쿼리
M("production", "staging")::cpu:(avg(usage)) [1h] BY index, host
// 웹 서버 로그 분석
L("web-logs")::nginx:(count(*)) {status >= 400} [1h] BY status
// 웹 및 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::re('cpu.*'):(usage) // cpu로 시작하는 메트릭 쿼리
L::re('web.*'):(count(*)) // web으로 시작하는 로그 쿼리
T::re('.*-service'):(traces) // -service로 끝나는 서비스 쿼리
서브쿼리 데이터 소스¶
서브쿼리는 DQL에서 복잡한 분석을 구현하는 중요한 기능으로, 하나의 쿼리 결과를 다른 쿼리의 데이터 소스로 사용할 수 있도록 합니다. 이러한 중첩 쿼리 메커니즘은 다단계 분석 요구 사항을 지원합니다.
실행 메커니즘¶
서브쿼리 실행은 다음 원칙을 따릅니다.
- 직렬 실행: 내부 서브쿼리가 먼저 실행된 후, 그 결과가 외부 쿼리의 데이터 소스로 사용됨
- 결과 캡슐화: 서브쿼리 결과는 임시 테이블 구조로 캡슐화되어 외부 쿼리에서 사용됨
- 네임스페이스 혼합: 서브쿼리는 서로 다른 네임스페이스의 혼합 쿼리를 지원하여 데이터 유형 간 분석 가능
- 성능 고려 사항: 서브쿼리는 계산 복잡성을 증가시키므로 쿼리 로직을 합리적으로 설계해야 함
기본 구문¶
실행 과정¶
일반적인 서브쿼리 예제를 살펴보겠습니다.
실행 과정은 다음과 같습니다.
-
내부 서브쿼리:
L::*:(count(*)) {level = 'error'} BY app_id- 모든 로그 데이터 스캔
- 오류 수준 로그 필터링
- app_id별로 그룹화하여 오류 수 집계
- 임시 테이블 생성:
app_id | count(*)
-
외부 쿼리:
L::(...):(count_distinct(app_id))- 서브쿼리 결과를 데이터 소스로 사용
- 서로 다른 app_id 수 집계
- 최종 결과: 오류가 있는 애플리케이션 수
적용 예제¶
// 오류가 있는 애플리케이션 수 집계
L::(L::*:(count(*)) {level = 'error'} BY app_id):(count_distinct(app_id))
// CPU 사용률이 높은 서버 분석
M::(M::cpu:(avg(usage)) [1h] BY host {avg(usage) > 80}):(count(host))
// 오류율이 1%를 초과하는 서비스 엔드포인트를 찾은 후, 영향을 받는 서비스 수 집계
M::(M::http_requests:(sum(request_count), sum(error_count)) [1h] BY service, endpoint
{sum(error_count) / sum(request_count) > 0.01}
):(count(service))
Select 절(select-clause)¶
Select 절은 쿼리에서 반환할 필드 또는 표현식을 지정하는 데 사용되며, DQL 쿼리의 가장 기본적이면서 중요한 부분 중 하나입니다.
필드 선택¶
기본 구문¶
필드명 규칙¶
필드명은 다음과 같은 형식으로 작성할 수 있습니다.
-
이름 직접 작성: 일반 식별자에 적합
- ✅
message - ✅
host_name - ✅
response_time
- ✅
-
백틱으로 감싸기: 특수 문자 또는 키워드가 포함된 필드명에 적합
- ✅
message - ✅
limit - ✅
host-name - ✅
column with spaces
- ✅
-
피해야 할 작성법: 작은따옴표와 큰따옴표로 감싸면 문자열이 되며 필드명이 아닙니다.
- ❌
'message' - ❌
"message"
- ❌
JSON 필드 추출¶
데이터 필드에 JSON 형식의 콘텐츠가 포함된 경우, JSON Path 구문 하위 집합을 사용하여 내부 필드의 데이터를 추출할 수 있습니다.
기본 구문¶
JSON Path 구문¶
- 점 표기법으로 객체 속성 접근:
.field_name - 대괄호 표기법으로 객체 속성 접근:
["key"](공백 또는 특수 문자가 포함된 키에 적합) - 배열 인덱스로 값 접근:
[index]
적용 예제¶
다음과 같은 JSON 로그 데이터가 있다고 가정합니다.
{
"message": "User login attempt",
"request": {
"method": "POST",
"path": "/api/login",
"headers": {
"user-agent": "Mozilla/5.0",
"content-type": "application/json"
},
"body": {
"username": "john.doe",
"password": "***",
"permissions": ["read", "write", "admin"]
}
},
"response": {
"status": 200,
"time": 156,
"data": [
{"id": 1, "name": "user1"},
{"id": 2, "name": "user2"}
]
}
}
// 요청 메서드 추출
L::auth_logs:(message@request.method)
// 요청 경로 추출
L::auth_logs:(message@request.path)
// 사용자 이름 추출
L::auth_logs:(message@request.body.username)
// 응답 상태 추출
L::auth_logs:(message@response.status)
// User-Agent 추출(하이픈 포함, 대괄호 필요)
L::auth_logs:(message@request.headers["user-agent"])
// 권한 배열의 첫 번째 요소 추출
L::auth_logs:(message@request.body.permissions[0])
// 응답 데이터의 첫 번째 객체 name 추출
L::auth_logs:(message@response.data[0].name)
// 서로 다른 요청 메서드 수 집계
L::auth_logs:(count(*)) [1h] BY message@request.method
// 응답 시간 분포 분석
L::auth_logs:(avg(message@response.time), max(message@response.time)) [1h] BY message@request.method
// 여러 필드 추출
L::auth_logs:(
message,
message@request.method as method,
message@request.path as path,
message@response.status as status,
message@response.time as response_time
) {message@response.status >= 400} [1h]
계산 필드¶
표현식 계산¶
기본 산술 연산을 지원합니다.
// 단위 변환(밀리초를 초로)
L::nginx:(response_time / 1000) as response_time_seconds
// 백분율 계산
M::memory:(used / total * 100) as usage_percentage
// 복합 계산
M::network:((bytes_in + bytes_out) / 1024 / 1024) as total_traffic_mb
함수 계산¶
다양한 집계 및 변환 함수를 지원합니다.
// 집계 함수
M::cpu:(max(usage), min(usage), avg(usage)) [1h] BY host
// 변환 함수
L::logs:(int(response_time) as response_time_seconds)
L::logs:(floor(response_time) as response_time_seconds)
조건부 표현식¶
CASE WHEN은 쿼리에서 조건에 따라 다른 값을 선택하는 데 사용됩니다. 일반적으로 집계 함수와 함께 사용되어 조건부 카운트, 조건부 합계 또는 조건에 따른 필드 정규화를 수행합니다.
기본 구문¶
동일한 필드에 대해 여러 값과 일치시키려면 단순 CASE 구문을 사용할 수도 있습니다.
WHEN은 작성된 순서대로 일치를 시도하며, 첫 번째 조건이 일치하면 해당 THEN 값을 반환합니다. 모두 일치하지 않으면 ELSE 값을 반환합니다. ELSE가 명시적으로 작성되지 않은 경우 기본적으로 nil을 반환합니다.
적용 예제¶
조건부 합계: 5xx 요청의 트래픽만 집계
조건부 카운트: 오류 요청 수 집계
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,regexp_replace
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에서 광범위한 상세 스캔이 트리거되지 않도록 하는 것이 좋습니다.
별칭¶
필드 또는 표현식에 별칭을 지정하여 결과를 더 읽기 쉽게 만들고 후속 참조를 용이하게 합니다.
기본 구문¶
적용 예제¶
// 간단한 별칭
M::cpu:(avg(usage) as avg_usage, max(usage) as max_usage) [1h] BY host
// 표현식 별칭
M::memory:((used / total) * 100 as usage_percent) [1h] BY host
// 함수 별칭
L::logs:(count(*) as error_count) {level = 'error'} [1h] BY service
// JSON 추출 별칭
L::api_logs:(
message@request.method as http_method,
message@response.status as http_status,
message@response.time as response_time_ms
) [1h]
사용 팁¶
DQL에서는 집계 함수의 결과를 원래 필드 이름으로 직접 참조할 수 있으므로 별칭 사용을 줄일 수 있습니다.
M::cpu:(max(usage)) [1h] BY host
// 결과에 max(usage) 열이 포함되며, 이후에 직접 usage 열 이름을 사용하여 서브쿼리 결과의 `max(usage)` 열을 가져올 수 있습니다.
M::(M::cpu:(max(usage)) [1h] BY host):(max(usage)) { usage > 80 }
그러나 여러 집계 함수가 동일한 필드를 사용하는 경우 후속에서 올바르게 구분하려면 별칭을 사용해야 합니다.
시간 절(time-clause)¶
시간 절은 DQL의 핵심 기능 중 하나로, 쿼리의 시간 범위, 집계 시간 창 및 Rollup 집계 함수를 지정하는 데 사용됩니다.
기본 구문¶
시간 범위¶
절대 타임스탬프¶
상대 시간¶
여러 시간 단위를 지원하며 혼합하여 사용할 수 있습니다.
[1h] // 현재부터 지난 1시간
[1h:5m] // 지난 1시간 전부터 지난 5분 전까지
[1h30m] // 지난 1시간 30분
[2h15m30s] // 지난 2시간 15분 30초
시간 표현식 설명¶
| 단위 | 설명 | 예제 |
|---|---|---|
| s | 초 | 30s |
| m | 분 | 5m |
| h | 시간 | 2h |
| d | 일 | 7d |
| w | 주 | 4w |
| y | 년 | 1y |
시간 표현식을 시간 절에서 사용하면 현재 시간을 기준으로 한 이전 오프셋을 나타내며, 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 열은 각 시간 창의 시작 시간을 나타냅니다.
단일 시간 창¶
전체 시간 범위가 하나의 값으로 집계됩니다.
쿼리 결과:
시간 창 집계¶
시간 간격별로 그룹화하여 집계합니다.
쿼리 결과:
{
"columns": ["time", "max(usage_total)"],
"values": [
[1721059200000, 37.46],
[1721058600000, 34.12],
[1721058000000, 33.81],
[1721057400000, 30.92],
[1721058000000, 34.53],
[1721057400000, 36.11]
]
}
Rollup 함수¶
Rollup 함수는 DQL의 중요한 사전 처리 단계로, 그룹 집계보다 먼저 실행되어 원시 시계열 데이터를 사전 처리합니다.
실행 순서¶
쿼리 실행 흐름에서 Rollup의 위치:
실행 메커니즘¶
Rollup의 실행 과정은 두 단계로 나뉩니다.
- 시계열별 처리: 각 독립적인 시계열에 개별적으로 Rollup 함수 적용
- 집계 계산: Rollup 처리된 결과에서 그룹 집계 실행
Rollup 약어, Rollup 함수 호출 및 명시적 집계 호출¶
DQL에는 혼동하기 쉬운 세 가지 시계열 함수 작성 방식이 있습니다.
- Rollup 약어: 시간 절에 함수 이름만 작성, 예:
[rate],[1h::5m:slope] - Rollup 함수 호출: 시간 절에 Rollup 함수에 알고리즘 매개변수 전달, 예:
[1h::1m:ewma(0.3)],[1h::1m:moving_average(5)],[1h::1m:percentile(95)] - 명시적 집계 호출: Select 절에 완전한 함수 호출 작성, 예:
rate(request_count),ewma(usage, 0.3),corr(cpu_usage, request_count)
세 가지 작성 방식의 주요 차이점은 실행 단계와 매개변수 기능입니다.
| 작성 방식 | 예제 | 실행 단계 | 적용 시나리오 |
|---|---|---|---|
| Rollup 약어 | [1h::5m:rate] |
그룹 집계 전, 각 원시 시계열별로 실행 | 먼저 각 시계열을 사전 처리한 후 그룹 집계 수행 |
| Rollup 함수 호출 | [1h::1m:ewma(0.3)] |
그룹 집계 전, 각 원시 시계열별로 실행 | Rollup 함수에 추가 알고리즘 매개변수가 필요한 경우 |
| 명시적 집계 호출 | ewma(usage, 0.3) |
Select 절 집계 단계 | 필드, 추가 매개변수 또는 여러 입력 필드를 지정해야 하는 경우 |
시간 절의 Rollup 입력 필드는 Select 필드, 시간 창 및 원시 시계열에 의해 결정되므로 Rollup 함수 호출은 추가 알고리즘 매개변수만 전달하며 시간 절에 필드 이름을 전달하지 않습니다. 단일 입력, 추가 매개변수가 없는 시계열 함수는 일반적으로 Rollup 약어를 지원할 수 있습니다. 추가 알고리즘 매개변수가 필요한 단일 입력 함수는 Rollup 함수 호출을 지원할 수 있습니다. 여러 입력 필드가 필요한 함수는 명시적 집계 호출을 사용해야 합니다.
예제: Counter 메트릭을 먼저 Rollup한 후 집계
Counter 메트릭은 먼저 각 원시 시계열에서 증가율을 계산한 다음 비즈니스 차원별로 합산해야 합니다.
// 권장: 먼저 각 시계열에 대해 rate를 계산한 다음 service별로 합산
M::http_requests:(sum(request_count)) [1h::5m:rate] BY service
// 권장하지 않음: Counter 원시 누적 값을 직접 집계하면 QPS가 아님
M::http_requests:(sum(request_count)) [1h::5m] BY service
예제: 추가 알고리즘 매개변수가 필요한 경우 Rollup 함수 호출 사용
ewma는 평활 계수 alpha를 명시적으로 전달해야 합니다. 먼저 각 원시 시계열에 대해 EWMA를 수행한 다음 그룹 집계에 들어가려면 시간 절에 작성할 수 있습니다.
// Rollup 함수 호출: 먼저 각 시계열에 대해 EWMA를 수행한 다음 host별로 평균 계산
M::cpu:(avg(usage)) [1h::1m:ewma(0.3)] BY host
// 오류: ewma는 alpha가 필요하므로 함수 이름만 쓸 수 없음
M::cpu:(avg(usage)) [1h::1m:ewma] BY host
// 오류: 시간 절의 Rollup 매개변수는 알고리즘 매개변수만 작성하고 필드 이름은 작성하지 않음
M::cpu:(avg(usage)) [1h::1m:ewma(usage, 0.3)] BY host
Select 집계 단계에서 EWMA를 계산하려면 명시적 집계 호출을 사용할 수도 있습니다.
시간 절 Rollup에 적합한 단일 숫자 매개변수 함수는 다음과 같습니다.
// 먼저 각 시계열에 대해 5점 이동 평균을 수행한 다음 host별로 평균 계산
M::cpu:(avg(usage)) [1h::1m:moving_average(5)] BY host
// 먼저 각 시계열에 대해 P95를 구한 다음 service별로 평균 계산
M::response_time:(avg(duration)) [1h::5m:percentile(95)] BY service
예제: 여러 입력 필드가 필요한 경우 명시적 집계 호출 사용
corr는 두 개의 입력 필드가 필요하므로 시간 절 Rollup에 작성할 수 없습니다.
// 올바름: Select 절에서 두 필드를 명시적으로 지정
M::service_metric:(corr(cpu_usage, request_count)) [1h::5m] BY service
// 오류: 시간 절 Rollup은 두 개의 입력 필드를 표현할 수 없음
M::service_metric:(avg(cpu_usage)) [1h::5m:corr] BY service
예제: 동일한 이름의 함수 실행 단계가 다른 경우
함수가 Rollup 약어와 명시적 집계 호출을 모두 지원하는 경우 두 작성 방식은 서로 다른 실행 단계를 나타내며 완전히 동등하다고 기본적으로 이해해서는 안 됩니다.
// Rollup 약어: 먼저 각 원시 시계열에서 zscore를 계산한 다음 그룹 집계에 들어감
M::cpu:(max(usage)) [1h::5m:zscore] BY host
// 명시적 집계 호출: Select 집계 단계에서 usage에 대해 zscore 계산
M::cpu:(zscore(usage)) [1h::5m] BY host
적용 시나리오¶
Rollup 함수의 일반적인 적용 시나리오는 Counter 메트릭 처리입니다.
Prometheus의 Counter 유형 메트릭의 경우 원시 값을 직접 집계하는 것은 의미가 없습니다. Counter는 단조 증가하기 때문입니다. 먼저 각 시계열의 증가율을 계산한 다음 집계해야 합니다.
*문제 예제: 두 서버의 요청 카운터가 있다고 가정합니다.
{
"host": "web-server-01",
"data": [
{"time": "2024-07-15 08:25:00", "request_count": 150},
{"time": "2024-07-15 08:20:00", "request_count": 140},
{"time": "2024-07-15 08:15:00", "request_count": 130},
{"time": "2024-07-15 08:10:00", "request_count": 120},
{"time": "2024-07-15 08:05:00", "request_count": 110},
{"time": "2024-07-15 08:00:00", "request_count": 100}
]
}
{
"host": "web-server-02",
"data": [
{"time": "2024-07-15 08:25:00", "request_count": 250},
{"time": "2024-07-15 08:20:00", "request_count": 240},
{"time": "2024-07-15 08:15:00", "request_count": 230},
{"time": "2024-07-15 08:10:00", "request_count": 220},
{"time": "2024-07-15 08:05:00", "request_count": 210},
{"time": "2024-07-15 08:00:00", "request_count": 200}
]
}
직접 집계의 문제점:
- web-server-01의 request_count 시작 값은 100
- web-server-02의 request_count 시작 값은 200
- 두 서버의 요청 속도는 동일하지만(둘 다 5분당 10개 요청), 절대값이 다름
Rollup 사용 솔루션:
실행 과정:
-
Rollup 단계(각 시계열에서 개별적으로 실행):
- web-server-01: rate([100, 110, 120, 130, 140, 150]) = 2 요청/분
- web-server-02: rate([200, 210, 220, 230, 240, 250]) = 2 요청/분
-
집계 단계:
- sum([2, 2]) = 4 요청/분
함수 유형¶
일반적인 Rollup 함수는 다음과 같습니다.
| 함수 유형 | 설명 | 적용 시나리오 |
|---|---|---|
rate() |
증가율 계산 | Counter 유형 메트릭 |
increase() |
증가량 계산 | Counter 유형 메트릭 |
last() |
마지막 값 가져오기 | Gauge 유형 메트릭 |
avg() |
평균값 계산 | 데이터 평활화 |
max() |
최대값 가져오기 | 피크 분석 |
min() |
최소값 가져오기 | 저점 분석 |
그러나 단일 값을 반환하는 거의 모든 집계 함수를 사용할 수 있으므로 여기서는 전체 함수 목록을 나열하지 않습니다.
기본 Rollup¶
Rollup 함수를 명시적으로 지정하지 않으면 DQL은 기본적으로 Rollup 계산을 수행하지 않습니다. PromQL의 기본 Rollup은 last이므로 Prometheus 메트릭을 계산하는 경우 이 차이를 반드시 이해하고 Rollup 함수를 수동으로 지정해야 합니다.
적용 예제¶
// 모든 서버의 총 요청 속도 계산
M::http_requests:(sum(request_count)) [rate]
// 오류율 계산
M::http_requests:(
sum(error_count) as errors,
sum(request_count) as requests
) [rate] BY service
시간 창 유연한 구문¶
DQL은 다양한 시간 창의 약어 형식을 지원하여 쿼리 작성을 더 편리하게 만듭니다.
약어 형식¶
[1h] // 시간 범위만 지정
[1h::5m] // 시간 범위 + 집계 간격
[1h:5m] // 시작 시간 + 종료 시간
[1h:5m:1m] // 시작 + 종료 + 간격
[1h:5m:1m:avg] // 전체 형식
[::5m] // 집계 간격만 지정
[:::sum] // rollup 함수만 지정
[sum] // rollup 함수만 지정(가장 간단한 형식)
시간 이동(SHIFT)¶
SHIFT는 전체 쿼리의 시간 창을 전체적으로 앞으로 이동하여 지정된 기간 이전의 데이터를 읽고, 결과의 시간 열은 여전히 현재 창을 표시합니다.
SHIFT는 시간 창 바로 뒤에 위치하며, BY, HAVING, ORDER BY 등의 절 앞에 옵니다.
필터링, 그룹화, 집계, 정렬 및 페이지 매김은 이동 전 데이터를 기준으로 실행됩니다. 결과의 구조는 SHIFT가 없는 쿼리와 동일하며, 시간 열만 현재 창에 유지됩니다. duration은 1h, 7d와 같은 양의 고정 시간이어야 합니다.
중첩된 SELECT는 각각 쿼리 수준 SHIFT를 선언할 수 있으며, 오프셋은 계층별로 누적됩니다. 쿼리 수준 SHIFT와 표현식 수준 SHIFT(DQL 함수 참조의 SHIFT 참조)는 함께 사용할 수 있습니다. 쿼리 수준 오프셋이 먼저 전체 쿼리의 기준 창을 결정한 다음, 표현식 수준 오프셋이 해당 기준 창을 기준으로 더 이전의 집계 값을 읽습니다.
필터 조건(where-clause)¶
필터 조건은 데이터 행을 필터링하여 조건을 만족하는 데이터만 후속 처리에 사용합니다.
기본 구문¶
여러 조건은 쉼표, AND, OR, &&, ||로 연결할 수 있습니다.
비교 연산자¶
| 연산자 | 설명 | 예제 |
|---|---|---|
= |
같음 | host = 'web-01' |
!= |
같지 않음 | status != 200 |
> |
큼 | cpu_usage > 80 |
>= |
크거나 같음 | memory_usage >= 90 |
< |
작음 | response_time < 1000 |
<= |
작거나 같음 | disk_usage <= 80 |
패턴 일치 연산자¶
| 연산자 | 설명 | 예제 |
|---|---|---|
=~ |
정규 표현식 일치 | message =~ 'error.*\\d+' |
!~ |
정규 표현식 불일치 | message !~ 'debug.*' |
집합 연산자¶
| 연산자 | 설명 | 예제 |
|---|---|---|
IN |
집합에 포함됨 | status IN [200, 201, 202] |
NOT IN |
집합에 포함되지 않음 | level NOT IN ['debug', 'info'] |
논리 연산자¶
| 연산자 | 설명 | 예제 |
|---|---|---|
AND 또는 && |
논리곱 | cpu > 80 AND memory > 90 |
OR 또는 | | |
논리합 | status = 500 OR status = 502 |
NOT |
논리부정 | NOT status = 200 |
팁:
OR연산자는 논리합 외에도 NULL 값 대체 의미를 제공합니다. 왼쪽 표현식이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 서브쿼리는 DQL에서 동적 필터링을 구현하는 강력한 기능으로, 하나의 쿼리 결과를 다른 쿼리의 필터 조건으로 사용할 수 있습니다. 이 메커니즘은 데이터 분석 결과를 기반으로 한 동적 필터링을 지원합니다.
쿼리 특징¶
- 동적 필터링: 필터 조건이 고정된 값이 아니라 쿼리를 통해 동적으로 계산됨
- 네임스페이스 혼합: 네임스페이스 간 쿼리를 지원하여 서로 다른 데이터 유형의 연관 분석 가능
- 직렬 실행: 서브쿼리가 먼저 실행되고 그 결과가 메인 쿼리의 필터링에 사용됨
- 배열 결과: 서브쿼리 결과는 배열로 캡슐화되므로
IN및NOT IN연산자만 지원
실행 흐름¶
일반적인 WHERE 서브쿼리 예제를 살펴보겠습니다.
실행 과정:
-
서브쿼리 실행:
O::HOST:(hostname) {provider = 'cloud-a'}- 모든 인프라스트럭처 객체 쿼리
- provider가 'cloud-a'인 호스트 필터링
- 호스트 이름 목록 반환:
['host-01', 'host-02', 'host-03']
-
메인 쿼리 실행:
M::cpu:(avg(usage)) [1h] BY host {host IN [...]}- CPU 사용률 데이터 쿼리
- 서브쿼리가 반환한 호스트만 집계
- 호스트별로 그룹화하여 평균 사용률 계산
적용 예제¶
// 특정 클라우드 서비스 제공업체의 서버 모니터링
M::cpu:(avg(usage)) { 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)¶
그룹화는 데이터 분석의 핵심 기능으로, 데이터를 지정된 차원별로 그룹화하여 집계하는 데 사용됩니다.
기본 구문¶
그룹화 유형¶
필드 그룹화¶
// 단일 필드 그룹화
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}
쿼리에 그룹화와 시간 창이 모두 포함된 경우 2차원 데이터 구조가 생성됩니다. Group By는 시간 창과 혼합하여 사용할 수 있으며, 이러한 쿼리 결과는 2차원 배열이 됩니다. 이 2차원 배열의 첫 번째 차원은 그룹 키로 구분된 여러 그룹이고, 두 번째 차원은 단일 그룹 내의 여러 시간 구간 데이터입니다.
*2차원 데이터 구조 예제:*
쿼리: `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]
]
}
]
}
이 2차원 배열을 다시 가공하려면:
- 이 2차원 배열의 결과를 필터링하려면 Having 절을 사용
- 2차원 배열의 단일 그룹 내 데이터를 정렬하거나 페이지 매기려면 order by, limit, offset 계열 문 사용
- 2차원 배열의 그룹을 정렬하거나 페이지 매기려면 sorder by, slimit, soffset 계열 문 사용
2차원 데이터 구조에 대한 자세한 설명과 정렬 및 페이지 매김 기능은 정렬 및 페이지 매김을 참조하세요.
Having 절(having-clause)¶
Having 절은 그룹 집계 후 결과를 필터링하는 데 사용되며, WHERE 절과 유사하지만 집계된 데이터에 적용됩니다.
기본 구문¶
WHERE와의 차이점¶
WHERE와 HAVING은 모두 데이터를 필터링하는 절이지만, 쿼리 실행의 서로 다른 단계에서 작동하며 처리하는 필터 조건도 다릅니다.
실행 순서 차이¶
-
WHERE 절:
- 그룹 집계 전에 실행
- 원시 데이터 행에 적용
- 조건을 만족하지 않는 데이터 행을 필터링하여 후속 처리할 데이터 양을 줄임
-
HAVING 절:
- 그룹 집계 후에 실행
- 집계된 결과에 적용
- 집계 함수의 결과를 기반으로 필터링
적용 시나리오¶
HAVING 절은 집계 결과를 필터링하는 데 적합합니다.
// 집계 함수 값 기반 필터링
M::cpu:(avg(usage) as avg_usage) [1h] BY host HAVING avg_usage > 80
// 여러 집계 조건 기반 필터링
M::http_requests:(
sum(request_count) as total,
sum(error_count) as errors
) [1h] BY service, endpoint
HAVING errors / total > 0.01 AND total > 1000
// 그룹 통계 기반 필터링
L::logs:(count(*) as count) [1h] BY service HAVING count > 100
// 복합 집계 조건 기반 필터링
M::response_time:(
avg(response_time) as avg_time,
max(response_time) as max_time,
min(response_time) as min_time
) [1h] BY endpoint
HAVING avg_time > 1000 AND max_time > 5000 AND (max_time - min_time) > 2000
정렬 및 페이지 매김¶
DQL의 정렬 및 페이지 매김은 매우 중요하고 독특한 기능으로, 시계열 데이터의 특성에 맞게 이중 정렬 메커니즘(그룹 내 정렬 및 그룹 간 정렬)을 설계했습니다. 이러한 설계를 통해 DQL은 복잡한 다차원 시계열 데이터 분석 요구 사항을 효율적으로 처리할 수 있습니다.
DQL 데이터 구조 이해¶
정렬 및 페이지 매김을 자세히 살펴보기 전에 DQL 쿼리 결과의 2차원 데이터 구조를 이해해야 합니다. 쿼리에 그룹화(BY)와 시간 창이 모두 포함된 경우 2차원 배열이 생성됩니다.
- 첫 번째 차원(그룹 차원): 그룹 키로 구분된 여러 그룹
- 두 번째 차원(시간 차원): 각 그룹 내에서 시간 창별로 집계된 데이터
2차원 데이터 구조에 대한 자세한 설명과 JSON 형식 예제는 그룹화 결과 처리를 참조하세요. 이 2차원 구조는 DQL 정렬 및 페이지 매김 기능의 기초이며, 이 구조를 이해하는 것이 DQL의 정렬 메커니즘을 이해하는 데 중요합니다.
그룹 내 정렬 및 페이지 매김(ORDER BY, LIMIT, OFFSET)¶
그룹 내 정렬 및 페이지 매김은 2차원 구조의 두 번째 차원, 즉 각 그룹 내부의 데이터에 적용됩니다. 이 정렬은 각 그룹 내에서 독립적으로 실행되며 다른 그룹의 데이터에 영향을 주지 않습니다.
기본 구문¶
실행 메커니즘¶
그룹 내 정렬의 실행 과정:
- 그룹 처리: 각 그룹에 대해 독립적으로 정렬 작업 실행
- 정렬 기준: 시간 필드, 집계 함수 결과 또는 계산 표현식을 사용할 수 있음
- 페이지 매김 제한: LIMIT은 각 그룹이 반환하는 데이터 행 수를 제한
- 오프셋 처리: OFFSET은 각 그룹의 처음 N개 행 데이터를 건너뜀
적용 예제¶
기본 시간 정렬¶
// 시간 기준 내림차순 정렬, 각 호스트의 최신 데이터 표시
M::cpu:(max(usage_total)) [1h::10m] BY host ORDER BY time DESC
// 시간 기준 오름차순 정렬, 이력 추세 표시
M::cpu:(max(usage_total)) [1h::10m] BY host ORDER BY time ASC
실행 결과(ORDER BY time DESC):
{
"series": [
{
"columns": ["time", "max(usage_total)"],
"name": "cpu",
"tags": {"host": "web-server-01"},
"values": [
[1721059200000, 78.5], // 12:00:00
[1721058600000, 82.3], // 11:50:00
[1721058000000, 75.8], // 11:40:00
[1721057400000, 88.2], // 11:30:00
[1721056800000, 72.1], // 11:20:00
[1721056200000, 69.4] // 11:10:00
]
},
{
"columns": ["time", "max(usage_total)"],
"name": "cpu",
"tags": {"host": "web-server-02"},
"values": [
[1721059200000, 45.2], // 12:00:00
[1721058600000, 52.8], // 11:50:00
[1721058000000, 48.5], // 11:40:00
[1721057400000, 61.3], // 11:30:00
[1721056800000, 55.7], // 11:20:00
[1721056200000, 58.9] // 11:10:00
]
}
]
}
숫자 기반 정렬¶
// CPU 사용률 기준 내림차순 정렬, 각 호스트의 피크 시간대 찾기
M::cpu:(max(usage_total) as max_usage_total) [1h::10m] BY host ORDER BY max_usage_total DESC
// 응답 시간 기준 오름차순 정렬, 성능이 가장 좋은 시간대 찾기
M::response_time:(avg(response_time) as avg_response_time) [1h::5m] BY endpoint ORDER BY avg_response_time ASC
그룹 내 페이지 매김¶
// 각 호스트의 최신 3개 데이터 포인트만 표시
M::cpu:(max(usage_total)) [1h::10m] BY host ORDER BY time DESC LIMIT 3
// 최신 2개 데이터 포인트를 건너뛰고 다음 3개 표시
M::cpu:(max(usage_total)) [1h::10m] BY host ORDER BY time DESC LIMIT 3 OFFSET 2
실행 결과(LIMIT 3):
{
"series": [
{
"columns": ["time", "max(usage_total)"],
"name": "cpu",
"tags": {"host": "web-server-01"},
"values": [
[1721059200000, 78.5], // 12:00:00 - 최신
[1721058600000, 82.3], // 11:50:00
[1721058000000, 75.8] // 11:40:00
// 처음 3개 데이터만 반환
]
},
{
"columns": ["time", "max(usage_total)"],
"name": "cpu",
"tags": {"host": "web-server-02"},
"values": [
[1721059200000, 45.2], // 12:00:00 - 최신
[1721058600000, 52.8], // 11:50:00
[1721058000000, 48.5] // 11:40:00
// 처음 3개 데이터만 반환
]
}
]
}
그룹 간 정렬 및 페이지 매김(SORDER BY, SLIMIT, SOFFSET)¶
그룹 간 정렬 및 페이지 매김은 DQL의 특징적인 기능으로, 2차원 구조의 첫 번째 차원, 즉 그룹 자체를 정렬하는 데 적용됩니다. 이 정렬은 각 그룹의 데이터를 단일 값으로 축소한 다음, 서로 다른 그룹 간의 이 값을 비교해야 합니다.
쿼리에 BY가 사용되지 않은 경우 결과는 일반적으로 하나의 그룹만 있습니다. 이 경우 SORDER BY는 정렬 효과가 일반적으로 보이지 않지만, SLIMIT / SOFFSET은 여전히 "그룹 수" 의미로 적용됩니다(즉, 해당 단일 그룹에 대해).
기본 구문¶
실행 메커니즘¶
그룹 간 정렬의 실행 과정:
- 차원 축소 계산: 각 그룹에 집계 함수를 적용하여 대표적인 숫자 값을 계산
- 그룹 정렬: 축소된 값을 기준으로 모든 그룹 정렬
- 그룹 페이지 매김: SLIMIT은 반환되는 그룹 수를 제한하고, SOFFSET은 처음 N개 그룹을 건너뜀
차원 축소 함수 선택¶
그룹 간 정렬은 반드시 집계 함수를 사용하여 차원을 축소해야 하며, 집계 함수를 지정하지 않으면 기본적으로 last 함수가 사용됩니다.
일반적으로 사용되는 차원 축소 함수는 다음과 같습니다.
| 함수 | 설명 | 적용 시나리오 |
|---|---|---|
last() |
마지막 값 가져오기 | 시계열의 현재 상태에 적합 |
max() |
최대값 가져오기 | 피크 분석에 적합 |
min() |
최소값 가져오기 | 저점 분석에 적합 |
avg() |
평균값 계산 | 전체 추세 분석에 적합 |
sum() |
합계 | 총량 통계에 적합 |
count() |
개수 | 빈도 분석에 적합 |
그러나 단일 값을 반환하는 거의 모든 집계 함수를 사용할 수 있으므로 여기서는 전체 함수 목록을 나열하지 않습니다.
적용 예제¶
평균값 기반 정렬¶
// 평균 CPU 사용률 기준 내림차순 정렬, 부하가 가장 높은 호스트 찾기
M::cpu:(avg(usage)) [1h::10m] BY host SORDER BY avg(usage) DESC SLIMIT 5
// 평균 응답 시간 기준 오름차순 정렬, 성능이 가장 좋은 서비스 찾기
M::response_time:(avg(response_time)) [1h] BY service SORDER BY avg(response_time) ASC SLIMIT 10
실행 과정 분석:
-
차원 축소 계산:
- web-server-01: avg(usage) = 77.7
- web-server-02: avg(usage) = 53.7
- web-server-03: avg(usage) = 90.9
-
그룹 정렬(avg(usage) DESC 기준):
- web-server-03: 90.9
- web-server-01: 77.7
- web-server-02: 53.7
-
최종 결과(SLIMIT 2):
{
"series": [
{
"columns": ["time", "avg(usage)"],
"name": "cpu",
"tags": {"host": "web-server-03"}, // 평균 사용률: 90.9 - 1위
"values": [
[1721059200000, 92.1],
[1721058600000, 95.7],
[1721058000000, 89.4]
]
},
{
"columns": ["time", "avg(usage)"],
"name": "cpu",
"tags": {"host": "web-server-01"}, // 평균 사용률: 77.7 - 2위
"values": [
[1721059200000, 78.5],
[1721058600000, 82.3],
[1721058000000, 75.8]
]
}
// web-server-02 (avg: 53.7)는 SLIMIT 2로 인해 필터링됨
]
}
적용 예제¶
// 최대 CPU 사용률 기준 정렬, 비정상적인 피크가 있는 호스트 찾기
M::cpu:(max(usage_total)) [1h::10m] BY host SORDER BY max(usage_total) DESC SLIMIT 10
// 최소 메모리 사용률 기준 정렬, 리소스 활용도가 가장 낮은 호스트 찾기
M::memory:(min(usage_percent)) [24h::1h] BY host SORDER BY min(usage_percent) ASC SLIMIT 5
// 최신 CPU 사용률 기준 정렬, 현재 부하가 가장 높은 호스트 찾기
M::cpu:(usage) [1h::10m] BY host SORDER BY last(usage) DESC SLIMIT 10
// 최신 오류율 기준 정렬, 현재 문제가 가장 많은 서비스 찾기
L::logs:(count(*) as error_count) {level = 'error'} [1h] BY service SORDER BY error_count DESC SLIMIT 5
이중 정렬 및 페이지 매김의 조합 사용¶
실제 애플리케이션에서는 그룹 내 정렬과 그룹 간 정렬이 자주 결합되어 복잡한 데이터 표시 요구 사항을 구현합니다. 이 조합은 그룹의 순서와 그룹 내 데이터의 순서를 동시에 제어할 수 있습니다.
실행 순서¶
이중 정렬의 실행 순서:
- 그룹 간 정렬: 먼저 모든 그룹을 정렬하고 페이지 매김
- 그룹 내 정렬: 선택된 그룹에 대해 내부 정렬 및 페이지 매김
적용 예제¶
모니터링 대시보드 시나리오¶
// CPU 사용률이 가장 높은 10개 서버를 찾고, 각 서버의 최신 5개 데이터 포인트 표시
M::cpu:(avg(usage)) [1h::10m] BY host
SORDER BY avg(usage) DESC SLIMIT 10 // 그룹 간 정렬: 사용률이 가장 높은 10개 찾기
ORDER BY time DESC LIMIT 5 // 그룹 내 정렬: 각 서버의 최신 5개 포인트 표시
실행 결과:
{
"series": [
{
"columns": ["time", "avg(usage)"],
"name": "cpu",
"tags": {"host": "web-server-03"}, // 평균 사용률: 90.9 - 1위
"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 - 2위
"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의 정렬 및 페이지 매김 기능을 마스터하면 강력한 모니터링 대시보드, 성능 분석 도구 및 비즈니스 인사이트 시스템을 구축할 수 있습니다.
주의
그룹 내 정렬과 그룹 간 정렬의 조합을 적절히 사용하면 데이터 분석의 효율성과 효과를 크게 향상시킬 수 있습니다.