cypher:複数のmatchと、matchをカンマで繋がる違いが分からなくて頭を机に打ち付けたのはどう考えてもお前らが悪い!


挨拶

初めまして!株式会社プラハCEO兼エンジニアの dowanna です!
普段は会社経営とか会社のみんなで沖縄旅行をしていますが
「そういえば俺エンジニアだったな」と思い出したので、advent calendarに初参加してみました。
生暖かい目で見ていただけると喜びます。

本題

neo4jを使っているとcypherクエリ、特にmatchクエリを繋ぐ事がありますよね。

「cypherって、直感的でわかりやすいじゃん!」
そんな風に考えていた時期が、僕にもありました。

でも少しずつ複雑なクエリを書き始めると、意図しないmatchの挙動に悩まされて頭を机に打ち付けたくなるので、同じところで誰かが躓かないよう備忘録を残しておきます。

複数のmatchと、カンマつなぎのmatchの違いがわからない

特に僕が当初よく理解できなかったのが

match hogehoge
match fugafuga

と「matchクエリを」繋げる方法(パターン1)と

match hogehoge, fugafuga

と「カンマでクエリ自体」繋げる方法(パターン2)の違いです。

これ同じことじゃないの??と思っていました。

というわけで実験してみましょう。

データの準備

実験用の簡単なデータを作ります。

create (a:User{id:'1'}),(b:User{id:'2'}),(c:User{id:'3'})
create (a)-[:KNOWS]->(b)-[:KNOWS]->(c)

こんなグラフができます。

実験1:特定のnodeを取得する

では、試しにnodeを(パターン1)で取得してみましょう

match (a:User{id:'1'})
match (b:User{id:'2'})
return a.id, b.id

試しにnodeを(パターン2)で取得してみましょう

match (a:User{id:'1'}), (b:User{id:'2'})
return a.id, b.id

同じ結果が得られました。

「なんだ、やっぱり複数のmatchも、matchのクエリつなぎも同じやんけ」

と思いきや、patternマッチになると話が変わってきます

実験2:patternを取得する

まずは(パターン1=複数のmatch)で取得してみましょう。

match (a:User{id:'1'})-[:KNOWS]-(b:User)
match (a:User{id:'1'})-[:KNOWS*1..2]-(c:User)
return a.id, b.id, c.id

こんなクエリを書くことは少ないかもしれませんが、やっている事を要約すると:

①「1さんが1次的に繋がっているユーザを探す」
②「1さんが1〜2次的に繋がっているユーザを探す」
③「そいつらのidを返す」

(パターン1=複数のmatch)の結果はこちら

割と直感的というか、意図した通りの結果が返ってくるのが分かります。

では(パターン2=matchをカンマで繋ぐ)で取得してみましょう。

match (a:User{id:'1'})-[:KNOWS]-(b:User), (a:User{id:'1'})-[:KNOWS*1..2]-(c:User)
return a.id, b.id, c.id

あれ!?no records!?なんで!?

説明:matchをクエリで繋ぐと、一つのクエリと見なされる

cypherのmatchには「同じmatchの中で一度出てきたパターンは2度と出てこない」という仕様が存在します。
つまりパターン2を図解すると(画質悪くて申し訳ないです)

①「1さんが1次的に繋がっているユーザを探す」
②「1さんが1〜2次的に繋がっているユーザを探す」
③「そいつらのidを返す」

まず①「1さんが1次的に繋がっているユーザを探す」ため、赤矢印のパスを通ります。

次に②「1さんが1〜2次的に繋がっているユーザを探す」ため、また1さんを起点にユーザを辿ろうとします。
これを青矢印②としましょう。

しかしすでに①のパターンが出現しているため、「同じmatchの中で一度出てきたパターンは2度と出てこない」仕様により、このパターンが返ってくる事無く、matchクエリが終了します。

なので(パターン2=matchをカンマで繋ぐ)を実行した場合、
①「1さんが1次的に繋がっているユーザを探す」は2さんを返しますが
②「1さんが1〜2次的に繋がっているユーザを探す」はno recordsとなります。

cypherのmatchクエリをカンマでつないだ場合、後に実行された結果が返却されるため、②のno recordsが返されます。だから何も取得できません。

説明:matchを複数実行すると、別々のクエリと見なされる

では(パターン1=複数のmatch)の場合、何が起きているのか。

match (a:User{id:'1'})-[:KNOWS]-(b:User) // ①
match (a:User{id:'1'})-[:KNOWS*1..2]-(c:User) // ②
return a.id, b.id, c.id

①「1さんが1次的に繋がっているユーザを探す」
②「1さんが1〜2次的に繋がっているユーザを探す」
③「そいつらのidを返す」

処理の流れはパターン2と同じですが、カンマ区切りとは異なり、それぞれのmatchが独立したクエリとして解釈される点に注意が必要です。

まず①で赤矢印のパスを辿ります。

ここでmatchクエリが一回終了します。赤矢印が消えるようなイメージです。

match (a:User{id:'1'})-[:KNOWS]-(b:User) // ① ⇦このクエリは終了
match (a:User{id:'1'})-[:KNOWS*1..2]-(c:User) // ②
return a.id, b.id, c.id

そして新しいクエリとしてパスを辿りはじめます。青矢印ですね。
今度は「これまで一度も出てきた事のないパターン」なので、その先に進めます。

だからパターン1の結果には3が含まれているんですね。
パターン1は別々のmatchクエリだから、一つ目のmatchクエリのパターンが、二つ目のmatchクエリのパターンに影響しない。
パターン2は同じmatchクエリだから、一つ目のmatchクエリのパターンが、二つ目のmatchクエリのパターンに影響する

これが今回伝えたかったことです。

タイトルには「お前らが悪い!」と書きましたが、matchの仕様をちゃんと読み解いていなかった僕が悪い・・・いやでもtutorialでちゃんと言及しないお前らが・・・ドキュメントにも記載しないお前らが・・・ウゥッ!!!

まとめ

  • matchを複数繋ぐクエリと、matchの中身をカンマで繋ぐクエリには明確な違いがある
  • カンマで繋いだ場合、一つのmatchクエリと見なされる
  • 「matchクエリ中、同じパターンは2度と返ってこない」仕様を意識しておくと、正しく使い分けられる