본문으로 건너뛰기

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문법이 못 읽었다. 줄·열 위치가 같이 온다오타를 찾는다
«언어가 읽는다» 와 «이 엔진이 돌린다» 는 다른 질문입니다

SUPPORTED 인 문장도 부팅한 백엔드가 거절할 수 있습니다. 그때는 어느 구성 때문인지 토큰으로 나옵니다 → 한계와 거부 토큰

어느 백엔드가 무엇을 받는지는 백엔드의 표에 있습니다.


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 → 거절 순으로 해소되고, 등록 응답에 실제로 쓰인 시간대와 그 출처가 함께 나옵니다.

DST 전환이 버킷 크기의 배수가 아니면 거절합니다

전환폭이 버킷을 나누지 못하면 격자가 어긋납니다. 조용히 밀린 격자는 모든 카운트를 그럴듯하게 틀리게 만듭니다.

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 · RETRACTEQL 스트림은 append-only 입니다
ALERT · EMIT 절알람은 «싱크의 이름» 이지 언어의 절이 아닙니다 — 문장마다 토픽이 생기고, 알람은 그 토픽을 구독하는 쪽이 정합니다

아직 아닌 것​

구성오늘
CONTEXTUNPARSEABLE
Named Window · Variable · 서브쿼리UNPARSEABLE
every-distinctUNPARSEABLE
percentile · to_bigdecimalUNSUPPORTED / function:<이름>
WATERMARK · 허용 지각없습니다 — 지각 이벤트는 열린 버킷에 합산됩니다
WINDOW SLIDING (크기, 슬라이드) (HOP)구문 오류로 막아 두었습니다
SESSION 창이식 집합 밖

🔴 「안 한다」와 「아직」을 같은 칸에 넣지 마십시오. 앞은 대안을 찾아야 하고, 뒤는 요청할 수 있습니다.

관련 문서​