データ指向アプリケーションデザイン | 第02章 データモデルとクエリ言語

発表者: キム・ジョンス、パク・スミン

発表資料

章の目的

  • 各データモデルの違いと特徴を理解する。
  • 作ろうとしているアプリケーションに、どのデータモデルが適しているか判断できるようにする。

RDB vs NoSQL

  • 第2章ではデータモデルの違いだけに集中する。
    • その他の内容: 耐障害性(第5章)、並行性制御(第7章)
  • NoSQL
    • スキーマの柔軟性と、局所性に起因するより高い性能
    • 一部のアプリケーションでは、アプリケーションが使うデータ構造により近い
  • RDB
    • 結合、N:1、N:Nの関係をうまくサポートする

NoSQL

文書に似た構造を複数のテーブルに分割するリレーショナル手法の問題点:

  • 扱いにくいスキーマ
  • 不要で複雑なアプリケーションコードが発生する

ドキュメントモデルの制限

  • 文書内のネストされた項目を直接参照できない。リレーショナルモデルでは、関連するキーさえあれば直接参照できるが、キーを探すために不要な巡回が発生することもある。
    • 例: ユーザーの詳細住所を参照する場合
      • user.address.road.detail
    • ネストが深すぎなければ、一般的には問題にならない。
  • 結合サポートが不十分
    • アプリケーションによって、問題になる場合もあればならない場合もある。
  • 多対多の関係が必要な場合は非効率
    • アプリケーションの複雑度が上がる。
    • アプリケーション側で処理する方法は、データベース内部の特化したコードで実行するより性能が悪い。

スキーマの柔軟性

特定のスキーマを強制しない。

  • 任意のキーと値を文書に追加できる。
  • 文書に含まれるフィールドの存在は保証されない。

アプローチによる処理方法の違い:

例: userにfirst_nameを追加する場合

// ドキュメントモデル
// アプリケーションでデータを読む場合を処理するコードだけあればよい。
// (データベースの変更を要求しない方法だが、よい方法には見えない。方法の違いを理解する程度にとどめよう。)
if (user?.name && user?.first_name == null) {
    user.first_name = user.name.split(" ")[0]
}
 
 
// リレーショナルモデル
// 別途スキーマ変更とデータマイグレーション作業が必要
ALTER TABLE users ADD COLUMN first_name test;
UPDATE users SET first_name = substring_index(name, ' ', 2);

ストレージ局所性

局所性: データやプログラムなどについて、特定の部分に集中的にアクセスする性質。

文書全体に頻繁にアクセスする必要がある場合、ストレージ局所性を活用できる。

  • 正規化されたリレーショナルモデルの構造より、非正規化されたドキュメントモデルの構造のほうが性能上の利点がある。
    • リレーショナルモデルは検索のために複数のインデックス検索が必要で、より多くのディスクシークと時間が必要になる。
  • 一度にその文書の多くの部分を必要とする場合にだけ適用される。
    • ドキュメントモデルでは、小さな部分だけにアクセスする必要がある場合でも文書全体を保存しなければならないため、大きな文書では無駄になり得る。

一般的には、文書をできるだけ小さく保ち、文書サイズの増加を最小化することが推奨される。

  • このような性能上の制限により、ドキュメントモデルが有用な状況は大きく減る。

RDBとNoSQLの統合

互いに似た機能を提供するという意味である。

RDB

  • MySQLを除くほとんどのRDBはXMLをサポートしており、ドキュメントモデルと非常に似たデータモデルを使用できる。
  • PostgreSQL 9.3+、MySQL 5.7+、DB2 10.5+は、JSON文書に対して似た機能を提供する。

NoSQL

  • RethinkDB: クエリでリレーショナル結合をサポートする。
  • MongoDB: ドライバーがデータベース参照を自動で解決する。結合はクライアント側で実行され、追加のネットワーク往復が必要で、最適化も不十分なため結合性能はよくない。

クエリ言語

宣言型 vs 命令型

  • SQLは宣言型である。
  • IMS、CODASYLは命令型であるが、ここでは重要ではない。

宣言型クエリ言語の利点:

  • 知りたいデータのパターンだけを指定すればよい。どのインデックスや結合関数を使うか、どの順序で実行するかは、データベースのクエリ最適化器の仕事である。
    • 満たすべき条件
    • データの変換、つまりソート、グループ化、集計など
  • 一般的に命令型クエリAPIより簡潔で扱いやすい。
  • データベースエンジンの詳細な実装が抽象化されているため、クエリを変更せずにデータベース性能を向上させることができる。

