EQL 언어
EQL(PlantPulse CEP SQL) 은 PlantPulse 의 스트리밍 질의 언어입니다. 기반은 SQL-92 이고, 스트리밍에 필요한 것만 확장했습니다.
이 페이지의 예제는 전부 골든 코퍼스(제품의 계약 테스트가 답을 고정해 둔 문장)에서 가져왔습니다. 지어낸 예제가 없습니다.
문장의 모양
[ INSERT INTO <스트림> ]
SELECT [DISTINCT] <식> [AS <별칭>] [, ...]
FROM <소스> [ <조인> ... ]
[ WHERE <조건> ]
[ GROUP BY <식> [, ...] ]
[ HAVING <조건> ]
[ OUTPUT LAST|FIRST|ALL|SNAPSHOT EVERY <기간> ]
[ ORDER BY <식> [ASC|DESC] [NULLS FIRST|NULLS LAST] [, ...] ]
[ LIMIT <n> [OFFSET <n>] ]
절의 순서는 고정입니다. 대소문자를 가리지 않습니다.
1 sec · 30 min · 10 SECONDS 처럼 씁니다. 맨 숫자는 아예 읽지 않습니다.
이 규칙에는 흉터가 있습니다 — 단위 없는 숫자를 다른 단위로 읽어 30초 창이 8시간 창이 된 적이 있습니다.
판정은 셋입니다
| 판정 | 뜻 | 할 일 |
|---|---|---|
SUPPORTED | 언어가 읽었고 공통 표현으로 온전히 실렸다 | 없음 |
UNSUPPORTED | 읽히긴 했는데 그 구성이 언어의 범위 밖이다. 토큰으로 이름한다 | 그 구성을 빼고 다시 쓴다 |
UNPARSEABLE | 문법이 못 읽었다. 줄·열 위치가 같이 온다 | 오타를 찾는다 |
SQL-92 코어
SELECT · WHERE · GROUP BY
SELECT site_id, COUNT(*) AS c FROM Point.win:time_batch(1 sec) GROUP BY site_id
SELECT tag_id, value
FROM Point.win:time(1 sec)
WHERE tag_id LIKE '%.TEMP'
집계가 있으면 창이 닫힐 때 그 내용 위에서 한 번 계산되고, 없으면 이벤트마다 한 행이 나옵니다.
CASE · 산술
SELECT tag_id, latency,
CASE WHEN latency > 1000 THEN 'HIGH'
WHEN latency > 500 THEN 'WARN'
ELSE 'OK' END AS band
FROM Point.win:length(1)
SELECT alarm_config_id, tag_id, asset_id, priority,
off_timestamp - timestamp AS duration_ms
FROM Alarm.win:length(1)
WHERE is_on = 'N'
AND off_timestamp - timestamp > 86400000
ELSE 없는 CASE 는 기본 백엔드가 거절합니다expr:case-without-else 입니다. 남는 경우에 무엇을 낼지 문장이 말하지 않으면, 엔진마다 다른 답을 냅니다.
DISTINCT
SELECT DISTINCT site_id, type FROM Point.win:time_batch(1 sec)
DISTINCT 에는 조건이 붙고, 거부로 없앨 수 없는 어긋남이 하나 있습니다GROUP BY · HAVING · 계산된 투영 · 이미 붙은 창과 함께 쓰면 이름을 대고 거부됩니다.
그리고 남는 차이 하나 — 기본 백엔드의 중복 제거는 스트림의 일생 전체에서 지우고, SQL 의 DISTINCT 는 결과 집합 안에서 지웁니다. 무한 스트림에서 둘은 같지 않습니다.
ORDER BY · LIMIT · OFFSET
SELECT id, count(*) AS c FROM Point.win:time_batch(1 sec)
GROUP BY id
ORDER BY c DESC
LIMIT 10 OFFSET 5
널의 자리도 문장이 정합니다 — ORDER BY c DESC NULLS LAST.
JOIN
SELECT p.id, o.name
FROM Point.win:length(10) AS p
INNER JOIN Other.win:length(10) AS o ON p.id = o.id
INNER JOIN 과 LEFT [OUTER] JOIN 을 ON 술어와 함께 씁니다. 조인은 왼쪽부터 접습니다.
단항 NOT
SELECT a FROM Point.win:length(10) WHERE NOT a = 1
NOT 의 «자리» 가 뜻입니다비교보다 위이고 AND 보다 아래입니다. 그래서 NOT a = 1 AND b = 2 는 (NOT a = 1) AND b = 2 입니다.
두 읽기 모두 파싱되고 다른 사건을 고릅니다 — 그래서 주석이 아니라 테스트가 지킵니다.
NOT LIKE · NOT IN · NOT BETWEEN · IS NOT NULL 은 여전히 각자 한 연산자이고, 단항 NOT 이 그것들의 NOT 을 가져가지 않습니다.
음수
음수는 부호 있는 리터럴이지 단항 마이너스가 아닙니다. 단항 마이너스는 * 와 이항 - 에 대한 우선순위를 정해야 하는데 엔진마다 답이 다르고, 틀리면 파싱 여부가 아니라 -a * b 의 뜻이 바뀝니다.
뺄셈은 그대로입니다 — a - 1 은 뺄셈입니다.
CEP 확장
WINDOW TUMBLING — 벽시계에 정렬된 버킷
SELECT id, count(*) AS c
FROM Point
WHERE quality = 0
WINDOW TUMBLING (1 min) ALIGN TO CALENDAR AT TIME ZONE 'Asia/Seoul'
GROUP BY id
SELECT count(*) AS c FROM Point WINDOW TUMBLING (30 sec) ALIGN TO FIRST EVENT
| 정렬 | 뜻 |
|---|---|
ALIGN TO CALENDAR | 달력 격자에 맞춥니다. 시간대를 함께 적을 수 있습니다 |
ALIGN TO FIRST EVENT | 첫 이벤트가 격자의 시작입니다 |
시간대는 문장이 적은 것 → PP_TZ → 거절 순으로 해소되고, 등록 응답에 실제로 쓰인 시간대와 그 출처가 함께 나옵니다.
전환폭이 버킷을 나누지 못하면 격자가 어긋납니다. 조용히 밀린 격자는 모든 카운트를 그럴듯하게 틀리게 만듭니다.
WINDOW SLIDING — 최근 «크기», 이벤트마다
SELECT site_id, avg(latency) AS avg_latency
FROM Point
WINDOW SLIDING (5 min)
GROUP BY site_id
이벤트 시각 기준 최근 구간을 보고, 이벤트가 올 때마다 다시 계산합니다.
뷰 체인 — 창의 옛 철자
SELECT id FROM Point(type = 'int').std:groupwin(id).win:time_batch(1 sec) GROUP BY id
| 뷰 | 뜻 |
|---|---|
win:time(<주기>) | 최근 n 시간 구간 (슬라이딩) |
win:length(<n>) | 최근 n 건 (슬라이딩) |
win:time_batch(<주기>) | n 시간마다 끊어지는 배치 |
win:length_batch(<n>) | n 건이 차면 닫히는 배치 |
win:ext_timed_batch(<시간필드>, <주기>) | 이벤트 안의 필드를 시각으로 |
std:groupwin(<키>) | 키마다 자기 창 |
std:unique(<키>) | 키마다 가장 최근 것만 |
std:lastevent() · std:firstevent() | 마지막 · 첫 이벤트 |
필터는 소스에 속하고 뷰 체인은 그 뒤에 붙습니다. 체인은 집합이 아니라 순서입니다.
std:groupwin 과 GROUP BY 는 같은 키여야 합니다어긋나면 창이 한 쪽으로 나뉘고 집계가 다른 쪽으로 묶입니다 — 아무도 쓰지 않은 문장에 대해 «맞는» 답이 나옵니다. 그래서 조용히 하나를 고르지 않고 거부합니다.
OUTPUT … EVERY — 출력 비율
SELECT current_timestamp() AS timestamp, opc_id,
COALESCE(MIN(latency), 0) AS min_latency,
COALESCE(MAX(latency), 0) AS max_latency,
COALESCE(AVG(latency), 0) AS avg_latency
FROM Point.std:groupwin(opc_id).win:time_batch(10 sec)
GROUP BY opc_id
OUTPUT LAST EVERY 60 SEC
SELECT line_id, AVG(oee) AS avg_oee, MIN(oee) AS worst_oee
FROM OEE.win:time_batch(1 min)
GROUP BY line_id
OUTPUT LAST EVERY 1 MIN
정책은 LAST · FIRST · ALL · SNAPSHOT 넷입니다. 틱 사이에 만들어진 행은 다음 틱까지 붙잡힙니다 — 장식이 아니라 비율 제한입니다.
INSERT INTO — 파생 스트림
INSERT INTO PointTimed
SELECT tag_id, AVG(latency) AS avg_latency
FROM Point.win:time_batch(1 min)
GROUP BY tag_id
결과 행을 이름 붙인 스트림으로 내보냅니다.
행 패턴
MATCH_RECOGNIZE — SQL:2016 표준
SELECT tag, n FROM Point MATCH_RECOGNIZE (
PARTITION BY tag_id
MEASURES first(h.tag_id) AS tag, count(h.latency) AS n
AFTER MATCH SKIP TO NEXT ROW
PATTERN (h{3})
DEFINE h AS h.latency > 100
)
| 자리 | 무엇 |
|---|---|
PARTITION BY | 키마다 따로 매치 |
MEASURES | 매치에서 꺼낼 값 |
PATTERN | 행 패턴 — 나열(연접) · |(교대) · 수량자 * + ? {n} {n,m} |
DEFINE | 각 변수의 조건 |
AFTER MATCH SKIP | 다음 매치를 어디서부터 찾을지 |
수량자 끝의 ? 는 reluctant(가능한 짧게)입니다.
pattern[...] — 옛 철자
SELECT a.machineId AS machine, b.severity AS sev
FROM pattern [
every a=AlarmEvent(severity >= 3)
-> b=AlarmEvent(machineId = a.machineId, severity >= 4)
where timer:within(10 sec)
]
사다리는 느슨한 쪽부터 -> · or · and · until · every/not · 가드 · 아톰입니다.
MATCH_RECOGNIZE 는 표준이고 pattern[...] 은 기존 문장이 쓰는 철자입니다. 같은 개념을 두 철자로 쓸 수 있다는 뜻이라, 어느 쪽으로 쓸지는 백엔드가 무엇을 받는지에 달립니다 — 백엔드의 표를 보십시오.
부재 — 「안 왔다」
SELECT a.machineId AS machine
FROM pattern [every a=AlarmEvent -> not ClearEvent for 30 min]
SELECT 'gap' AS gap
FROM pattern [not AssetData(assetId = 'A-1') for 30 min]
다른 패턴 연산자는 전부 «도착» 으로 끝나는데 부재에는 도착이 없습니다. 그래서 「안 왔다」를 언제 결론지어도 되는지를 문장이 말해야 합니다. 없으면 매치가 끝나지 않거나, 아무도 적지 않은 순간에 끝납니다.
기간 없는 not 은 문법이 계속 읽습니다 — 그리고 pattern:not:unbounded 로 이름을 대며 거절합니다. 문법에서 지우면 「오타가 있다」가 되는데, 파서는 그 문장을 잘 읽었습니다.
for 인 이유WITHIN 은 이 언어에 이미 where timer:within(t) 로 있고 극성이 반대입니다 — 매치가 «이겨야 하는» 시한 대 비도착이 «버텨야 하는» 지속시간. 한 낱말로 합치면 두 문장이 같아 보이는데 다른 사건 집합에 발화하고, 그 차이는 런타임에 안 보입니다.
함수
언어가 아는 함수는 열여덟 개가 전부입니다. 여기 없는 이름은 function:<이름> 으로 거절됩니다.
| 함수 | 부류 | 규칙 |
|---|---|---|
count(*) · count(x) | 집계 | count(x) 는 널을 안 셉니다. 절대 널이 아닙니다(0) |
sum avg min max | 집계 | 널 무시, 전부 널이면 널 |
first(x) · last(x) | 집계 | 창 안 첫 · 마지막 비널 값 |
stddev(x) | 집계 | 표본 표준편차(n−1). 비수치 인자는 거절 |
median(x) | 집계 | 중앙값. 비수치 인자는 거절 |
coalesce(a, b, …) | 스칼라 | 첫 비널. 타입은 넓히기만 — 수치·비수치 혼합은 거절 |
cast(x, <타입>) | 스칼라 | 쉼표 형만. 널 → 널 |
to_int to_long to_float to_double | 스칼라 | 문자열 → 수치 |
concat(a, b, …) · a || b | 스칼라 | 가변 인자. 널이 하나라도 있으면 널 |
current_timestamp() | 시간 | 처리 시각 (epoch ms) |
prev(n, x) · prior(n, x) | 창 위치 | 창 안 n 번째 앞 이벤트 |
집계는 널을 무시하고, 남는 게 없으면 널입니다. 예외는 count 뿐이고 그것만 0 입니다.
범위 밖
하지 않기로 정한 것
| 안 하는 것 | 왜 |
|---|---|
| 다른 CEP 제품의 언어 전량 구현 | 호환 프로젝트가 아닙니다 |
| 형판화(템플릿 + 파라미터) | 로직에 종류를 강제하면 나중에 문제가 됩니다 |
| 문장별 백엔드 라우팅 | 부팅당 하나. 라우팅이 이중 발행 결함의 근원이었습니다 |
| 백엔드 폴백 | 폴백은 「어느 엔진이 이 결과를 냈는가」를 흐립니다 |
CREATE STREAM 류 DDL | 이벤트 타입의 정본은 API 등록입니다. 문장 안 DDL 을 받으면 정본이 둘이 됩니다 |
문장별 EVENT TIME <열> | 이벤트 시각은 타입 등록이 정합니다. 문장마다 시계가 갈리면 같은 타입의 두 문장을 비교할 수 없습니다 |
UPDATE · DELETE · RETRACT | EQL 스트림은 append-only 입니다 |
ALERT · EMIT 절 | 알람은 «싱크의 이름» 이지 언어의 절이 아닙니다 — 문장마다 토픽이 생기고, 알람은 그 토픽을 구독하는 쪽이 정합니다 |
아직 아닌 것
| 구성 | 오늘 |
|---|---|
CONTEXT | UNPARSEABLE |
| Named Window · Variable · 서브쿼리 | UNPARSEABLE |
every-distinct | UNPARSEABLE |
percentile · to_bigdecimal | UNSUPPORTED / function:<이름> |
WATERMARK · 허용 지각 | 없습니다 — 지각 이벤트는 열린 버킷에 합산됩니다 |
WINDOW SLIDING (크기, 슬라이드) (HOP) | 구문 오류로 막아 두었습니다 |
SESSION 창 | 이식 집합 밖 |
🔴 「안 한다」와 「아직」을 같은 칸에 넣지 마십시오. 앞은 대안을 찾아야 하고, 뒤는 요청할 수 있습니다.
관련 문서
- 백엔드 — 어느 구성이 어디서 도나
- 한계와 거부 토큰
- 이벤트 타입 등록
- EQL — 이벤트 쿼리 언어 도움말 — 콘솔에서 쓰는 EQL 전반