このドキュメントでは、効率的なクエリ、最適化されたエッジ トラバーサル、効果的なデータ管理手法に焦点を当てて、Spanner Graph スキーマを設計するためのベスト プラクティスについて説明します。
Spanner スキーマ(Spanner Graph スキーマではない)の設計については、スキーマ設計のベスト プラクティスをご覧ください。
スキーマ設計を選択する
スキーマ設計はグラフのパフォーマンスに影響します。次のトピックでは、効果的な戦略を選択する方法について説明します。
スキーマ化された設計とスキーマレス設計
スキーマ化された設計では、グラフ定義が Spanner Graph スキーマに保存されます。これは、定義の変更が少ない安定したグラフに適しています。スキーマはグラフ定義を適用し、プロパティはすべての Spanner データ型をサポートします。
スキーマレス設計では、データからグラフ定義が推測されるため、スキーマの変更を必要とせずに柔軟性を高めることができます。動的ラベルとプロパティは、デフォルトでは適用されません。プロパティは有効な JSON 値である必要があります。
スキーマ データ マネジメントとスキーマレス データ マネジメントの主な違いは次のとおりです。使用するスキーマのタイプを決定するうえで、グラフクエリも考慮してください。
| 機能 | スキーマ化されたデータ マネジメント | スキーマレス データ マネジメント |
|---|---|---|
| グラフ定義の保存 | グラフ定義は Spanner Graph スキーマに保存されます。 | グラフの定義はデータから推測されます。ただし、Spanner Graph はデータを検査して定義を推測しているわけではありません。 |
| グラフ定義の更新 | Spanner Graph スキーマの変更が必要です。定義が明確で、変更が頻繁でない場合に適しています。 | Spanner Graph スキーマの変更は必要ありません。 |
| グラフ定義の適用 | プロパティ グラフ スキーマは、エッジで許可されるノードタイプを適用します。また、グラフノードまたはエッジタイプで許可されるプロパティとプロパティ タイプも適用します。 | デフォルトでは適用されません。チェック制約を使用して、ラベルとプロパティのデータの完全性を適用できます。 |
| プロパティのデータ型 | timestamp など、任意の Spanner データ型をサポートします。 |
動的プロパティは有効な JSON 値である必要があります。 |
グラフクエリに基づいてスキーマ設計を選択する
多くの場合、スキーマ化された設計とスキーマレス設計は、同程度のパフォーマンスを提供します。ただし、クエリで複数のノードタイプまたはエッジタイプにまたがる定量化されたパスパターンを使用する場合は、スキーマレス設計のほうがパフォーマンスが向上します。
その主な理由は、基盤となるデータモデルです。スキーマレス設計では、すべてのデータが単一のノードテーブルとエッジテーブルに保存され、DYNAMIC LABEL が適用されます。複数のタイプをトラバースするクエリは、テーブル スキャンを最小限に抑えて実行されます。
一方、スキーマ化された設計では、通常、ノードタイプとエッジタイプごとに個別のテーブルが使用されます。このため、複数のタイプにまたがるクエリでは、対応するすべてのテーブルからデータをスキャンして結合する必要があります。
次のクエリは、スキーマレス設計で適切に動作するクエリの例と、両方の設計で適切に動作するクエリの例です。
スキーマレス設計
次のクエリは、複数のタイプのノードとエッジを照合できる定量化されたパスパターンを使用するため、スキーマレス設計のほうがパフォーマンスが向上します。
このクエリの定量化されたパスパターンは、複数のエッジタイプ(
TransferまたはWithdraw)を使用しており、1 ホップを超えるパスの中間ノードタイプを指定していません。GRAPH FinGraph MATCH p = (:Account {id:1})-[:Transfer|Withdraw]->{1,3}(:Account) RETURN TO_JSON(p) AS p;このクエリの定量化されたパスパターンは、複数のエッジタイプ(
OwnsまたはTransfers)を使用して、PersonノードとAccountノード間の 1~3 ホップのパスを検索します。長いパスの中間ノードタイプは指定しません。これにより、パスはさまざまなタイプの中間ノードを通過できます。例:(:Person)-[:Owns]->(:Account)-[:Transfers]->(:Account)GRAPH FinGraph MATCH p = (:Person {id:1})-[:Owns|Transfers]->{1,3}(:Account) RETURN TO_JSON(p) AS p;このクエリの定量化されたパスパターンは、エッジラベルを指定せずに、
PersonノードとAccountノード間の 1~3 ホップのパスを検索します。前のクエリと同様に、さまざまなタイプの中間ノードをパスが通過できます。GRAPH FinGraph MATCH p = (:Person {id:1})-[]->{1,3}(:Account) RETURN TO_JSON(p) AS p;このクエリは、任意の方向(
-[:Owns]-)のOwnsタイプのエッジを使用して、Accountノード間の 1~3 ホップのパスを探します。パスは、どちらの方向でもエッジを通過でき、中間ノードは指定されていないため、2 ホップのパスは異なるタイプのノードを通過する可能性があります。例:(:Account)-[:Owns]-(:Person)-[:Owns]-(:Account)GRAPH FinGraph MATCH p = (:Account {id:1})-[:Owns]-{1,3}(:Account) RETURN TO_JSON(p) AS p;
両方の設計
次のクエリは、スキーマ化された設計とスキーマレス設計の両方で同程度のパフォーマンスを発揮します。定量化されたパス (:Account)-[:Transfer]->{1,3}(:Account) には、1 つのノードタイプ Account と 1 つのエッジタイプ Transfer が含まれます。パスには 1 つのノードタイプと 1 つのエッジタイプのみが含まれるため、両方の設計でパフォーマンスは同程度になります。中間ノードには明示的にラベルが付けられていませんが、パターンによって Account ノードに制約されます。Person ノードは、この定量化されたパスの外側にあります。
GRAPH FinGraph
MATCH p = (:Person {id:1})-[:Owns]->(:Account)-[:Transfer]->{1,3}(:Account)
RETURN TO_JSON(p) AS p;
Spanner Graph スキーマのパフォーマンスを最適化する
スキーマ化された Spanner Graph スキーマかスキーマレスの Spanner Graph スキーマのどちらかを選択したら、次の方法でパフォーマンスを最適化します。
エッジ トラバーサルを最適化する
エッジ トラバーサルは、特定のノードから始めて、接続されたエッジに沿って移動し、他のノードに到達することで、グラフ内を移動するプロセスです。スキーマはエッジの方向を定義します。エッジ トラバーサルは Spanner Graph の基本的なオペレーションであるため、エッジ トラバーサルの効率を高めることで、アプリケーションのパフォーマンスを大幅に向上させることができます。
エッジは次の 2 つの方向に走査できます。
- フォワード エッジ トラバーサル: ソースノードのアウトバウンド エッジをたどります。
- リバース エッジ トラバーサル: 宛先ノードの受信エッジに沿って移動します。
フォワード エッジ トラバーサルとリバース エッジ トラバーサルのクエリの例
次のクエリの例では、特定の人物の Owns エッジのフォワード エッジ トラバーサルを実行します。
GRAPH FinGraph
MATCH (person:Person {id: 1})-[owns:Owns]->(accnt:Account)
RETURN accnt.id;
次のクエリの例では、指定されたアカウントの Owns エッジのリバース エッジ トラバーサルを実行します。
GRAPH FinGraph
MATCH (accnt:Account {id: 1})<-[owns:Owns]-(person:Person)
RETURN person.name;
フォワード エッジ トラバーサルを最適化する
フォワード エッジ トラバーサルのパフォーマンスを向上させるには、ソースからエッジへのトラバーサルとエッジから宛先へのトラバーサルを最適化します。
ソースからエッジへのトラバーサルを最適化するには、
INTERLEAVE IN PARENT句を使用して、エッジ入力テーブルをソースノード入力テーブルにインターリーブします。インターリーブは、子テーブルの行と対応する親テーブルの行をストレージに配置する、Spanner のストレージ最適化手法です。インターリーブの詳細については、スキーマの概要をご覧ください。エッジから宛先へのトラバーサルを最適化するには、エッジと宛先
ノードの間に外部キー制約を作成します。これにより、エッジから宛先への制約が適用され、宛先テーブルのスキャンが不要になるため、パフォーマンスが向上します。適用された外部キーが書き込みパフォーマンスのボトルネックを引き起こす場合(ハブノードの更新時など)は、代わりに情報用の外部キーを使用します。
次の例は、適用された外部キー制約と情報外部キー制約でインターリーブを使用する方法を示しています。
適用される外部キー
このエッジテーブルの例では、PersonOwnAccount は次の処理を行います。
ソースノード テーブル
Personにインターリーブします。宛先ノードテーブル
Accountに適用される外部キーを作成します。
CREATE TABLE Person (
id INT64 NOT NULL,
name STRING(MAX),
) PRIMARY KEY (id);
CREATE TABLE Account (
id INT64 NOT NULL,
create_time TIMESTAMP,
close_time TIMESTAMP,
) PRIMARY KEY (id)
CREATE TABLE PersonOwnAccount (
id INT64 NOT NULL,
account_id INT64 NOT NULL,
create_time TIMESTAMP,
CONSTRAINT FK_Account FOREIGN KEY (account_id)
REFERENCES Account (id)
) PRIMARY KEY (id, account_id),
INTERLEAVE IN PARENT Person ON DELETE CASCADE;
情報用の外部キー
このエッジテーブルの例では、PersonOwnAccount は次の処理を行います。
ソースノード テーブル
Personにインターリーブします。宛先ノードテーブル
Accountに情報用の外部キーを作成します。
CREATE TABLE Person (
id INT64 NOT NULL,
name STRING(MAX),
) PRIMARY KEY (id);
CREATE TABLE Account (
id INT64 NOT NULL,
create_time TIMESTAMP,
close_time TIMESTAMP,
) PRIMARY KEY (id)
CREATE TABLE PersonOwnAccount (
id INT64 NOT NULL,
account_id INT64 NOT NULL,
create_time TIMESTAMP,
CONSTRAINT FK_Account FOREIGN KEY (account_id)
REFERENCES Account (id) NOT ENFORCED
) PRIMARY KEY (id, account_id),
INTERLEAVE IN PARENT Person ON DELETE CASCADE;
リバース エッジ トラバーサルを最適化する
リバース トラバーサルまたは双方向トラバーサルを含むクエリは一般的であるため、クエリでフォワード トラバーサルのみを使用する場合を除き、リバース エッジ トラバーサルを最適化します。
リバース エッジ トラバーサルを最適化するには、次の操作を行います。
エッジテーブルにセカンダリ インデックスを作成します。
インデックスを宛先ノード入力テーブルにインターリーブして、エッジを宛先ノードと共存させます。
エッジ プロパティをインデックスに保存します。
次の例は、エッジテーブル PersonOwnAccount のリバース エッジ トラバーサルを最適化するセカンダリ インデックスを示しています。
INTERLEAVE IN句は、インデックス データを宛先ノードテーブルAccountと同じ場所に配置します。STORING句は、エッジ プロパティをインデックスに格納します。
インターリーブ インデックスの詳細については、インデックスとインターリーブをご覧ください。
CREATE TABLE PersonOwnAccount (
id INT64 NOT NULL,
account_id INT64 NOT NULL,
create_time TIMESTAMP,
) PRIMARY KEY (id, account_id),
INTERLEAVE IN PARENT Person ON DELETE CASCADE;
CREATE INDEX AccountOwnedByPerson
ON PersonOwnAccount (account_id)
STORING (create_time),
INTERLEAVE IN Account;
セカンダリ インデックスを使用してプロパティをフィルタする
セカンダリ インデックスを使用すると、特定のプロパティ値に基づいてノードとエッジを効率的に検索できます。インデックスを使用すると、テーブル全体のスキャンを回避できます。これは、大規模なグラフで特に有効です。
プロパティでノードのフィルタリングを高速化する
次のクエリは、指定されたニックネームのアカウントを検索します。セカンダリ インデックスを使用しないため、一致する結果を見つけるには、すべての Account ノードをスキャンする必要があります。
GRAPH FinGraph
MATCH (acct:Account)
WHERE acct.nick_name = "abcd"
RETURN acct.id;
スキーマでフィルタされたプロパティにセカンダリ インデックスを作成して、フィルタリング プロセスを高速化します。
CREATE TABLE Account (
id INT64 NOT NULL,
create_time TIMESTAMP,
is_blocked BOOL,
nick_name STRING(MAX),
) PRIMARY KEY (id);
CREATE INDEX AccountByNickName
ON Account (nick_name);
プロパティでエッジのフィルタリングを高速化する
セカンダリ インデックスを使用すると、プロパティ値に基づくエッジのフィルタリングのパフォーマンスを向上させることができます。
フォワード エッジ トラバーサル
セカンダリ インデックスがない場合、このクエリは人物のすべてのエッジをスキャンして、create_time フィルタに一致するエッジを見つける必要があります。
GRAPH FinGraph
MATCH (person:Person)-[owns:Owns]->(acct:Account)
WHERE person.id = 1
AND owns.create_time >= PARSE_TIMESTAMP("%c", "Thu Dec 25 07:30:00 2008")
RETURN acct.id;
次のコードは、エッジのソースノード参照(id)とエッジ プロパティ(create_time)にセカンダリ インデックスを作成して、クエリの効率を高めます。また、クエリはインデックスをソースノード入力テーブルのインターリーブされた子として定義し、インデックスをソースノードと共存させます。
CREATE TABLE PersonOwnAccount (
id INT64 NOT NULL,
account_id INT64 NOT NULL,
create_time TIMESTAMP,
) PRIMARY KEY (id, account_id),
INTERLEAVE IN PARENT Person ON DELETE CASCADE;
CREATE INDEX PersonOwnAccountByCreateTime
ON PersonOwnAccount (id, create_time),
INTERLEAVE IN Person;
リバース エッジ トラバーサル
セカンダリ インデックスがない場合、次のリバース エッジ トラバーサル クエリは、指定された create_time の後に指定されたアカウントを所有する人物を見つけるために、すべてのエッジを読み取る必要があります。
GRAPH FinGraph
MATCH (acct:Account)<-[owns:Owns]-(person:Person)
WHERE acct.id = 1
AND owns.create_time >= PARSE_TIMESTAMP("%c", "Thu Dec 25 07:30:00 2008")
RETURN person.id;
次のコードは、エッジの宛先ノード参照(account_id)とエッジ プロパティ(create_time)にセカンダリ インデックスを作成して、クエリの効率を高めます。また、クエリはインデックスを宛先ノードテーブルのインターリーブされた子として定義し、インデックスを宛先ノードと共存させます。
CREATE TABLE PersonOwnAccount (
id INT64 NOT NULL,
account_id INT64 NOT NULL,
create_time TIMESTAMP,
) PRIMARY KEY (id, account_id),
INTERLEAVE IN PARENT Person ON DELETE CASCADE;
CREATE INDEX AccountOwnedByPersonByCreateTime
ON PersonOwnAccount (account_id, create_time),
INTERLEAVE IN Account;
ダングリング エッジを防止する
0 個または 1 個のノードを接続するエッジ(ダングリング エッジ)は、Spanner Graph クエリの効率とグラフ構造の完全性を損なう可能性があります。関連するエッジを削除せずにノードを削除すると、ダングリング エッジが発生する可能性があります。エッジを作成しても、そのソースノードまたは宛先ノードが存在しない場合には、ダングリング エッジが発生することがあります。ダングリング エッジを防ぐには、Spanner Graph スキーマに次のものを組み込みます。
- 参照制約を使用する。
- 省略可: エッジがまだ接続されているノードを削除する場合は、
ON DELETE CASCADE句を使用する。ON DELETE CASCADEを使用しない場合、対応するエッジを削除せずにノードを削除しようとすると失敗します。
参照制約を使用する
次の手順に沿って、両方のエンドポイントでインターリーブと外部キーの適用を使用すると、ダングリング エッジを防ぐことができます。
エッジ入力テーブルをソースノード入力テーブルにインターリーブして、エッジのソースノードが常に存在するようにします。
エッジに適用外部キー制約を作成して、エッジの宛先ノードが常に存在するようにします。外部キーを適用するとダングリング エッジを防ぐことができますが、エッジの挿入と削除のコストが高くなります。
次の例では、適用された外部キーと INTERLEAVE IN PARENT 句を使用して、エッジ入力テーブルをソースノード入力テーブルにインターリーブします。適用された外部キーとインターリーブを併用すると、フォワード エッジ トラバーサルを最適化することもできます。
CREATE TABLE PersonOwnAccount (
id INT64 NOT NULL,
account_id INT64 NOT NULL,
create_time TIMESTAMP,
CONSTRAINT FK_Account FOREIGN KEY (account_id) REFERENCES Account (id) ON DELETE CASCADE,
) PRIMARY KEY (id, account_id),
INTERLEAVE IN PARENT Person ON DELETE CASCADE;
ON DELETE CASCADE を使用してエッジを削除する
インターリーブまたは適用された外部キーを使用してダングリング エッジを防ぐ場合は、Spanner Graph スキーマで ON DELETE CASCADE 句を使用して、ノードを削除するトランザクションでノードの関連エッジを削除します。詳細については、インターリーブ テーブルのカスケードの削除と外部キーアクションをご覧ください。
異なるタイプのノードを接続するエッジのカスケードを削除する
次の例は、Spanner Graph スキーマで ON DELETE CASCADE を使用して、ソースノードまたは宛先ノードを削除するときにダングリング エッジを削除する方法を示しています。どちらの場合も、削除されたノードのタイプと、エッジで接続されているノードのタイプは異なります。
ソースノード
インターリーブを使用して、ソースノードが削除されたときにダングリング エッジを削除します。次の例は、インターリーブを使用して、ソースノード(Person)が削除されたときに出力エッジを削除する方法を示しています。詳細については、インターリーブされたテーブルを作成するをご覧ください。
CREATE TABLE PersonOwnAccount (
id INT64 NOT NULL,
account_id INT64 NOT NULL,
create_time TIMESTAMP,
CONSTRAINT FK_Account FOREIGN KEY (account_id) REFERENCES Account (id) ON DELETE CASCADE,
) PRIMARY KEY (id, account_id),
INTERLEAVE IN PARENT Person ON DELETE CASCADE
宛先ノード
外部キー制約を使用して、宛先ノードが削除されたときにダングリング エッジを削除します。次の例は、エッジテーブルで ON
DELETE CASCADE を使用して、宛先ノード(Account)が削除されたときに入力エッジを削除する方法を示しています。
CONSTRAINT FK_Account FOREIGN KEY(account_id)
REFERENCES Account(id) ON DELETE CASCADE
同じタイプのノードを接続するエッジのカスケードを削除する
エッジのソースノードと宛先ノードが同じタイプで、エッジがソースノードにインターリーブされている場合、ON DELETE CASCADE はソースノードまたは宛先ノードのいずれかに定義できますが、両方に定義することはできません。
このようなシナリオでダングリング エッジが発生しないようにするには、ソースノード入力テーブルにインターリーブせず、代わりに、ソースノードと宛先ノードの参照に 2 つの適用された外部キーを作成します。
次の例では、エッジ入力テーブルとして AccountTransferAccount を使用します。転送エッジの両方のエンドノードに 1 つずつ、合計 2 つの外部キーを定義します。どちらも ON DELETE CASCADE アクションを使用します。
CREATE TABLE AccountTransferAccount (
id INT64 NOT NULL,
to_id INT64 NOT NULL,
amount FLOAT64,
create_time TIMESTAMP NOT NULL,
order_number STRING(MAX),
CONSTRAINT FK_FromAccount FOREIGN KEY (id) REFERENCES Account (id) ON DELETE CASCADE,
CONSTRAINT FK_ToAccount FOREIGN KEY (to_id) REFERENCES Account (id) ON DELETE CASCADE,
) PRIMARY KEY (id, to_id);
ノードとエッジで有効期間(TTL)を構成する
TTL を使用すると、指定した期間後にデータを期限切れにして削除できます。スキーマで TTL を使用すると、有効期間や関連性が限られているデータを削除して、データベースのサイズとパフォーマンスを維持できます。たとえば、セッション情報、一時キャッシュ、イベントログを削除するように構成できます。
次の例では、TTL を使用して、閉鎖から 90 日後にアカウントを削除します。
CREATE TABLE Account (
id INT64 NOT NULL,
create_time TIMESTAMP,
close_time TIMESTAMP,
) PRIMARY KEY (id),
ROW DELETION POLICY (OLDER_THAN(close_time, INTERVAL 90 DAY));
ノードテーブルに TTL ポリシーを定義する場合は、関連するエッジの処理方法を構成して、意図しないダングリング エッジを防ぐ必要があります。
インターリーブされたエッジテーブルの場合: エッジテーブルがノードテーブルにインターリーブされている場合は、
ON DELETE CASCADEを使用してインターリーブの関係を定義できます。これにより、TTL がノードを削除すると、関連するインターリーブ エッジも削除されます。外部キーを含むエッジテーブルの場合: エッジテーブルが外部キーでノードテーブルを参照している場合は、次の 2 つのオプションがあります。
- 参照ノードが TTL によって削除されたときにエッジを自動的に削除するには、外部キーで
ON DELETE CASCADEを使用します。これにより、参照整合性が維持されます。 - 参照ノードが削除された後もエッジを残す(ダングリング エッジを作成する)には、外部キーを情報用の外部キーとして定義します。
- 参照ノードが TTL によって削除されたときにエッジを自動的に削除するには、外部キーで
次の例では、AccountTransferAccount エッジテーブルに 2 つのデータ削除ポリシーが適用されています。
- TTL ポリシーは、10 年以上前の転送レコードを削除します。
ON DELETE CASCADE句は、アカウントが削除されたときに、ソースに関連付けられているすべての転送レコードを削除します。
CREATE TABLE AccountTransferAccount (
id INT64 NOT NULL,
to_id INT64 NOT NULL,
amount FLOAT64,
create_time TIMESTAMP NOT NULL,
order_number STRING(MAX),
) PRIMARY KEY (id, to_id),
INTERLEAVE IN PARENT Account ON DELETE CASCADE,
ROW DELETION POLICY (OLDER_THAN(create_time, INTERVAL 3650 DAY));
ノード入力テーブルとエッジ入力テーブルを結合する
テーブルの列が別のテーブルとの関係を定義している場合は、1 つのテーブルでノードとその入出力エッジを定義できます。この方法には次のようなメリットがあります。
テーブル数の削減: スキーマ内のテーブル数を減らし、データ管理を簡素化します。
クエリ パフォーマンスの向上: 別のエッジテーブルへの結合を使用するトラバーサルを排除します。
詳細については、単一のテーブル内でノードとエッジを定義するをご覧ください。