Zum Hauptinhalt springen

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.

Zeitperioden benötigen zwingend eine «Einheit»

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​

EntscheidungBedeutungAktion
SUPPORTEDSprache hat den Satz gelesen und vollständig in einen gemeinsamen Ausdruck überführtKeine
UNSUPPORTEDGelesen, aber die Struktur liegt außerhalb des Sprachumfangs. Wird als Token benanntStruktur entfernen und neu schreiben
UNPARSEABLEGrammatik konnte den Satz nicht lesen. Zeilen- und Spaltenposition wird mitgeliefertTippfehler suchen
«Die Sprache liest» und «Die Engine führt aus» sind unterschiedliche Fragen

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 abgelehnt

Dies 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 kann

Bei 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
Die «Position» von NOT ist entscheidend

Sie 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
AusrichtungBedeutung
ALIGN TO CALENDARAusrichtung am Kalendergitter. Die Zeitzone kann mit angegeben werden
ALIGN TO FIRST EVENTDas 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.

Ablehnung, wenn der DST-Wechsel kein Vielfaches der Bucket-Größe ist

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
ViewBedeutung
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 verwenden

Bei 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
)
PositionInhalt
PARTITION BYSeparates Matching pro Schlüssel
MEASURESAus dem Match zu extrahierende Werte
PATTERNZeilenmuster — Sequenz (Konkatenation) · | (Alternation) · Quantifikator * + ? {n} {n,m}
DEFINEBedingung für jede Variable
AFTER MATCH SKIPStartpunkt 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.

Die Existenz zweier Oberflächen ist beabsichtigt und hat einen Preis

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]
Die Periode ist Teil des Operators — sie kann nicht entfernt werden

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.

Warum das Wort for verwendet wird

WITHIN 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.

FunktionKategorieRegel
count(*) · count(x)Aggregationcount(x) zählt Nullwerte nicht. Ergibt niemals Null (0)
sum avg min maxAggregationIgnoriert Nullwerte; ergibt Null, wenn alles Null ist
first(x) · last(x)AggregationErster · Letzter Nicht-Null-Wert im Fenster
stddev(x)AggregationStichprobenstandardabweichung (n−1). Nicht-numerische Argumente werden abgelehnt
median(x)AggregationMedian. Nicht-numerische Argumente werden abgelehnt
coalesce(a, b, …)SkalarErster Nicht-Null-Wert. Typ wird nur erweitert — Mischung aus numerisch/nicht-numerisch wird abgelehnt
cast(x, <type>)SkalarNur Komma-Format. Null → Null
to_int to_long to_float to_doubleSkalarString → Numerisch
concat(a, b, …) · a || bSkalarVariable Argumente. Ergibt Null, wenn mindestens ein Argument Null ist
current_timestamp()ZeitVerarbeitungszeit (epoch ms)
prev(n, x) · prior(n, x)Fensterpositionn-tes Ereignis vor dem aktuellen im Fenster
Null-Regel für Aggregationen in einem Satz

Aggregationen ignorieren Nullwerte und ergeben Null, wenn nichts übrig bleibt. Die einzige Ausnahme ist count, das 0 ergibt.


Außerhalb des Umfangs

Bewusst ausgeschlossen​

AusgeschlossenGrund
Vollständige Implementierung anderer CEP-SprachenKein Kompatibilitätsprojekt
Templatisierung (Template + Parameter)Die Erzwingung von Logiktypen würde später zu Problemen führen
Backend-Routing pro SatzNur eines pro Boot. Routing war die Ursache für Fehler bei Doppelpublikationen
Backend-FallbackFallbacks verschleiern, «welche Engine dieses Ergebnis geliefert hat»
DDL-Typ CREATE STREAMDie Referenz für Ereignistypen ist die API-Registrierung. DDL im Satz würde zu zwei Referenzen führen
EVENT TIME <열> pro SatzDie Ereigniszeit wird durch die Typ-Registrierung bestimmt. Unterschiedliche Uhren pro Satz würden den Vergleich gleicher Typen unmöglich machen
UPDATE · DELETE · RETRACTEQL-Streams sind append-only
ALERT · EMIT KlauselnEin 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​

FunktionStatus heute
CONTEXTUNPARSEABLE
Named Window · Variable · SubqueryUNPARSEABLE
every-distinctUNPARSEABLE
percentile · to_bigdecimalUNSUPPORTED / function:<name>
WATERMARK · Zulässige VerspätungNicht vorhanden — verspätete Ereignisse werden auf offene Buckets aufsummiert
WINDOW SLIDING (크기, 슬라이드) (HOP)Durch Syntaxfehler blockiert
SESSION-FensterAuß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​