fbpx

Cypherならできる!SQLには難しいこと10選(後編) #neo4j #cypher #sql

この記事は1年以上前に投稿されました。情報が古い可能性がありますので、ご注意ください。

本記事は先日投稿した前編からの続きです。

10-Things-You-Can-Do-With-Cypher

目次:Cypherならできる!SQLには難しいこと10選

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人目まで反復処理を行います。そうすると、以下のような結果になります。


built-in-data-structure-support

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つの値のみを表示します。


UNWIND-and-collect

実行後は、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します。これを実行すると、以下のような結果になります。


combine-read-and-write

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してユーザーを作成します。このクエリが作成する映画、ユーザー、リレーションシップのノードは以下です。


data-loading-csv

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リストを行にします。


data-loading-API

また、この入れ子になっているオブジェクトの方に手を伸ばし、データを引き出し、入れ子になっているフィールドからユーザーを作成することも可能です。

まとめ

ここまで、リレーショナルデータベースと比較してグラフデータベースを使用することの多くの利点を説明しながら、Cypherの機能を紹介してきました。是非、従来のSQLにはないこれらの機能を試してみてください!

下記の資料もぜひご覧ください:

  • Cypher Refcardは、Cypherの全機能を紹介する非常に便利なリファレンスで、詳細なCypherマニュアルへ直接飛べるリンクが随所に埋め込まれています。
  • CypherについてのGraphAcademy Trainingを行っており、インタラクティブ・ビギニングコース / アドバンスコースの用意があります。
  • Cypher開発者ガイドも提供しています。


Cypher クエリ言語をNeo4j AuraDBクラウドグラフ・データベース・インスタンスで今すぐ試してみましょう。
(クレジットカード不要)
Neo4j AuraDBを今すぐ無料で取得

New call-to-action
新規CTA