Cypherならできる!SQLには難しいこと10選(後編) #neo4j #cypher #sql
この記事は1年以上前に投稿されました。情報が古い可能性がありますので、ご注意ください。
本記事は先日投稿した前編からの続きです。
目次:Cypherならできる!SQLには難しいこと10選
- CypherとSQLの比較
- データセット
- グラフパターンとJOINの比較
- 1. 経路パターン
- 2. 最短経路
- 3. GROUP BYを使用せずに集計
- 4. ビルトインのデータ構造サポート:リストとマップ
- 5. UNWIND と Collect
- 6. マップ・プロジェクションとパターン内包
- 7. ReadとWriteの併用
- 8. トランザクショナル・バッチング
- 9. ビルトインのデータローディング:CSV
- 10. ビルトインのデータローディング: API
- まとめ
(注:「3. GROUP BYを使用せずに集計」以前は前編にて)
4. ビルトインのデータ構造サポート:リストとマップ
プログラミング経験者であれば、リストやマップを第一級関数として利用することの便利さをご存知でしょう。Cypherはリストをフルサポートしています。range、slice、マップなどが作成できます。リスト、リスト内包、リスト上の量化子に対するインデックスアクセスが可能です。さらに、リストへの条件付与と供に、マップについては、ドットアクセス、キーアクセス、マッププロジェクションを用意しています。
// リスト内包表記 WITH [id IN range(1,100) WHERE id%13 = 0 | // リテラルマップの構築 {id:id, name:'Joe '+id, age:id%100}] as people // リスト要素に対するリスト量子述語 WHERE ALL(p in people WHERE p.age % 13 = 0) RETURN people
上記のリストとマップの使い方の例は、少しわざとらしいですね。100個の数値の範囲から始めたので、1から100まで、リストに対して反復処理を行います。それらのIDはこのリストの中にあります。
そして、私のラッキーナンバーは13なので、フィルターを加えて、13おきの数字 ー つまり、13で割り切れる数字をすべて選びます。そのIDごとに、次の要素を含むマップを作成します:IDフィールド、IDを含む連結した文字列であるnameフィールド、IDを100で割った余りであるageフィールド。1人目から99人目まで反復処理を行います。そうすると、以下のような結果になります。
5. UNWIND と Collect
次のUNWINDおよびCollectという機能は、私が構築に携わったものです。Collect関数は、数値の羅列をリストに変換するものです。UNWINDはその逆です。Collectは複数の行をリストに変換し、UNWINDはリストを複数の行に変換します。UNWINDは、特にリストに基づくデータ作成・更新において威力を発揮します。マップや配列のリストのようなデータを渡すと、UNWINDでリストの要素に対して反復処理を行い、すべての要素に対してエンティティを作成または更新することができます。
// リストを10,000行に変換 UNWIND range(1,10000) as id CREATE (p:Person {id:id, name:"Joe "+id, age:id%100}) // 人を各年齢層に集計し、年齢層ごとにCollectする RETURN p.age, count(*) as ageCount, collect(p.name)[0..5] as ageGroup
上記の例では、10,000人のリストを、1から10,000までのIDフィールドを持つ行に変換します。そして各行に、IDを持つ人物を作成します。
Collectの逆関数では、グループ化キーとして年齢を返し、各年齢層に属する人数のcountを返します。あらゆる表現をCollectできます。この例では人の名前をCollectしています。次に、Collectに対してリストスライス操作を行い、最初の5つの値のみを表示します。
実行後は、1から100までの年齢層が表示されます。100 x 100 = 10,000なので、各年齢層には100人ずつ入っています。リストに表示されるのは、各年齢層の最初の5人です。
6. マップ・プロジェクションとパターン内包
Neo4jは、グラフ型ドメインデータのAPIクエリ言語である「GraphQL」に注目してきました。6年ほど前、私たちはGraphQLのとある重要な機能をCypherに追加できないかと考えました。それは「マップ・プロジェクション」と「パターン内包」です。
MATCH (m:Movie) // プロパティへアクセスする マップ・プロジェクション RETURN m { .*, // パターン内包 actors: [(m)<-[r:ACTED_IN]-(a) | // プロパティへのアクセス、入れ子の式 a { .name, roles: r.roles, movies: size([()<-[:ACTED_IN]-(a)|a]) } ][0..5], // リストスライス操作 // フィルタと式によるパターン内包 directors: [(m)<-[:DIRECTED]-(d) WHERE d.born < 1975 | d.name] } as movieDocument LIMIT 10 <サンプルデータセット: Movies>
上記のマップ・プロジェクションの例では、 Moviesのデータベースを利用し、検索対象の映画に関する全てのプロパティをRETURNしています。上の例の.* がそれにあたります。また、このマップには他のフィールドを追加することもできます。例えば、具体的なフィールド名を追加して入れ子構造のマップにすることもできます。これは、フロントエンドやJavaScriptアプリケーションで、入れ子構造になっているデータをJSONオブジェクトとして取得するなど、ドキュメントのような要素をRETURNしたい場合に適しています。最後に、WHERE句でのパターン内包は、 list comprehension(リスト内包)と似ていますが、要素のリストではなく、関連するグラフパターンを対象としている点が違います。
7. ReadとWriteの併用
SQLを使ったことがある人なら、ReadとWriteのサポートの必要性に気付くかもしれません。insert文やselectからのinsertはできますが、それくらいのものです。そこで、ReadとWriteを同じクエリで組み合わせて使うことをおすすめします。GraphQLでは、データベースを更新しながら、データをフェッチすることも可能です。この例では、映画のタイトル、人名、評価の星をパラメータに設定するだけです。このパラメータをクエリするには、まずReadで映画を探し、Write文を打ちます。
param rating=>({title:'The Matrix',name:'Emil Eifrem', stars:5}) MATCH (m:Movie {title:$rating.title}) // 必要があれば、write文 (※create if not exists と同意) MERGE (u:User {name:$rating.name}) // 必要があれば、write文 MERGE (u)-[r:RATED]->(m) // write文 SET r.stars = $rating.stars WITH * MATCH (m)<-[:ACTED_IN]-(a:Person) // read & return RETURN u, m, r, collect(a) <サンプルデータセット: Movies>
Userが存在しない場合は、「Emil Eifrem」という名前でUserを作成します。UserとMovieの間の「RATED(評価)」のリレーションシップが存在しない場合は作成し、既に存在する場合はアクセスします。そして、新しい評価を設定します。今回は5です。更新後、再びReadを行い、この映画に出演した全ての俳優を探し、ユーザー、映画、評価、そして全ての俳優をRETURNします。これを実行すると、以下のような結果になります。
8. トランザクショナル・バッチング
データベースで大規模な更新を行う場合、更新の情報はトランザクションがコミットされるまでメモリ上に保持されるという事実に対処する必要があります。トランザクショナル・バッチングでは、一度に使用するメモリ量を制限することができます。
:auto MATCH (o:Order) // 1億の注文情報の場合 CALL { WITH o MATCH (o)-[r:ORDERS]->() WITH o, sum(r.count) as products, sum(toFloat(r.unitPrice)*r.quantity) as total SET o.count = products, o.total = total } IN TRANSACTIONS OF 100000 ROWS <サンプルデータセット: Northwind>
注文情報から2つの新しいフィールドを計算したいケースです。それぞれのORDERについて、商品数と合計金額を取得しています。単価と数量を掛け合わせたものが、合計金額です。この2つの属性をORDERノードに設定します。ここでは、各サブ・トランザクションにおいて10万回の更新を行うことになります。これを実行すると、次々とトランザクションでデータが更新されていく様子がわかると思います。
9. ビルトインのデータローディング:CSV
Neo4j では、URL を介して CSV ファイルにアクセスし、CSV ファイルを使用してグラフの更新を行うことができます。
WITH "https://data.neo4j.com/importing/ratings.csv" AS url LOAD CSV WITH HEADERS FROM url AS row MATCH (m:Movie {movieId:row.movieId}) MERGE (u:User {userId:row.userId}) ON CREATE SET u.name = row.name MERGE (u)-[r:RATED]->(m) SET r.rating = toFloat(row.rating) SET r.timestamp = toInteger(row.timestamp) <サンプルデータセット: Movies>
URLを使って、CSVファイルをマップ構造に変換します。各行がクエリツールで使用できるマップ構造のように作用するのです。ユーザーが存在しない場合は、MERGEしてユーザーを作成します。このクエリが作成する映画、ユーザー、リレーションシップのノードは以下です。
10. ビルトインのデータローディング: API
ここでは、APIから直接JSONデータをロードすることができる、ユーザー定義の手順を用意しています。
WITH "https://api.stackexchange.com/2.2/questions?pagesize=2&order=desc&sort=creation&tagged=neo4j&site=stackoverflow&filter=!5-i6Zw8Y)4W7vpy91PMYsKM-k9yzEsSC1_Uxlf" AS url // URL から json データをロード CALL apoc.load.json(url) YIELD value // 要素リストを行に変換 UNWIND value.items AS item // 行のデータを分解 RETURN item.title, item.owner, item.creation_date, keys(item) LIMIT 5;
YIELDを使ってデータを受け取り、UNWINDし、このJSONリストを行にします。
また、この入れ子になっているオブジェクトの方に手を伸ばし、データを引き出し、入れ子になっているフィールドからユーザーを作成することも可能です。
まとめ
ここまで、リレーショナルデータベースと比較してグラフデータベースを使用することの多くの利点を説明しながら、Cypherの機能を紹介してきました。是非、従来のSQLにはないこれらの機能を試してみてください!
下記の資料もぜひご覧ください:
- Cypher Refcardは、Cypherの全機能を紹介する非常に便利なリファレンスで、詳細なCypherマニュアルへ直接飛べるリンクが随所に埋め込まれています。
- CypherについてのGraphAcademy Trainingを行っており、インタラクティブ・ビギニングコース / アドバンスコースの用意があります。
- Cypher開発者ガイドも提供しています。
Cypher クエリ言語をNeo4j AuraDBクラウドグラフ・データベース・インスタンスで今すぐ試してみましょう。
(クレジットカード不要)
Neo4j AuraDBを今すぐ無料で取得