Webの例:

HTMLにstyleを適用する場合:

  • CSSは宣言型である。
  • JSでDOM APIを使う方法は命令型である。
  • 宣言型は命令型より、可読性、生産性、保守性に優れている。

MapReduceクエリ

ここでは重要ではない。

関数型プログラミングにあるmap、reduce関数を基盤にしている。

欠点:

  • クエリを書くより難しい。
  • 宣言型クエリ言語は、クエリ最適化器にクエリ性能を高める機会を与える。

MongoDB 2.2では、aggregate pipelineという宣言型クエリ言語のサポートが追加された。

グラフ型データモデル

アプリケーションのデータモデルが主に1:N、つまりツリー構造のデータである場合、またはエンティティ間に関係がない場合は、ドキュメントモデルが適している。

しかし、N:Nの関係が非常に一般的な場合は、グラフモデルが適している。

グラフの構成要素

頂点(Vertex、またはノードやエンティティ)。 Spring Dataではノードエンティティと呼ぶ(@NodeEntity)。 辺(Edge、または関係や弧 arc)。

例:

  • ソーシャルグラフ
    • 頂点 = 人、辺 = 友人関係
  • Webグラフ
    • 頂点 = Webページ、辺 = リンク
  • 道路ネットワーク
    • 頂点 = 交差点、辺 = 道路
  • Facebook
    • 複数種類の頂点と辺を単一のグラフとして保持する。
    • 頂点 = 人、場所、イベント、チェックイン、コメントなど
    • 辺 = 人同士の関係、チェックインが発生した位置、誰がどの投稿にコメントしたか、誰がイベントに参加したかなど

もしFacebookをリレーショナルモデルで作るなら?

  • 人、場所、イベント、チェックイン、コメントなどがすべてテーブルとして定義されるだろう。
  • そして各テーブル間に非常に複雑な関係が必要になるだろう。
  • グラフモデルを適用した瞬間、このような複雑さは単純化される。

グラフモデルの種類

  • プロパティグラフモデル
  • トリプルストアモデル。ここでは重要ではない。

グラフ用の宣言型クエリ言語

  • Cypher
  • SPARQL
  • Datalog

プロパティグラフ

頂点の構成要素:

  • id
  • 出力(outgoing)辺の集合
  • 入力(incoming)辺の集合
  • プロパティのコレクション、キーと値のペア

辺の構成要素:

  • id
  • 辺が始まる頂点(tail vertex)
  • 辺が終わる頂点(head vertex)
  • 2つの頂点間の関係タイプを説明するラベル
  • プロパティのコレクション、キーと値のペア

リレーショナルスキーマを使ってプロパティグラフを表現する:

CREATE TABLE vertices (
    vertex_id integer PRIMARY KEY,
    properties json
)
 
 
CREATE TABLE edges (
    edge_id integer PRIMARY KEY,
    tail_vertex integer REFERENCES vertices(vertex_id),
    head_vertex integer REFERENCES vertices(vertex_id),
    label text,
    properties json
)
 
 
CREATE INDEX edges_tails ON edges(tail_vertex)
CREATE INDEX edges_heads ON edges(head_vertex)
  • 頂点は他の頂点と辺で接続される。
    • どの型と関連するかを制限するスキーマはない。
  • ある頂点が与えられれば、その頂点の入力辺と出力辺を効率よく探すことができ、グラフを走査できる。
  • 関係の種類ごとに異なるラベルを使うことで、単一のグラフに異なる種類の情報を保存しながら、データモデルをきれいに保てる。

このような機能により、グラフはデータモデリングに多くの柔軟性を提供する。

グラフは発展性が高く、アプリケーションに機能を追加する場合でも、データ構造の変更を受け入れやすいように簡単に拡張できる。

Cypher

プロパティグラフのための宣言型クエリ言語。

  • Neo4jグラフデータベース用に作られた。
  • Wiki: Cypherは、パターンで表現されるプロパティグラフを効率よく問い合わせ、更新できる宣言型グラフクエリ言語である。Cypherは比較的単純だが非常に強力な言語であり、非常に複雑なデータベースクエリもCypherで簡単に表現できる。

データモデルの作成:

