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時間ウィンドウになった事例があります。
判定は3種類です
| 判定 | 意味 | 対応 |
|---|---|---|
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'
集計がある場合、ウィンドウが閉じる際にその内容に基づいて一度だけ計算され、ない場合はイベントごとに1行が出力されます。
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 には条件があり、拒否では解消できない不一致が1つありますGROUP BY · HAVING · 計算された投影 · 既に適用されたウィンドウと共に使用すると 名称が提示され拒否 されます。
また、もう1つの違いがあります。デフォルトバックエンドの重複排除は ストリームの全期間 から削除しますが、SQL の DISTINCT は 結果集合の中 で削除します。無限ストリームにおいて、この2つは同一ではありません。
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 時間区間 (スライディング) |
win:length(<n>) | 直近 n 件 (スライディング) |
win:time_batch(<interval>) | n 時間ごとに区切られる バッチ |
win:length_batch(<n>) | n 件が溜まったら閉じる バッチ |
win:ext_timed_batch(<시간필드>, <interval>) | イベント 内のフィールド を時刻として使用 |
std:groupwin(<key>) | キーごとに独立したウィンドウ |
std:unique(<key>) | キーごとに最新の1件のみ |
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 の4種類です。ティック間に生成された行は 次のティックまで保持 されます。これは装飾ではなく 比率制限 です。
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[...] は既存の文章で使用されている表記です。同一の概念を2つの表記で記述できるという意味であり、どちらを使用するかはバックエンドが何を受け付けるかに依存します — バックエンド の表を参照してください。
不在 — 「来なかった」
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) として存在し、極性が反対 です。マッチングが «勝ち抜けるべき» 期限に対し、非到着は «耐え抜くべき» 持続時間です。一つの単語に統合すると2つの文章が同じに見えますが、異なるイベント集合に発火 し、その差はランタイムで表面化しません。
関数
言語が認識する関数は 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製品の言語を完全に実装 | 互換プロジェクトではないため |
| テンプレート化 (テンプレート + パラメータ) | ロジックに種類を強制すると後々問題になるため |
| 文章ごとのバックエンドルーティング | 1回の起動につき1つ。ルーティングが 二重発行欠陥の根源 であったため |
| バックエンドフォールバック | フォールバックは 「どのエンジンがこの結果を出したか」 を曖昧にするため |
CREATE STREAM 類 DDL | イベントタイプの正本は API登録 である。文章内DDLを許容すると正本が2つになるため |
文章ごとの EVENT TIME <열> | イベント時刻は タイプ登録 が決定する。文章ごとに時計が異なると、同一タイプの2つの文章を比較できないため |
UPDATE · DELETE · RETRACT | EQLストリームは append-only であるため |
ALERT · EMIT 句 | アラームは «シンクの名前» であり、言語の句ではない。文章ごとにトピックが生成され、アラームはそのトピックを購読する側で決定するため |
未実装のもの
| 構成 | 現状 |
|---|---|
CONTEXT | UNPARSEABLE |
| Named Window · Variable · サブクエリ | UNPARSEABLE |
every-distinct | UNPARSEABLE |
percentile · to_bigdecimal | UNSUPPORTED / function:<name> |
WATERMARK · 許容遅延 | なし — 遅延イベントは 開いているバケットに合算 される |
WINDOW SLIDING (크기, 슬라이드) (HOP) | 構文エラーとしてブロック済み |
SESSION ウィンドウ | 移植集合外 |
🔴 「実装しない」と「未実装」を同じ欄に含めないでください。 前者は代替案を探すべき事項であり、後者はリクエスト可能な事項です。
関連ドキュメント
- バックエンド — どの構成がどこで動作するか
- 制限事項と拒否トークン
- イベントタイプ登録
- EQL — イベントクエリ言語ヘルプ — コンソールで使用するEQL全般