EQL-Sprache
EQL (PlantPulse CEP SQL) ist die Streaming-Abfragesprache von PlantPulse. Sie basiert auf SQL-92 und wurde nur um die für das Streaming notwendigen Funktionen erweitert.
Alle Beispiele auf dieser Seite stammen aus dem Golden Corpus (Sätze, deren Antworten durch Vertragstests des Produkts fixiert sind). Es gibt keine erfundenen Beispiele.
Struktur der Sätze
[ 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>] ]
Die Reihenfolge der Klauseln ist fix. Die Sprache unterscheidet nicht zwischen Groß- und Kleinschreibung.
Die Schreibweise erfolgt wie 1 sec · 30 min · 10 SECONDS. Die führende Zahl wird ansonsten nicht erkannt.
Es gibt eine Ausnahme in dieser Regel — Zahlen ohne Einheit wurden fälschlicher şekilde als andere Einheiten interpretiert, wodurch aus einem 30-Sekunden-Fenster ein 8-Stunden-Fenster wurde.
Drei Arten der Entscheidung
| Entscheidung | Bedeutung | Aktion |
|---|---|---|
SUPPORTED | Sprache hat den Satz gelesen und vollständig in einen gemeinsamen Ausdruck überführt | Keine |
UNSUPPORTED | Gelesen, aber die Struktur liegt außerhalb des Sprachumfangs. Wird als Token benannt | Struktur entfernen und neu schreiben |
UNPARSEABLE | Grammatik konnte den Satz nicht lesen. Zeilen- und Spaltenposition wird mitgeliefert | Tippfehler suchen |
Ein Satz, der als SUPPORTED markiert ist, kann vom gestarteten Backend abgelehnt werden. In diesem Fall wird per Token angegeben, welche Struktur die Ursache ist → Limits und Ablehnungs-Token
Welches Backend welche Funktionen unterstützt, ist in der Tabelle unter Backend aufgeführt.
SQL-92 Core
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'
Wenn eine Aggregation vorhanden ist, wird diese einmalig beim Schließen des Fensters berechnet. Wenn keine vorhanden ist, wird pro Ereignis eine Zeile ausgegeben.
CASE · Arithmetik
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
CASE ohne ELSE wird vom Standard-Backend abgelehntDies ist expr:case-without-else. Wenn der Satz nicht definiert, was im Fall einer Differenz ausgegeben werden soll, liefern verschiedene Engines unterschiedliche Antworten.
DISTINCT
SELECT DISTINCT site_id, type FROM Point.win:time_batch(1 sec)
DISTINCT ist an Bedingungen geknüpft; es gibt eine Abweichung, die nicht durch Ablehnung beseitigt werden kannBei Verwendung mit GROUP BY · HAVING · berechneten Projektionen · bereits zugewiesenen Fenstern erfolgt eine Ablehnung mit Namensnennung.
Und ein verbleibender Unterschied — die Duplikatentfernung im Standard-Backend löscht über die gesamte Lebensdauer des Streams, während das SQL-DISTINCT innerhalb des Ergebnissatzes löscht. Bei unendlichen Streams sind diese beiden Vorgänge nicht identisch.
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
Die Position von Nullwerten wird ebenfalls durch den Satz bestimmt — 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 und LEFT [OUTER] JOIN werden zusammen mit einem ON-Prädikat verwendet. Joins werden von links nach rechts abgearbeitet.
Unärer NOT
SELECT a FROM Point.win:length(10) WHERE NOT a = 1
NOT ist entscheidendSie liegt über Vergleichen und unter AND. Daher ist NOT a = 1 AND b = 2 gleich (NOT a = 1) AND b = 2.
Beide Lesarten werden geparst, aber sie wählen unterschiedliche Ereignisse aus — daher wird dies durch Tests und nicht durch Kommentare gesichert.
NOT LIKE · NOT IN · NOT BETWEEN · IS NOT NULL sind weiterhin jeweils einzelne Operatoren, und der unäre NOT übernimmt nicht deren NOT.
Negative Zahlen
Negative Zahlen sind vorzeichenbehaftete Literale und keine unären Minusoperatoren. Ein unärer Minusoperator müsste die Priorität gegenüber * und dem binären - festlegen, was je nach Engine variiert. Ein Fehler hierbei würde nicht das Parsing, sondern die Bedeutung von -a * b ändern.
Die Subtraktion bleibt unverändert — a - 1 ist eine Subtraktion.
CEP Erweiterungen
WINDOW TUMBLING — Uhrzeit-ausgerichtete Buckets
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
| Ausrichtung | Bedeutung |
|---|---|
ALIGN TO CALENDAR | Ausrichtung am Kalendergitter. Die Zeitzone kann mit angegeben werden |
ALIGN TO FIRST EVENT | Das erste Ereignis markiert den Beginn des Gitters |
Die Zeitzone wird in der Reihenfolge Satzangabe → PP_TZ → Ablehnung aufgelöst. In der Antwort der Registrierung wird die tatsächlich verwendete Zeitzone und deren Quelle mitgeliefert.
Wenn die Umstellungsspanne den Bucket nicht exakt teilen kann, verschiebt sich das Gitter. Ein stillschweigend verschobenes Gitter führt dazu, dass alle Zählungen plausibel, aber falsch sind.
WINDOW SLIDING — Letzte «Größe», bei jedem Ereignis
SELECT site_id, avg(latency) AS avg_latency
FROM Point
WINDOW SLIDING (5 min)
GROUP BY site_id
Betrachtet das aktuelle Intervall basierend auf der Ereigniszeit und berechnet dieses bei jedem Eintreffen eines Ereignisses neu.
View-Chain — Alte Schreibweise für Fenster
SELECT id FROM Point(type = 'int').std:groupwin(id).win:time_batch(1 sec) GROUP BY id
| View | Bedeutung |
|---|---|
win:time(<interval>) | Letztes n-Stunden-Intervall (Sliding) |
win:length(<n>) | Letzte n Ereignisse (Sliding) |
win:time_batch(<interval>) | Alle n Stunden abschließender Batch |
win:length_batch(<n>) | Batch, der schließt, wenn n Ereignisse erreicht sind |
win:ext_timed_batch(<시간필드>, <interval>) | Zeitstempel aus einem Feld innerhalb des Ereignisses |
std:groupwin(<key>) | Eigenes Fenster pro Schlüssel |
std:unique(<key>) | Nur das aktuellste Ereignis pro Schlüssel |
std:lastevent() · std:firstevent() | Letztes · Erstes Ereignis |
Filter gehören zur Quelle, die View-Chain folgt danach. Eine Chain ist eine Reihenfolge, keine Menge.
std:groupwin und GROUP BY müssen denselben Schlüssel verwendenBei Abweichungen wird das Fenster auf die eine Seite und die Aggregation auf die andere Seite aufgeteilt — es käme eine «korrekte» Antwort für einen Satz zustande, den niemand so geschrieben hat. Daher erfolgt eine Ablehnung statt einer stillschweigenden Wahl.
OUTPUT … EVERY — Ausgaberate
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
Es gibt vier Richtlinien: LAST · FIRST · ALL · SNAPSHOT. Zeilen, die zwischen zwei Ticks erstellt wurden, werden bis zum nächsten Tick zurückgehalten — dies ist kein Dekor, sondern eine Ratenbegrenzung.
INSERT INTO — Abgeleiteter Stream
INSERT INTO PointTimed
SELECT tag_id, AVG(latency) AS avg_latency
FROM Point.win:time_batch(1 min)
GROUP BY tag_id
Gibt Ergebniszeilen an einen benannten Stream aus.
Zeilenmuster
MATCH_RECOGNIZE — SQL:2016 Standard
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
)
| Position | Inhalt |
|---|---|
PARTITION BY | Separates Matching pro Schlüssel |
MEASURES | Aus dem Match zu extrahierende Werte |
PATTERN | Zeilenmuster — Sequenz (Konkatenation) · | (Alternation) · Quantifikator * + ? {n} {n,m} |
DEFINE | Bedingung für jede Variable |
AFTER MATCH SKIP | Startpunkt für das nächste Match |
Das Zeichen ? am Ende eines Quantifikators steht für reluctant (so kurz wie möglich).
pattern[...] — Alte Schreibweise
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)
]
Die Prioritätsfolge ist von schwach nach stark: -> · or · and · until · every/not · Guard · Atom.
MATCH_RECOGNIZE ist der Standard, während pattern[...] die Schreibweise bestehender Sätze ist. Dies bedeutet, dass dasselbe Konzept mit zwei Schreibweisen ausgedrückt werden kann; welche zu verwenden ist, hängt davon ab, was das Backend unterstützt — siehe Tabelle unter Backend.
Abwesenheit — «Nicht eingetroffen»
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]
Alle anderen Mustoperator enden mit einer «Ankunft», aber bei der Abwesenheit gibt es keine Ankunft. Daher muss der Satz definieren, wann die Schlussfolgerung «nicht eingetroffen» gezogen werden darf. Ohne diese Definition würde ein Match entweder niemals enden oder in einem Moment enden, den niemand spezifiziert hat.
Ein not ohne Periode wird von der Grammatik weiterhin gelesen — und führt zur Ablehnung unter dem Namen pattern:not:unbounded. Würde man dies aus der Grammatik entfernen, ergäbe es die Meldung «Tippfehler», obwohl der Parser den Satz korrekt gelesen hätte.
for verwendet wirdWITHIN existiert in dieser Sprache bereits als where timer:within(t) und hat eine entgegengesetzte Polarität — eine Frist, die ein Match «überwinden muss», gegenüber einer Dauer, die eine Nicht-Ankunft «aushalten muss». Würde man beides in einem Wort zusammenfassen, sähen zwei Sätze gleich aus, würden aber auf unterschiedliche Ereignismengen feuern, was zur Laufzeit nicht sichtbar wäre.
Funktionen
Die Sprache kennt insgesamt nur achtzehn Funktionen. Namen, die hier nicht aufgeführt sind, werden als function:<name> abgelehnt.
| Funktion | Kategorie | Regel |
|---|---|---|
count(*) · count(x) | Aggregation | count(x) zählt Nullwerte nicht. Ergibt niemals Null (0) |
sum avg min max | Aggregation | Ignoriert Nullwerte; ergibt Null, wenn alles Null ist |
first(x) · last(x) | Aggregation | Erster · Letzter Nicht-Null-Wert im Fenster |
stddev(x) | Aggregation | Stichprobenstandardabweichung (n−1). Nicht-numerische Argumente werden abgelehnt |
median(x) | Aggregation | Median. Nicht-numerische Argumente werden abgelehnt |
coalesce(a, b, …) | Skalar | Erster Nicht-Null-Wert. Typ wird nur erweitert — Mischung aus numerisch/nicht-numerisch wird abgelehnt |
cast(x, <type>) | Skalar | Nur Komma-Format. Null → Null |
to_int to_long to_float to_double | Skalar | String → Numerisch |
concat(a, b, …) · a || b | Skalar | Variable Argumente. Ergibt Null, wenn mindestens ein Argument Null ist |
current_timestamp() | Zeit | Verarbeitungszeit (epoch ms) |
prev(n, x) · prior(n, x) | Fensterposition | n-tes Ereignis vor dem aktuellen im Fenster |
Aggregationen ignorieren Nullwerte und ergeben Null, wenn nichts übrig bleibt. Die einzige Ausnahme ist count, das 0 ergibt.
Außerhalb des Umfangs
Bewusst ausgeschlossen
| Ausgeschlossen | Grund |
|---|---|
| Vollständige Implementierung anderer CEP-Sprachen | Kein Kompatibilitätsprojekt |
| Templatisierung (Template + Parameter) | Die Erzwingung von Logiktypen würde später zu Problemen führen |
| Backend-Routing pro Satz | Nur eines pro Boot. Routing war die Ursache für Fehler bei Doppelpublikationen |
| Backend-Fallback | Fallbacks verschleiern, «welche Engine dieses Ergebnis geliefert hat» |
DDL-Typ CREATE STREAM | Die Referenz für Ereignistypen ist die API-Registrierung. DDL im Satz würde zu zwei Referenzen führen |
EVENT TIME <열> pro Satz | Die Ereigniszeit wird durch die Typ-Registrierung bestimmt. Unterschiedliche Uhren pro Satz würden den Vergleich gleicher Typen unmöglich machen |
UPDATE · DELETE · RETRACT | EQL-Streams sind append-only |
ALERT · EMIT Klauseln | Ein Alarm ist ein «Name einer Senke» und keine Sprachklausel — pro Satz entsteht ein Topic, und der Alarm wird von demjenigen definiert, der das Topic abonniert |
Noch nicht implementiert
| Funktion | Status heute |
|---|---|
CONTEXT | UNPARSEABLE |
| Named Window · Variable · Subquery | UNPARSEABLE |
every-distinct | UNPARSEABLE |
percentile · to_bigdecimal | UNSUPPORTED / function:<name> |
WATERMARK · Zulässige Verspätung | Nicht vorhanden — verspätete Ereignisse werden auf offene Buckets aufsummiert |
WINDOW SLIDING (크기, 슬라이드) (HOP) | Durch Syntaxfehler blockiert |
SESSION-Fenster | Außerhalb des Lieferumfangs |
🔴 «Ausgeschlossen» und «Noch nicht implementiert» dürfen nicht in dieselbe Kategorie gefasst werden. Im ersten Fall muss eine Alternative gesucht werden, im zweiten Fall kann die Funktion angefordert werden.
Verwandte Dokumente
- Backend — Wo welche Struktur ausgeführt wird
- Limits und Ablehnungs-Token
- Registrierung von Ereignistypen
- EQL — Ereignis-Abfragesprache Hilfe — Allgemeines zu EQL in der Konsole