CREATE
(NAmerica:Location {name:'North America', type:'continent'}),
(USA:Location {name:'United States', type:'country'}),
(Idaho:Location {name:'Idaho', type:'state'}),
(Lucy:Person {name:'Lucy'}),
(Idaho) -[:WITHIN]-> (USA) -[:WITHIN]-> (NAmerica),
(Lucy) -[:BORN_IN]-> (Idaho)

問題: アメリカからヨーロッパへ移住したすべての人の名前を探す。

MATCH
(person) -[:BORN_IN]-> () -[:WITHIN*0..]-> (USA:Location {name:'United States'}),
(person) -[:LIVES_IN]-> () -[:WITHIN*0..]-> (EU:Location {name:'Europe'})
RETURN person.name

クエリの実行方法:

  1. すべての人の検索から始め、出生地と居住地を確認し、条件に合う人だけを返す。
  2. 2つのLocationから始め、アメリカとヨーロッパのすべての位置を探し、leafに該当する頂点のうち、BORN_IN、LIVES_INの入力辺を通じて見つかった人を返す。

宣言型の利点:

  • 実行方法を詳しく記述する必要がない。
  • クエリ最適化器が最も効率的な戦略を自動で選択する。

リレーショナルモデルで上のクエリを行うなら?

  • 可能ではあるが難しい。
  • 通常、リレーショナルモデルではクエリに必要な結合を事前に知っている。FROM句で宣言するためである。
  • しかしグラフクエリでは、探している頂点のために複数の辺を走査しなければならない。
    • 何回走査するか分からない。
    • 結合数を事前に固定できない。

Cypherクエリでは、-[:WITHIN*0..]->によって走査を非常に簡潔に表現する。*0は0回以上を意味する。

SQL:1999以降、可変長の走査経路に対するクエリは、再帰共通テーブル式(Recursive common table expression、以下Recursive CTE)、つまりWITH RECURSIVE文を使って表現できる。

  • PostgreSQL、DB2、Oracle、SQL Serverでサポートされる。
  • MySQL 5.7+でサポートされる。

トリプルストアとSPARQL

ここでは重要ではない。

プロパティグラフモデルとほぼ同じである。

  • 同じ概念に対する用語だけが異なる。

データを主語(subject)、述語(predicate)、目的語(object)という非常に単純な三部構文(three-part statements)形式で保存する。

  • 主語 = 頂点
  • 目的語 = 別の頂点、またはプリミティブデータ型のデータ
  • 述語 = 辺

Turtle

  • Wiki: Terse RDF Triple Language、つまりTurtleは、Resource Description Frameworkデータモデルでデータを表現するための構文およびファイル形式である。Turtle構文は、RDFクエリ言語であるSPARQLの構文に似ている。

SPARQLは、RDFデータモデルを使用するトリプルストアクエリ言語である。

Datalog

データモデルはトリプルストアモデルに似ている。

主語、述語、目的語 -> 述語(主語、目的語)

グラフデータベースランキング

https://db-engines.com/en/ranking/graph+dbms

一行要約: グラフデータベースを使いたい場合はNeo4jを使えばよい。

Neo4j

Spring Dataがサポートしている。

https://spring.io/guides/gs/accessing-data-neo4j/

spring-boot-starter-data-neo4j
@NodeEntity
data class Food(
    @Id
    val id: Long? = null,
    val name: String,
}
 
 
interface FoodRepository : Neo4jRepository<Food, Long>
 
 
@NodeEntity
data class Store(
    @Id
    val id: Long? = null,
    val name: String,
    @Relationship(type = "has")
    val foods: Set<Food>,
}
 
 
interface StoreRepository : Neo4jRepository<Store, Long>

Summary

  • 歴史的には、データを1つの大きなツリーとして表現しようとしてきた。
  • N:N関係の表現には適していなかったため、リレーショナルモデルが登場した。
  • 最近になって、リレーショナルモデルにも適していないアプリケーションがあることが分かり、非リレーショナルデータモデルであるNoSQLが登場した。

NoSQLには2つの主要な系統がある。

  • ドキュメントモデル
    • すべてのデータが文書に含まれ、文書間の関係がほとんどない場合に使う。
  • グラフモデル
    • ドキュメントモデルとは正反対に、すべてが潜在的に関連している場合に使う。

3つのモデルはいずれも現在広く使われている。

  • あるモデルで別のモデルをまねることはできるが、多くの場合その結果は混乱したものになる。

ドキュメントモデル、グラフモデルの利点:

  • 保存するデータのためのスキーマを強制しないため、変化する要件に合わせてアプリケーションを簡単に変更できる。