Spanner Graph スキーマを設計する際のベスト プラクティス

このドキュメントでは、効率的なクエリ、最適化されたエッジ トラバーサル、効果的なデータ管理手法に焦点を当てて、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;

ダングリング エッジを防止する

ノードを 1 つも接続しないエッジ(ダングリング エッジ)は、Spanner Graph クエリの効率とグラフ構造の完全性を損なう可能性があります。ダングリング エッジは、関連するエッジを削除せずにノードを削除すると発生する可能性があります。エッジを作成しても、そのソースノードまたは宛先ノードが存在しない場合にも、ダングリング エッジが発生することがあります。ぶら下がりエッジを防ぐには、Spanner Graph スキーマに次のものを組み込みます。

参照制約を使用する

次の手順に沿って、両方のエンドポイントでインターリーブと強制外部キーを使用すると、ぶら下がりエッジを防ぐことができます。

  1. エッジ入力テーブルをソースノード入力テーブルにインターリーブして、エッジのソースノードが常に存在するようにします。

  2. エッジに適用外部キー制約を作成して、エッジの宛先ノードが常に存在するようにします。外部キーを適用するとダングリング エッジを防ぐことができますが、エッジの挿入と削除のコストが高くなります。

次の例では、適用された外部キーを使用し、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 を使用します。これにより、参照整合性が維持されます。
    • 参照ノードが削除された後もエッジを残す(ダングリング エッジを作成する)には、外部キーを情報用の外部キーとして定義します。

次の例では、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));

ノード入力テーブルとエッジ入力テーブルを結合する

スキーマを最適化するには、単一のテーブル内でノードとその入出力エッジを定義します。このアプローチには次のようなメリットがあります。

  • テーブル数の削減: スキーマ内のテーブル数を減らし、データ管理を簡素化します。

  • クエリ パフォーマンスの向上: 別のエッジテーブルへの結合を使用するトラバーサルを排除します。

この手法は、テーブルの主キーが別のテーブルとの関係も定義している場合に有効です。たとえば、Account テーブルに複合主キー (owner_id, account_id) がある場合、owner_id 部分は Person テーブルを参照する外部キーにできます。この構造により、Account テーブルは Account ノードと Person ノードからの入力エッジの両方を表すことができます。

  CREATE TABLE Person (
    id INT64 NOT NULL,
  ) PRIMARY KEY (id);

  -- Assume each account has exactly one owner.
  CREATE TABLE Account (
    owner_id INT64 NOT NULL,
    account_id INT64 NOT NULL,
  ) PRIMARY KEY (owner_id, account_id);

Account テーブルを使用して、Account ノードとその入力 Owns エッジの両方を定義できます。次の CREATE PROPERTY GRAPH ステートメントに示します。EDGE TABLES 句では、Account テーブルにエイリアス Owns を指定します。これは、グラフ スキーマ内の各要素に一意の名前を付ける必要があるためです。

  CREATE PROPERTY GRAPH FinGraph
    NODE TABLES (
      Person,
      Account
    )
    EDGE TABLES (
      Account AS Owns
        SOURCE KEY (owner_id) REFERENCES Person
        DESTINATION KEY (owner_id, account_id) REFERENCES Account
    );

次のステップ