Spanner Graph: GoogleSQL expressions = GQL expressions
この記事は Spanner Advent Calendar 3日目の記事 & apstndb Advent Calendar 5日目の記事です。
2024年8月の Cloud Next Tokyo で Spanner Graph の Public Preview が発表され、 Spanner は ISO GQL という新しい言語に対応しました。(なおタイムリーなことに 2024年12月2日に GA になりました。)
ユーザの心理として、新しいデータベース言語は宣伝されていても表現力に疑問があることが多いのではないかと思います。
Spanner Graph が発表されてからドキュメントを一通り読んだりおそらくほぼ完全な parser を実装した 経験からわかったことを書いてみます。
TL;DR
- GoogleSQL と GQL の式および型は完全に一致する。
- 全ての GoogleSQL の式は GQL でも有効だし、逆に全ての GQL で導入された式は GoogleSQL でも使える。
- サブクエリも例外ではない。
SQL の文や GQL の statement は直接お互いに流用できるものではない
まず前提として、 SQL の SELECT
文を構成する句や GQL クエリを構成する statement などの構文要素は分解不可能であり、お互いの言語で使うことはできません。
しかし、式、関数、型まで完全に異なるものにしていては処理系を共有する利益がありません。
プログラミング言語で例えると、 Java VM を使った Java, Kotlin, Scala, Clojure のような JVM 言語の構文は全く別のものであるが、関数やクラスには相互運用性がある利益を享受していることに似ています。
GoogleSQL の式と GQL の式は同じものである
GQL で使える Google SQL 式
事実、GQL の式レベルの要素のドキュメントには GQL では GoogleSQL にあるもの全てが使えるという記述が書かれています。
- 演算子 https://cloud.google.com/spanner/docs/reference/standard-sql/graph-operators
Graph Query Language (GQL) supports all GoogleSQL operators, including the following GQL-specific operators:
- 関数 https://cloud.google.com/spanner/docs/reference/standard-sql/graph-gql-functions
All GoogleSQL functions are supported, including the following GQL-specific functions:
- 型 https://cloud.google.com/spanner/docs/reference/standard-sql/graph-data-types
Graph Query Language (GQL) supports all GoogleSQL data types, including the following GQL-specific data type:
- 条件式 https://cloud.google.com/spanner/docs/reference/standard-sql/graph-conditional-expressions
Graph Query Language (GQL) supports all GoogleSQL conditional expressions. To learn more, see Conditional expressions.
これは何を意味するかというと、 Spanner Graph の GQL は2024年にリリースされたばかりでありながら、ベータのパブリックリリースからだと2017年からの積み重ねがあり、 Google Cloud で最も力を入れられているデータベースの一つである Spanner の充実した関数ライブラリや豊富な型が使えるということです。
例えば次のようなことが言えます。
- ベクトル検索ができる
- Spanner Graph と同時に発表された全文検索もできる
- GoogleSQL で最も特色ある型である Protocol Buffers も公式ドキュメント上の例はないが使える
- もちろん他の型のリテラル、
STRUCT
,JSON
,ARRAY
などの複雑な型のコンストラクタも GQL で使える。
- もちろん他の型のリテラル、
Spanner の第一言語である GoogleSQL が強化されれば自然と GQL も強くなる。この事実は歴史があるグラフデータベースと比べても長所の一つとなることでしょう。
GoogleSQL で使える GQL 式
あまり気付かれていないかもしれませんが、逆に GQL のために追加された式レベルの要素は全て GoogleSQL 側のドキュメントにも書かれ、使用可能になっています。
- 式 https://cloud.google.com/spanner/docs/reference/standard-sql/operators#operator_list
- Graph concatenation operator
-
Graph logical operator
- これは GQL MATCH statement の言語要素であって式ではないので間違いでは?
-
Graph predicates
-
ALL_DIFFERENT
predicate -
PROPERTY_EXISTS
predicate -
IS DESTINATION
predicate -
IS SOURCE
predicate -
SAME
predicate
-
- 関数
- GQL functions と全く同じ内容のページが GoogleSQL の関数リファレンス側にもある。
- 型 https://cloud.google.com/spanner/docs/reference/standard-sql/data-types
-
Graph element type(
GRAPH_ELEMENT
), Graph path type(GRAPH_PATH
) は GoogleSQL 側のデータ型としても定義されている。
-
Graph element type(
- GQL 固有の条件式は存在しない
これは何を意味するのでしょうか?
一つの意味は GRAPH_TABLE
operator で SQL に GQL を組み込む時に、 GQL の RETURN
statement で一旦 JSON 等に変換しなくてもそのまま SQL に持ち出して扱うことが可能であることを示しています。
You can use the
RETURN
statement to produce output with graph pattern variables. These variables can be referenced outsideGRAPH_TABLE
. For example,SELECT n.name, n.id FROM GRAPH_TABLE( FinGraph MATCH (n) RETURN n );
ただし、 他のネイティブのグラフデータベースの一部と異なり、GRAPH_ELEMENT
, GRAPH_PATH
型は Spanner API の最終的な結果として返すことができない型のため最終的には他の型になる必要があります。
The following query produces an error because n is a graph element and graph elements can't be included as query output:
-- Error SELECT n FROM GRAPH_TABLE( FinGraph MATCH (n) RETURN n );
Spanner Graph reference for openCypher users では GRAPH_ELEMENT
型の持つ情報を返すために TO_JSON
を使うことが提案されています。 JSON encodings として JSON に変換した結果は定義されています。(FORMAT
も対応してほしいですね。)
In Spanner Graph, query results don't return graph elements. Use the
TO_JSON
function to return graph elements as JSON.
サブクエリも式
さて、ここまでは触れていませんでしたがサブクエリも式の一つです。サブクエリについては少し状況が違って、ドキュメント上 Subqueries in GoogleSQL, GQL subqueries ともに4種類のサブクエリはその言語そのものに書くことができるとしか書かれていません。
SQL subqueries in GQL
ドキュメントを隅々まで読むと、 GQL MATCH statement の example に quantified された edge pattern (-[e:Transfers]->{1,2}
の部分)にヒットした bind variable(e
の型は ARRAY<GRAPH_ELEMENT>
) を ARRAY<INT64>
に変換するために ARRAY
サブクエリが使われていることが確認できます。
GRAPH FinGraph
MATCH ANY (src:Account {id: 7})-[e:Transfers]->{1,2}(dst:Account)
LET ids_in_path = ARRAY(SELECT e.to_id FROM UNNEST(e) AS e)
RETURN src.id AS source_account_id, dst.id AS destination_account_id, ids_in_path
/*----------------------------------------------------------+
| source_account_id | destination_account_id | ids_in_path |
+----------------------------------------------------------+
| 7 | 16 | 16 |
| 7 | 20 | 16,20 |
+----------------------------------------------------------*/
そう、 GQL には SQL サブクエリを埋め込むことができます。明記されていませんが試してみると SQL サブクエリは全て GQL に組み込むことができます。
せっかくなので GQL サブクエリのサブクエリの中身を全て SQL に書き換えてみましょう。これらは全て valid な GQL クエリです。
GQL のセマンティクスについてはこの記事のスコープではありませんが、 GQL サブクエリのドキュメントに書かれた実行結果と一致します。
GQL VALUE subquery の GoogleSQL Scalar subquery への書き換え
spanner> GRAPH FinGraph
RETURN (
SELECT p.name
FROM Person AS p
WHERE p.name LIKE '%e%'
LIMIT 1
) AS results;
+---------+
| results |
+---------+
| Alex |
+---------+
GQL ARRAY subquery の GoogleSQL Array subquery への書き換え
spanner> GRAPH FinGraph
MATCH (p:Person)-[:Owns]->(account:Account)
RETURN
p.name, account.id AS account_id,
ARRAY (
SELECT transfer.amount AS transfers
FROM Account AS a
JOIN AccountTransferAccount AS transfer ON (a.id = transfer.id)
JOIN Account AS b ON (transfer.to_id = b.id)
WHERE a.id = account.id
) AS transfers;
+------+------------+--------------------------+
| name | account_id | transfers |
+------+------------+--------------------------+
| Alex | 7 | [300.000000, 100.000000] |
| Dana | 20 | [500.000000, 200.000000] |
| Lee | 16 | [300.000000] |
+------+------------+--------------------------+
GQL IN subquery の GoogleSQL IN subquery への書き換え
spanner> GRAPH FinGraph
RETURN 'Dana' IN (
SELECT p.name
FROM Person AS p
JOIN PersonOwnAccount AS o ON (p.id = o.id)
JOIN Account AS a ON (o.account_id = a.id)
) AS results;
+---------+
| results |
+---------+
| true |
+---------+
GQL EXISTS subquery の GoogleSQL EXISTS subquery への書き換え
spanner> GRAPH FinGraph
RETURN EXISTS (
SELECT *
FROM Person AS p
JOIN PersonOwnAccount AS o ON (p.id = o.id)
JOIN Account AS a ON (o.account_id = a.id)
WHERE p.Name LIKE 'D%'
) AS results;
+---------+
| results |
+---------+
| true |
+---------+
これで全てです。 GoogleSQL サブクエリは GQL 内で使うことができます。
GQL subqueries in GoogleSQL
逆はどうでしょうか。こちらもドキュメントを隅まで読んでみると一箇所にそれらしい記述があります。
The following examples update an
Account
node and aTransfer
edge in the graph using Spanner Graph queries with DML:-- Use Graph pattern matching to identify Account nodes to update: UPDATE Account SET is_blocked = false WHERE id IN { GRAPH FinGraph MATCH (a:Account WHERE a.id = 1)-[:TRANSFERS]->{1,2}(b:Account) RETURN b.id }
このクエリがどの構文を使っているかを説明することはドキュメントの隅まで読んでも難しいことです。
結論から言うと、これは GQL IN subquery を GoogleSQL の DML の WHERE 句の中で使っています。
value [ NOT ] IN { gql_query_expr }
ところで先ほどの GQL IN subquery の構文に含まれた gql_query_expr
については公式リファレンス上にも定義がありません。
検証の結果、 GQL syntax に書かれたルールを使うとこのように定義できるものだとわかりました。
graph_query_expr:
[GRAPH clause]
multi_linear_query_statement
通常、 GQL クエリを実行する時は GQL か SQL 文かを区別するために先頭の GRAPH
は必須です。しかし、 GQL サブクエリでは GQL であることがわかっているため、 GRAPH
なしに GQL statement から始めることができます。
GRAPH FinGraph
RETURN 'Dana' IN {
-- GRAPH FinGraph は不要
MATCH (p:Person)-[o:Owns]->(a:Account)
RETURN p.name
} AS results;
しかし、 GoogleSQL に GQL を組み込む場合はどのプロパティグラフを使うのかを知ることができません。 先ほどの DML に含まれる GQL IN subquery の GRAPH
clause を削除すると、次のようなエラーが出ます。
spanner> UPDATE Account SET is_blocked = false
WHERE id IN {
MATCH (a:Account WHERE a.id = 1)-[:TRANSFERS]->{1,2}(b:Account)
RETURN b.id
};
ERROR: spanner: code = "InvalidArgument", desc = "No graph reference found in the current context of graph subquery. Try starting the graph subquery with the GRAPH clause [at 2:13]\\nWHERE id IN {\\n ^"
結果として、GoogleSQL に GQL を含める場合は、必ず次の形で含めなければなりません。
GRAPH clause
multi_linear_query_statement
前節の逆に GoogleSQL に GQL サブクエリを含む形に GQL subqueries のページの例を書き換えることで GQL サブクエリが GoogleSQL で使えることを示しましょう。
GQL ARRAY subquery
spanner> SELECT
p.name, account.id AS account_id,
ARRAY {
GRAPH FinGraph
MATCH (a:Account)-[transfer:Transfers]->(:Account)
WHERE a.id = account.id
RETURN transfer.amount AS transfers
} AS transfers
FROM Person AS p
JOIN PersonOwnAccount AS o ON (p.id = o.id)
JOIN Account AS account ON (o.account_id = account.id);
+------+------------+--------------------------+
| name | account_id | transfers |
+------+------------+--------------------------+
| Alex | 7 | [300.000000, 100.000000] |
| Dana | 20 | [500.000000, 200.000000] |
| Lee | 16 | [300.000000] |
+------+------------+--------------------------+
GQL EXISTS subquery
spanner> SELECT EXISTS {
GRAPH FinGraph
MATCH (p:Person)-[o:Owns]->(a:Account)
WHERE p.Name LIKE 'D%'
RETURN p.Name
LIMIT 1
} AS results;
+---------+
| results |
+---------+
| true |
+---------+
GQL IN subquery
spanner> SELECT 'Dana' IN {
GRAPH FinGraph
MATCH (p:Person)-[o:Owns]->(a:Account)
RETURN p.name
} AS results;
+---------+
| results |
+---------+
| true |
+---------+
GQL VALUE subquery
spanner> SELECT VALUE {
GRAPH FinGraph
MATCH (p:Person)
WHERE p.name LIKE '%e%'
RETURN p.name
LIMIT 1
} AS results;
+---------+
| results |
+---------+
| Alex |
+---------+
これで全てです。GQL サブクエリは全てが GoogleSQL の valid な式なことが確認できました!
まとめ
- GQL の式と GoogleSQL の式は完全に同一の概念であることが確認できました。
- Spanner の GQL は歴史は浅いですが、 GoogleSQL の式が使える事実は非常に強力であり、今後も強化され続けることが期待できます。
- 複雑さを増しますが、必要に応じて GQL と GoogleSQL をサブクエリで混在して自由に使い分けることができます。
- Spanner Graph の GQL は強力な言語です。使いましょう。
余談: GoogleSQL と GQL は実装を共有している
Spanner Graph がリリースされた頃から、 GQL は GoogleSQL と同じ場所で実装されているのではないかという予想はできました。
2024年11月にリリースされた ZetaSQL 2024.11.1 は GQL の実装を含んでいるため、ついにそれの答え合わせができた形となります。
Added the support for Graph query syntax and the related documentation.
よってこの記事の内容は実装の中身も確認できる事実です。
Discussion