🔖

Spanner Graph: GoogleSQL expressions = GQL expressions

2024/12/04に公開

この記事は 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 にあるもの全てが使えるという記述が書かれています。

これは何を意味するかというと、 Spanner Graph の GQL は2024年にリリースされたばかりでありながら、ベータのパブリックリリースからだと2017年からの積み重ねがあり、 Google Cloud で最も力を入れられているデータベースの一つである Spanner の充実した関数ライブラリや豊富な型が使えるということです。

例えば次のようなことが言えます。

  • ベクトル検索ができる
  • Spanner Graph と同時に発表された全文検索もできる
  • GoogleSQL で最も特色ある型である Protocol Buffers も公式ドキュメント上の例はないが使える
    • もちろん他の型のリテラル、 STRUCT, JSON, ARRAY などの複雑な型のコンストラクタも GQL で使える。

Spanner の第一言語である GoogleSQL が強化されれば自然と GQL も強くなる。この事実は歴史があるグラフデータベースと比べても長所の一つとなることでしょう。

GoogleSQL で使える GQL 式

あまり気付かれていないかもしれませんが、逆に GQL のために追加された式レベルの要素は全て GoogleSQL 側のドキュメントにも書かれ、使用可能になっています。

これは何を意味するのでしょうか?
一つの意味は GRAPH_TABLE operator で SQL に GQL を組み込む時に、 GQL の RETURN statement で一旦 JSON 等に変換しなくてもそのまま SQL に持ち出して扱うことが可能であることを示しています。

https://cloud.google.com/spanner/docs/reference/standard-sql/graph-sql-queries#graph_table_operator

You can use the RETURN statement to produce output with graph pattern variables. These variables can be referenced outside GRAPH_TABLE. For example,

SELECT n.name, n.id
FROM GRAPH_TABLE(
  FinGraph
  MATCH (n)
  RETURN n
);

ただし、 他のネイティブのグラフデータベースの一部と異なり、GRAPH_ELEMENT, GRAPH_PATH 型は Spanner API の最終的な結果として返すことができない型のため最終的には他の型になる必要があります。

https://cloud.google.com/spanner/docs/reference/standard-sql/graph-sql-queries#graph_table_operator

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

逆はどうでしょうか。こちらもドキュメントを隅まで読んでみると一箇所にそれらしい記述があります。

https://cloud.google.com/spanner/docs/graph/insert-update-delete-data#update_nodes_or_edges_with_graph_queries_and_dml

The following examples update an Account node and a Transfer 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.1GQL の実装を含んでいるため、ついにそれの答え合わせができた形となります。

Added the support for Graph query syntax and the related documentation.

よってこの記事の内容は実装の中身も確認できる事実です。

Discussion