グラフクエリ言語(Cypher・Gremlin)
グラフをJOINの連鎖で書く辛さから解放されます。宣言的パターンマッチ(Cypher)と命令的トラバーサル(Gremlin)の発想の違いと最適化の仕組みが分かります。
- 1.Cypher はグラフの形を丸ごと絵で書く宣言型。実行順序はオプティマイザに委ね、内部では index-free adjacency を活かしたポインタ辿りに変換される。
- 2.Gremlin はトラバーサルの手順を一手ずつ積み上げる命令型。パイプライン評価により中間結果を溜め込まず、実行順序が結果と性能を直接左右する。
- 3.SQLのJOINは無方向の集合演算だが、グラフクエリは有向の隣接を軸にした探索。どちらも最適化はまず開始点の絞り込み(選択性)から始まる。
SQLでグラフを書くと何が辛いのか
「友人の友人の友人」を SQL で書くと、自己結合が3段重なります。
SELECT DISTINCT f3.user_id
FROM friends f1
JOIN friends f2 ON f1.friend_id = f2.user_id
JOIN friends f3 ON f2.friend_id = f3.user_id
WHERE f1.user_id = 123;
ホップ数が増えるたびに JOIN が増え、結合順序をオプティマイザが誤ると中間結果が爆発します。SQLのJOINは本来「2つの集合から条件に合う組を全部作る」無方向の集合演算であり、グラフの「ある頂点から辺を辿って次の頂点へ」という有向・逐次的な移動とは発想の型が違います。グラフクエリ言語はこの移動そのものを一級市民として言語に組み込みます。代表が宣言型の Cypher と命令型の Gremlin です。
Cypher:グラフの形をパターンとして書く
Cypher(Neo4j が開発し openCypher として標準化)は、探したいグラフの形を ASCII アート風のパターンとして書く宣言型言語です。
MATCH (a:Person {name: "Alice"})-[:FRIEND]->(b)-[:FRIEND]->(c)
WHERE a <> c
RETURN DISTINCT c.name
丸括弧がノード、角括弧付きの矢印が辺を表し、(a)-[:FRIEND]->(b) は「a から FRIEND 辺で b へ」という向きを持つ隣接そのものです。宣言型なので、どの順にどう辿るかはユーザーが書かず、クエリオプティマイザが決めます。
内部では、この MATCH パターンはまず論理演算子のグラフ(Expand、NodeByLabelScan、Filter など)に変換され、実行計画へ落とされます。鍵となるのが Expand 演算子で、これは「ある頂点集合から特定方向・特定ラベルの辺をすべて展開する」操作です。ネイティブグラフDBでは各ノードが隣接エッジへの物理ポインタを持つ index-free adjacency の構造そのものを使って Expand を実行するため、1ホップの展開コストはグラフ全体の件数に依存せず、そのノードの次数(隣の数)だけに比例します。
オプティマイザの主な仕事は開始点(アンカー)の選択です。MATCH パターン中でラベルやプロパティ条件が最も絞り込める頂点をまず見つけ(例では name: "Alice" を持つ Person ラベル)、そこから Expand を連鎖させます。これは SQLオプティマイザがカーディナリティ推定で選択性の高いテーブルから結合を始めるのと同じ発想で、開始点を誤ると「全ノードから3ホップ展開」のような爆発が起きます。
(a)-[:FRIEND*1..3]->(c) のような可変長パス(1〜3ホップ)は、実装上は各ホップで Expand を繰り返す反復処理に展開されます。最短経路や到達可能性だけが欲しい場合はBFSで打ち切れますが、全パスの列挙が要求されると経路数が指数的に増えるため、Cypher実装の多くは重複頂点の再訪を避ける枝刈りや、最短距離のみを返す専用演算子を用意しています。
Gremlin:辿る手順そのものを組み立てる
Gremlin(Apache TinkerPop)は対照的に、トラバーサル(辿る操作)を関数呼び出しの連鎖として明示的に書く命令型・関数型言語です。
g.V().has('name', 'Alice').as('start')
.out('FRIEND')
.out('FRIEND')
.where(neq('start'))
.dedup()
.values('name')
g.V() が全頂点集合から出発し、has(...) で絞り込み、out('FRIEND') で「FRIEND辺を辿って先の頂点集合へ移る」を明示的に2回書いています。実行順序が構文の並び順とほぼ一致するため、書いた通りに実行されるのが命令型の特徴です。裏を返せば、どの順で辿るかの最適化責任の多くが書き手側にあるということでもあります。
Gremlin の実行モデルはパイプライン(ストリーム)評価です。g.V().has(...).out(...).out(...) の各ステップは、前段が1件生成するたびに後段へ即座に流すイテレータの連鎖として動きます。全頂点をいったん配列に集めてから次のステップへ渡すのではなく、1つの「トラバーサ(traverser、探索中の1本の経路情報を持つトークン)」がパイプラインを流れていくイメージです。
トラバーサの流れ(概念)
V() → 1件ずつ生成 ──┐
has(...)で通過/破棄 ──┤ 1件ずつ次段へ
out('FRIEND')で分岐 ──┤ (中間結果を全件バッファしない)
dedup()で重複除去 ────┘
この方式の利点は、limit(10) のような打ち切りがあるとパイプライン全体が早期終了できる点です。欠点は、has('name','Alice') を先に置くか out('FRIEND') の後に置くかで中間集合のサイズが桁違いに変わることで、絞り込み条件をできるだけ早いステップに書くのが定石になります。これは SQLで WHERE 条件をどのJOIN段で評価するかによって結合アルゴリズムの中間結果サイズが変わるのと同じ理屈で、Gremlinではオプティマイザ任せにできない分、書き手の責任がより重くなります。
Gremlinはステップの並び順がそのまま実行順序になるため、同じ論理的意図でも書き方次第で性能が数桁変わります。例えば次数の大きいハブ頂点を経由するステップを先に書くと、そこで中間トラバーサ数が急増します。多くの処理系(例:TinkerPop の一部戦略、JanusGraphの述語プッシュダウン)は簡単な並び替え最適化を行いますが、Cypherのオプティマイザほど積極的な計画探索は期待できません。
パターンマッチングの最適化:どちらも「開始点探し」に帰着する
宣言型と命令型は書き味が正反対でも、内部最適化のコア課題は共通しています。
| 観点 | Cypher(宣言型) | Gremlin(命令型) |
|---|---|---|
| 書くもの | グラフの形(パターン) | 辿る手順(ステップの連鎖) |
| 実行順序の決定者 | クエリオプティマイザ | 書き手(一部は処理系が並び替え) |
| 内部モデル | 論理演算子木→Expand連鎖に変換 | トラバーサのパイプライン評価 |
| 最適化の主軸 | 開始点(アンカー)選択と結合順序 | フィルタを早いステップへ寄せる |
| 向いている場面 | アドホックな探索的クエリ、宣言的な保守性 | 複雑な多段トラバーサルの細かい制御 |
Cypherの MATCH パターンは複数の開始候補(ラベル付き頂点のどれから始めるか)を持ちうるため、オプティマイザはカーディナリティ推定に基づき最も件数の絞れる頂点ラベル・プロパティ条件からアンカーを選び、そこから Expand を連鎖させる計画を立てます。これは複数テーブルの結合順序を探索するのと同型の問題で、探索空間が組み合わせ的に増えるため、多くの実装はヒューリスティクス(最も選択的な述語を持つパターン要素を優先するなど)で妥協します。
Gremlinには対応するオプティマイザ層が薄いため、同じ最適化判断をクエリの書き方自身に埋め込むことになります。g.V().hasLabel('Person').out('FRIEND') と g.V().out('FRIEND').where(hasLabel('Person')) は論理的に近くても、前者はフィルタ後にExpandする分だけ中間トラバーサ数を抑えられ、実務上はっきり速くなることがあります。
探索的・アドホックなクエリや、クエリの保守性・可読性を優先するならCypher。既存のグラフ処理パイプラインへの組み込みや、ステップ単位で挙動を細かく制御したい複雑なトラバーサル(条件分岐、副作用のあるステップなど)にはGremlinが向きます。多くのグラフDBエンジン(JanusGraph、Amazon Neptuneなど)は両方の言語を同じストレージ層に対して受け付けられるようにしており、内部の物理隣接構造は共通です。
Cypherは宣言的パターンマッチで実行順序をオプティマイザに委ね、内部ではExpand演算子によりindex-free adjacencyを辿ること。Gremlinは命令的トラバーサルで、パイプライン(ストリーム)評価により中間結果をバッファせず1本ずつ流すこと。両者とも最適化の核心は開始点の絞り込み(選択性)にあり、SQLのJOINが無方向の集合演算であるのに対しグラフクエリは有向の隣接探索である、という対比を説明できるように。
まとめ
グラフクエリ言語は「グラフの形」か「辿る手順」のどちらを書くかで二分されます。CypherはASCIIアート風のパターンで形を宣言し、オプティマイザが開始点を選んでExpand演算子の連鎖に変換、内部では index-free adjacency の物理ポインタを辿ります。Gremlinは辿る手順を関数呼び出しの連鎖として明示し、パイプライン評価で1本のトラバーサを流しながら処理するため、ステップの並び順が性能を直接左右します。SQLのJOINが無方向の集合演算であるのに対し、両言語とも有向の隣接探索を一級市民として扱う点が本質的な違いです。最適化の核心はどちらも同じで、最も絞り込める開始点を見つけ、そこから展開すること。これは結合順序探索やカーディナリティ推定がSQL側で担う役割と地続きであり、ストレージ側のindex-free adjacencyがその高速な展開を支えています。
データベース Article
グラフクエリ言語(Cypher・Gremlin)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
グラフDB
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 5
導入後に効く点
Gremlin はトラバーサルの手順を一手ずつ積み上げる命令型。パイプライン評価により中間結果を溜め込まず、実行順序が結果と性能を直接左右する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 5
判断チェックリスト
- 自社の用途が「グラフDB / Cypher」に近いか確認する。
- 強みである「Cypher はグラフの形を丸ごと絵で書く宣言型。実行順序はオプティマイザに委ね、内部では index-free adjacency を活かしたポインタ辿りに変換される。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。