跳到主要内容

EQL 语言

EQL (PlantPulse CEP SQL) 是 PlantPulse 的流查询语言。它基于 SQL-92,并针对流处理需求进行了扩展。

本页面的所有示例均取自黄金语料库(产品契约测试中固定答案的句子),不存在凭空编造的示例。

语句结构​

[ INSERT INTO <stream> ]
SELECT [DISTINCT] <식> [AS <별칭>] [, ...]
FROM <소스> [ <조인> ... ]
[ WHERE <condition> ]
[ GROUP BY <식> [, ...] ]
[ HAVING <condition> ]
[ 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已解析,但 其结构超出了语言范围。以 Token 形式命名移除该结构后重新编写
UNPARSEABLE语法解析失败。附带 行·列位置检查拼写错误
«语言可解析» 与 «引擎可执行» 是不同的问题

即使判定为 SUPPORTED 的语句,也可能被启动的后端拒绝。此时将以 Token 形式标明原因 → 限制与拒绝 Token

关于各后端支持的内容,请参阅 后端 表格。


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

Null 的位置由语句决定 —— 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(<interval>)最近 n 小时区间 (Sliding)
win:length(<n>)最近 n 条 (Sliding)
win:time_batch(<interval>)每 n 小时截断的 批处理
win:length_batch(<n>)满 n 条则关闭的 批处理
win:ext_timed_batch(<시간필드>, <interval>)将事件 内部字段 作为时间
std:groupwin(<key>)每个 Key 拥有独立窗口
std:unique(<key>)每个 Key 仅保留最新一条
std:lastevent() · std:firstevent()最后 · 第一条事件

过滤器属于源,视图链附加其后。 链是 顺序 而非集合。

std:groupwin 与 GROUP BY 必须使用相同的 Key

若不一致,将导致 窗口向一边划分而聚合向另一边绑定 —— 从而对没人编写的语句给出 «正确» 答案。因此不静默选择其一,而是直接拒绝。

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 四种。在 Tick 之间产生的行将被 保留至下一个 Tick —— 这并非装饰,而是 频率限制。

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按 Key 分别匹配
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) 存在且 极性相反 —— 匹配是 «必须赢过» 的时限,而未到达是 «必须撑过» 的持续时间。若合并为一个词,两句将看起来相同,但 触发的事件集不同,且该差异在运行时不可见。


函数

本语言仅支持 18 个 函数。此处未列出的名称将以 function:<name> 拒绝。

函数类别规则
count(*) · count(x)聚合count(x) 不计算 Null。绝对非 Null(0)
sum avg min max聚合忽略 Null,若全部为 Null 则返回 Null
first(x) · last(x)聚合窗口内第一个 · 最后一个 非 Null 值
stddev(x)聚合样本 标准差(n−1)。拒绝非数值参数
median(x)聚合中位数。拒绝非数值参数
coalesce(a, b, …)标量第一个非 Null。类型仅允许 拓宽 —— 拒绝数值与非数值混合
cast(x, <type>)标量仅限逗号格式。Null → Null
to_int to_long to_float to_double标量字符串 → 数值
concat(a, b, …) · a || b标量可变参数。只要包含一个 Null 即为 Null
current_timestamp()时间处理时间 (epoch ms)
prev(n, x) · prior(n, x)窗口位置窗口内第 n 个前置事件
聚合 Null 规则总结

聚合忽略 Null,若无剩余值则返回 Null。 唯一例外是 count,其返回 0。


范围外

已决定不实现的功能​

不实现的功能原因
全量实现其他 CEP 产品的语言本项目非兼容性项目
模板化 (模板 + 参数)强制逻辑分类会导致后续维护问题
逐句后端路由每个启动实例仅限一个。路由曾是 重复发布故障的根源
后端回退 (Fallback)回退会模糊 «哪个引擎产生了此结果»
CREATE STREAM 类 DDL事件类型的唯一事实来源是 API 注册。允许语句内 DDL 会导致事实来源不唯一
逐句 EVENT TIME <열>事件时间由 类型注册 决定。若每句时钟不一,则无法比较相同类型的两个语句
UPDATE · DELETE · RETRACTEQL 流为 append-only
ALERT · EMIT 子句报警是 «接收端名称» 而非语言子句 —— 每个语句创建 Topic,报警由订阅该 Topic 的一方决定

尚未实现的功能​

结构当前状态
CONTEXTUNPARSEABLE
Named Window · Variable · 子查询UNPARSEABLE
every-distinctUNPARSEABLE
percentile · to_bigdecimalUNSUPPORTED / function:<name>
WATERMARK · 容许延迟不支持 —— 延迟事件 累加至开放桶
WINDOW SLIDING (크기, 슬라이드) (HOP)通过语法错误予以拦截
SESSION 窗口超出移植集

🔴 请勿将 «不实现» 与 «尚未实现» 放在同一类别中。 前者需寻找替代方案,后者可以提出请求。

相关文档​