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 → 拒绝。注册响应中将包含 实际使用的时区及其来源。
若转换幅度无法整除桶,网格将偏移。静默偏移的网格会导致 所有计数产生看似合理但错误的偏差。
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。 唯一例外是 count,其返回 0。
范围外
已决定不实现的功能
| 不实现的功能 | 原因 |
|---|---|
| 全量实现其他 CEP 产品的语言 | 本项目非兼容性项目 |
| 模板化 (模板 + 参数) | 强制逻辑分类会导致后续维护问题 |
| 逐句后端路由 | 每个启动实例仅限一个。路由曾是 重复发布故障的根源 |
| 后端回退 (Fallback) | 回退会模糊 «哪个引擎产生了此结果» |
CREATE STREAM 类 DDL | 事件类型的唯一事实来源是 API 注册。允许语句内 DDL 会导致事实来源不唯一 |
逐句 EVENT TIME <열> | 事件时间由 类型注册 决定。若每句时钟不一,则无法比较相同类型的两个语句 |
UPDATE · DELETE · RETRACT | EQL 流为 append-only |
ALERT · EMIT 子句 | 报警是 «接收端名称» 而非语言子句 —— 每个语句创建 Topic,报警由订阅该 Topic 的一方决定 |
尚未实现的功能
| 结构 | 当前状态 |
|---|---|
CONTEXT | UNPARSEABLE |
| Named Window · Variable · 子查询 | UNPARSEABLE |
every-distinct | UNPARSEABLE |
percentile · to_bigdecimal | UNSUPPORTED / function:<name> |
WATERMARK · 容许延迟 | 不支持 —— 延迟事件 累加至开放桶 |
WINDOW SLIDING (크기, 슬라이드) (HOP) | 通过语法错误予以拦截 |
SESSION 窗口 | 超出移植集 |
🔴 请勿将 «不实现» 与 «尚未实现» 放在同一类别中。 前者需寻找替代方案,后者可以提出请求。
相关文档
- 后端 — 阐述各项结构在何处运行
- 限制与拒绝 Token
- 事件类型注册
- EQL — 事件查询语言帮助 — 控制台使用的 EQL 全指南