Rule SQL リファレンス
EMQX のルールでは、データの抽出、フィルタリング、拡張、変換のために SQL ベースの構文を使用します。この SQL ライクな構文には、SELECT と FOREACH の2種類のステートメントがあります。
| ステートメント | 説明 |
|---|---|
SELECT | SQL ステートメントの結果が単一のメッセージとなる場合に使用します。 |
FOREACH | 1つの入力メッセージから0個以上のメッセージを生成する場合に使用します。 |
各ルールには正確に1つのステートメントを含めることができます。SQL ステートメントは豊富な組み込み関数を提供しており、簡単な変換やタイムスタンプの作成などが可能です。
また、SQL ステートメントは式内に jq プログラム を埋め込むことをサポートしており、必要に応じて複雑なデータ変換を行うことができます。式は SELECT および FOREACH ステートメント内に埋め込むことができます。SELECT と FOREACH ステートメントで参照可能なフィールドについては、データソースとフィールド を参照してください。
SELECT ステートメント
SELECT ステートメントは、入力メッセージから特定のフィールドを選択し、フィールド名の変更やデータの変換、条件に基づくメッセージのフィルタリングを行います。
ルールエンジンの SQL における SELECT ステートメントの基本形式は以下の通りです。
SELECT <fields_expressions> FROM <topic> [WHERE <conditions>]SELECT 句で出力に含めるフィールド(メッセージのペイロードおよびメタデータの両方)を指定し、WHERE 句で特定の条件に基づいてメッセージをフィルタリングできます。
FROM 句
FROM 句はクエリのデータソースを指定します。特定のトピックや条件に合致するイベントからデータを選択できます。
トピックによる選択
例えば、トピックパターン t/# と my/other/topic にパブリッシュされたすべてのメッセージに適用されるルールを定義したい場合、以下のように記述します。
SELECT clientid, payload.clientid as myclientid FROM "t/#", "my/other/topic"ここで、
SELECT句は出力に含めるフィールドを指定します。clientid: メタデータ内のクライアントIDpayload.clientid: メッセージペイロード内のクライアントID。ペイロード内のすべてのフィールドはpayloadの下に格納されています。as構文により、payload.clientidフィールドをmyclientidとして名前変更しています。
イベントによる選択
ルールをイベントに紐づけることも可能です。例えば、クライアント c1 が EMQX に接続を開始した際に IP アドレスとポート番号を取得したい場合、以下のように記述します。
SELECT peername as ip_port FROM "$events/client_connected" WHERE clientid = 'c1'TIP
利用可能なすべてのイベントは、EMQX ダッシュボードのルール編集画面の Events タブで確認できます。
WHERE 句
WHERE 句は、FROM 句で指定したトピックやイベントのフィルタに加えて、メッセージが満たすべき追加条件を指定し、メッセージのフィルタリングをさらに絞り込むためのオプションです。
例えば、トピック t/# に属し、ユーザー名が eric のメッセージのみをフィルタリングする場合は以下のように記述します。
SELECT * FROM "t/#" WHERE username = 'eric'TIP
WHERE 句で使用するフィールドは、メッセージのメタデータまたはペイロードに存在するフィールドでなければなりません。存在しない場合はエラーになります。
式の利用
式 は SELECT および WHERE 句でデータ変換に使用できます。例えば、以下の SQL 文は clientid フィールドの値を大文字に変換し、サフィックスを付加して、出力メッセージのフィールド名を cid としています。
SELECT (upper(clientid) + '_UPPERCASE_LETTERS') as cid FROM "t/#"次の例は括弧付きの算術式を使ったデータ変換の例です。
SELECT (payload.integer_field + 2) * 2 as num FROM "t/#"複雑な構造を持つペイロードのフィールドにドット表記でアクセスすることも可能です(ペイロードが JSON フォーマットであることが前提です)。
SELECT payload.a.b.c.deep as my_field FROM "t/#"以下は WHERE 句で等価演算子(=)を使い、フィールドが特定の値を持つかをテストする例です。SELECT 句で * を使うと、メタデータとペイロードのすべてが出力メッセージに転送されます。
SELECT * FROM "t/#" WHERE payload.x.y = 1WHERE 句では and と or 演算子を使って複雑なブール式を作成できます。
SELECT * FROM "t/#" WHERE payload.name = "sensor_1" and payload.temperature > 39FOREACH ステートメント
FOREACH ステートメントは SELECT ステートメントのより一般的な形態と見なせます。1つの入力メッセージから0個以上の出力メッセージを生成できます。特定の条件に基づいてデータをフィルタリングし、その結果を MQTT トピックやデータブリッジに出力する際に使用します。
ルールエンジンの SQL における FOREACH ステートメントの基本形式は以下の通りです。
FOREACH <expression_that_evaluates_to_array> [as <name>]
[DO <fields_expressions>]
[INCASE <condition>]
FROM <topic>
[WHERE <condition>]FOREACH ステートメントは、入力メッセージから配列を作成する FOREACH 句で始まります。FROM と WHERE 句は SELECT ステートメントの同名句と同じ目的で機能します。FOREACH ステートメントには、FOREACH、FROM、WHERE 句のほかに2つのオプション句があります。
| 句 | 必須/任意 | 説明 |
|---|---|---|
DO | 任意 | FOREACH で選択した配列の各要素を変換します。SELECT ステートメントの SELECT 句に相当し、同じ式を受け入れます。 |
INCASE | 任意 | 指定した条件に合致しない配列要素を除外します。WHERE 句と同じ式を受け入れます。 |
TIP
FOREACH 句を除き、FOREACH ステートメントの他の句はすべて SELECT ステートメントの対応する句と対応しています。したがって、FOREACH ステートメントは前述の通り SELECT ステートメントの一般化とみなせます。以下の2つのステートメントは等価です(jq('.', payload) はペイロードを配列でラップしています)。
FOREACH jq('.', payload) as it
DO it.field_1, it.field_2
FROM "t/#"SELECT payload.field_1, payload.field_2
FROM "t/#"上記の FOREACH 句の as 構文は配列の要素に名前を付けるために使用され、DO 句内で「現在の」要素を簡単に参照できます。as name 部分を省略した場合、デフォルトで item という名前が使われます。
以下は FOREACH ステートメントで2つの値を出力する例です。両方の値は value という1つのフィールドのみを含み、その値はそれぞれメッセージの field_1 と field_2 の値です。
FOREACH jq('[.field_1, .field_2]', payload)
DO item as value
FROM "t/#"FOREACH ステートメントは入力データが配列形式であることを要求します。入力メッセージがすでに配列を含む場合は、直接 FOREACH ステートメントを適用できます。
例えば、トピック t/# にパブリッシュされたメッセージで、センサーの idx が1以上の場合にタイムスタンプ、クライアントID、センサー名、インデックスを出力したい場合は以下のように記述します。
FOREACH
payload.sensors as sensor
DO
timestamp,
clientid,
upper(sensor.name) as name,
sensor.idx as idx
INCASE
sensor.idx >= 1
FROM "t/#"ここで、
FOREACH句は入力メッセージのペイロード内のsensorsフィールドを配列として指定し、配列要素にsensorという名前を付けています。DO句は出力に含めるフィールドを指定しています。timestampは入力メッセージのメタデータからのタイムスタンプです。clientidは入力メッセージのメタデータからのクライアントIDです。sensor.nameは組み込み関数upperで大文字化され、as構文でnameに名前変更されます。sensorはFOREACH句で指定された配列の現在の要素を指します。sensor.idxはas句でidxに名前変更されます。
INCASE句はフィルタ条件を追加し、idxフィールドの値が1以上のセンサーのみを取得します。FROM句はトピックパターンt/#にマッチするメッセージを対象としています。
ルール作成後は、本番環境に投入する前に必ずルールのテストを行うことを推奨します。ダッシュボードUIにはサンプルメッセージでルールをテストできる機能があります。SQL ステートメントのテスト方法の詳細は ルールのテスト を参照してください。上記のルールは以下の JSON フォーマットのペイロードを入力としてテストできます。
{"sensors": [
{"idx":0, "name":"t0"},
{"idx":1, "name":"t1"},
{"idx":2, "name":"t2"}
]
}入力メッセージに配列が含まれていない場合は、jq 関数を使ってペイロードを配列でラップできます。例えば以下のように記述します。
FOREACH jq('.', payload)
DO item.field_1, item.field_2
FROM "t/#"EMQX は高度な変換のために jq 関数の使用をサポートしています。詳細は 組み込みの jq 関数 を参照してください。
式と演算
EMQX のルール構文では、データ変換やメッセージのフィルタリングのために式を使用できます。これらの式は SELECT、FOREACH、DO、INCASE、WHERE などの句で利用可能です。このセクションでは式の使用方法について説明します。以下は式を構成する演算子の一覧です。また、組み込み関数も多数利用可能です。
算術演算
| 演算子 | 用途 | 戻り値 |
|---|---|---|
+ | 加算、または文字列の連結 | 合計、または連結された文字列 |
- | 減算 | 差 |
* | 乗算 | 積 |
/ | 除算 | 商 |
div | 整数除算 | 整数の商 |
mod | 剰余 | 剰余 |
論理演算
| 演算子 | 用途 | 戻り値 |
|---|---|---|
> | より大きい | true/false |
< | より小さい | true/false |
<= | 以下 | true/false |
>= | 以上 | true/false |
<> | 等しくない | true/false |
!= | 等しくない | true/false |
= | 完全に等しいか比較 | true/false |
=~ | トピックがトピックフィルターにマッチするか判定(トピックマッチ専用) | true/false |
and | 論理積(AND) | true/false |
or | 論理和(OR) | true/false |
CASE 式
CASE 式は条件付きの処理を行うために使用します。CASE 式は他言語の if-then-else 文に相当します。以下の例で使い方を示します。
SELECT
CASE WHEN payload.x < 0 THEN 0
WHEN payload.x > 7 THEN 7
ELSE payload.x
END as x
FROM "t/#"例えば、メッセージが以下の場合、
{"x": 8}出力は以下のようになります。
{"x": 7}さらに例
SELECT ステートメントの例
トピック
"t/a"のメッセージからすべてのフィールドを抽出する。sqlSELECT * FROM "t/a"トピック
"t/a"または"t/b"のメッセージからすべてのフィールドを抽出する。sqlSELECT * FROM "t/a","t/b"トピックが
t/#にマッチするメッセージからすべてのフィールドを抽出する。sqlSELECT * FROM "t/#"トピックが
t/#にマッチするメッセージからqos、username、clientidフィールドを抽出する(出力メッセージのペイロードにこれらのフィールドが含まれます)。sqlSELECT qos, username, clientid FROM "t/#"ペイロードに
usernameフィールドがあり、その値が'Steven'のメッセージからusernameフィールドを抽出する(FROM句で#を使うのは推奨されません。これはルールが EMQX に送信されるすべてのメッセージに対して評価されるためです)。sqlSELECT username FROM "#" WHERE username='Steven'入力メッセージのペイロードの
xフィールドを抽出し、出力メッセージでyとして名前変更する。WHERE句でも新しいエイリアスyを使用できる。ペイロードが{"x": 1}のメッセージにマッチし、{"x": 2}にはマッチしないルールの例。sqlSELECT payload.x as x FROM "tests/test_topic_1" WHERE y = 1ペイロードが
{"x": {"y": 1}}のメッセージ(例:{"x": {"y": 1}, "other": "field"}も含む)にマッチする例。sqlSELECT * FROM "#" WHERE payload.x.y = 1クライアントID が
'c1'の MQTT クライアントが接続した場合、そのソース IP アドレスとポート番号を抽出する例。sqlSELECT peername as ip_port FROM "$events/client_connected" WHERE clientid = 'c1'トピックが
't/topic'にマッチし、QoS レベルが 1 のすべてのサブスクリプションにマッチし、clientidを出力する例。sqlSELECT clientid FROM "$events/session_subscribed" WHERE topic = 'my/topic' and qos = 1上記の例と似ていますが、トピックマッチ演算子
=~を使ってトピックフィルター't/#'にマッチさせる例。sqlSELECT clientid FROM "$events/session_subscribed" WHERE topic =~ 't/#' and qos = 1キー
"foo"のユーザープロパティを抽出する例(ユーザープロパティは MQTT 5.0 の新機能であり、古い MQTT バージョンには該当しません)。sqlSELECT pub_props.'User-Property'.foo as foo FROM "t/#"
TIP
FROM句のトピックはダブルクォーテーション("")またはシングルクォーテーション('')で囲む必要があります。WHERE句の条件で文字列を使う場合はシングルクォーテーション('')で囲みます。FROM句に複数のトピックがある場合はカンマ(,)で区切ります。例:SELECT * FROM "t/1", "t/2"。- ペイロードの内部フィールドにアクセスするにはドット記法を使います。例:ネストされた JSON の場合、
payload.outer_field.inner_fieldでアクセス可能です。 - パフォーマンスに影響するため、ペイロードに対してエイリアスを作成するのは避けてください。例:
SELECT payload as pは推奨されません。 - 一部のエスケープシーケンスは使用時にアンエスケープが必要です。詳細は unescape 関数 を参照してください。
FOREACH ステートメントの例
クライアントID c_steve のメッセージがトピック t/1 に届き、メッセージ本文が JSON 形式で sensors フィールドが複数のオブジェクトを含む配列であるとします。例:
{
"date": "2020-04-24",
"sensors": [
{"name": "a", "idx":0},
{"name": "b", "idx":1},
{"name": "c", "idx":2}
]
}例1
sensors 配列の各オブジェクトをトピック sensors/${idx}(idx はオブジェクトのインデックス)に再パブリッシュし、内容は ${name}(オブジェクトの name)とします。上記の入力例に対してルールエンジンは以下の3つのメッセージを発行します。
- トピック: sensors/0
内容: a - トピック: sensors/1
内容: b - トピック: sensors/2
内容: c
このルールでは以下のアクション設定が必要です。
- アクションタイプ: メッセージ再パブリッシュ
- 送信先トピック:
sensors/${idx} - 送信先 QoS: 2
- メッセージ内容テンプレート:
${name}
SQL ステートメントは以下の通りです。
FOREACH
payload.sensors
FROM "t/#"この SQL 文の FOREACH 句は配列 sensors を指定しており、配列内の各オブジェクトに対して「メッセージ再パブリッシュ」アクションが3回実行されます。
例2
sensors 配列の中で id フィールドの値が1以上のオブジェクトのみをトピック sensors/${idx} に再パブリッシュし、内容は clientid=${clientid},name=${name},date=${date} とします。上記の入力例に対しては、id が0の要素は除外されるため、2つのメッセージが発行されます。
- トピック: sensors/1
内容: clientid=c_steve,name=b,date=2023-04-24 - トピック: sensors/2
内容: clientid=c_steve,name=c,date=2023-04-24
このルールでは以下のアクション設定が必要です。
- アクションタイプ: メッセージ再パブリッシュ
- 送信先トピック:
sensors/${idx} - 送信先 QoS: 2
- メッセージ内容テンプレート:
clientid=${clientid},name=${name},date=${date}
SQL ステートメントは以下の通りです。
FOREACH
payload.sensors
DO
clientid,
item.name as name,
item.idx as idx
INCASE
item.idx >= 1
FROM "t/#"この SQL 文では、FOREACH 句で配列 sensors を指定し、DO 句で各操作に必要なフィールドを選択しています。clientid はメッセージのメタデータから、name と idx は現在のセンサーオブジェクトから取得します。item は sensors 配列の現在のオブジェクトを表します。INCASE 句は配列オブジェクトのフィルタ条件を指定し、条件に合わないオブジェクトは無視されます。
DO および INCASE 句では、item を使って現在のオブジェクトにアクセスできますが、FOREACH 句の as 構文で変数名をカスタマイズすることも可能です。したがって、この例の SQL は以下のようにも書けます。
FOREACH
payload.sensors as s
DO
clientid,
s.name as name,
s.idx as idx
INCASE
s.idx >= 1
FROM "t/#"例3
例2を拡張し、clientid フィールドの c_steve の c_ プレフィックスを削除します。
ルールエンジンには FOREACH、DO、INCASE 句で呼び出せる多数の組み込み関数があります。c_steve を steve に変換したい場合、例2の SQL を以下のように変更します。
FOREACH
payload.sensors as s
DO
nth(2, tokens(clientid,'_')) as clientid,
s.name as name,
s.idx as idx
INCASE
s.idx >= 1
FROM "t/#"複数の式を FOREACH 句に記述できますが、最後の式が繰り返す配列を指定している必要があります。
例えば、入力メッセージのペイロードが以下のように構造化されている場合、
{
"date": "2020-04-24",
"data": {
"sensors": [
{"name": "a", "idx":0},
{"name": "b", "idx":1},
{"name": "c", "idx":2}
]
}
}FOREACH 句でペイロードの data に別名を付けてから配列を選択できます。
FOREACH
payload.data as d
d.sensors as s
...これは以下と同等です。
FOREACH
payload.data.sensors as s
...この機能は複雑な構造のペイロードを扱う際に便利です。