cypher:やはり俺のwithの使い方はまちがっている(collectを使ったページネーションの作法)


挨拶

初めまして!株式会社プラハCEO兼エンジニアの dowanna です!
普段は会社経営とか社長の給料を社内で一番低くしたり副業にうつつを抜かす自社エンジニアをとっちめたりしていますが
「そういえば俺エンジニアだったな」と思い出したので、advent calendarに2度目の参加を試みました。
チンして5分経ったスープのような生暖かい目で見ていただけると喜びます。

本題

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

「cypherって、withさえあれば無敵じゃん!」
そんな風に考えていた時期が、僕にもありました。

でも少しずつ複雑なクエリを書き始めると意図しないwithの挙動に悩まされて定期的に嘔吐するようになったので、少しでも誰かの詰まりどころを解消できたら良いなと思い、今日はwithの癖を少し解説したいと思います。

withケース1:nodeと総数を取得したい

webサービスを作っていると、ページネーションを実装したい時が出てきますよね。
ページネーションの際には「ページサイズ分のnode」と「node総数」が欲しい時がありますよね。
そんな時は、こんなクエリを書く事になるでしょう。

実験データの作成

create (a:User{id:'1'}),(b:User{id:'2'}),(c:User{id:'3'})

nodeと総数の取得

match (u:User)
with u, count(u) as total
return u.id, total

よーし、これで今日の仕事終わり!ゼルダ遊ぼ!と思っていたら、こんな結果に:

あれ!?
total=3になるはずなのに、どうしてtotal=1!?

と思いつつ、こんな具合にクエリを書き換えてみます

match (u:User)
with collect(u) as users, count (distinct u) as total // 一度uのリストを作って
unwind users as u                                     // uのリストを元に戻す
return u.id, total

すると、当初想定していた結果が得られました:

これで無事ゼルダが遊べるわけですが、理由が分からないと気持ち悪いのでneo4jのコミュニティに問い合わせてみました。

曰く、

with u, count(u) is aggregating the count and grouping by u. Since each u is unique, the count would thus be 1 for each u.

the collect(u) as users creates a single collection of all users and for this collection we count all users and thus a single collection of users and a count of 3.

日本語でおk

  • uはそれぞれ異なるnodeなので、u毎にcountが行われる(count=1)
  • collect(u)を行うと、uをまとめたlistに対してcountが行われる(長さ3のリストに対するcount=3)

withでaggregation関数を使う時は、関数の引数(node)がどのようなgroupを形成しているか意識する必要がありそうです。
ただwithで渡したnodeに対してcountするのと、collect()した結果とに対してcountするのとでは意味が違うようです。

withケース2:要素数を制限したい

returnするクエリ結果を絞るだけなら skip X limit Yすれば良いのですが

match (u:User)
return u.id
skip 1 limit 1

例えば高負荷なクエリを流す必要があるけれど、DBに100万件のuserが存在するため、全てのuserにはクエリを実行したく無い場合:

match (u:User)
// 鬼のように複雑で高負荷なmatch,create,setクエリ
return u.id
skip 1 limit 1

100万件のuserにクエリを流してからskip limitしても、高負荷なクエリが100万件に実行された後に絞り込んでいるので、パフォーマンス上の問題が出るかもしれません。高負荷なクエリを流す前に、途中の検索結果を絞りたくなりますよね。
そんな時は以下のようなクエリを書けば解決です!

collectする時に要素数を絞る

match (u:User)
with collect(u)[1..2] as users
unwind users as u
// 鬼のように複雑で高負荷なmatch,create,setクエリ
return u.id

え〜でもcollectしてunwindなんて無駄っぽいから嫌だよ〜という方にはコチラ

withで繋ぐ時にskip limitで要素数を絞る

match (u:User)
with u
skip 1 limit 1
// 鬼のように複雑で高負荷なmatch,create,setクエリ
return u.id

個人的には「withにもskip limitが使える」事を知った時はヒャッハー!と叫びたくなったので、是非皆さんも使って叫んでみてください。

まとめ

  • withの中でaggregation関数を使う時は、groupingに注意
  • withもskip limit出来る。ヒャッハー!!