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

このドキュメントでは、Spanner Graph スキーマを設計するためのベスト プラクティスを使用して、効率的なクエリを作成する方法について説明します。スキーマ設計は反復的である可能性があるため、最初に重要なクエリパターンを特定して、スキーマ設計の方針を確認することをおすすめします。

Spanner スキーマ設計のベスト プラクティスの一般的な情報については、スキーマ設計のベスト プラクティスをご覧ください。

エッジ トラバーサルを最適化する

エッジ トラバースは、特定のノードから始めて、接続されたエッジに沿って移動し、他のノードに到達することで、グラフ内を移動するプロセスです。エッジ トラバーサルは Spanner Graph の基本的なオペレーションであるため、エッジ トラバーサルの効率を高めることがアプリケーション パフォーマンスの鍵となります。

エッジは 2 つの方向に走査できます。送信元ノードから宛先ノードへの走査は「エッジの順方向走査」と呼ばれ、宛先ノードから送信元ノードへの走査は「エッジの逆方向走査」と呼ばれます。

インターリーブを使用してフォワード エッジ トラバーサルを最適化する

フォワードエッジ トラバーサルのパフォーマンスを向上させるには、エッジ入力テーブルをソースノード入力テーブルにインターリーブして、エッジをソースノードと共存させます。インターリーブは、子テーブルの行と対応する親テーブルの行をストレージに物理的に配置する、Spanner のストレージ最適化手法です。インターリーブの詳細については、スキーマの概要をご覧ください。

次の例は、これらのベスト プラクティスを示しています。

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

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 TABLE Person (
  id               INT64 NOT NULL,
  name             STRING(MAX),
) PRIMARY KEY (id);

CREATE TABLE Account (
  id               INT64 NOT NULL,
  create_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;

セカンダリ インデックスを使用してリバース エッジ トラバーサルを最適化する

たとえば、厳格なデータ整合性のためにエッジで外部キーを作成したくない場合は、次の例に示すように、エッジ入力テーブルにセカンダリ インデックスを直接作成できます。

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 Reverse_PersonOwnAccount
ON PersonOwnAccount (account_id);

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

ダングリング エッジとは、2 つ未満のノードを接続するエッジです。ダングリング エッジは、関連するエッジを削除せずにノードが削除された場合や、エッジをノードに適切にリンクせずにエッジが作成された場合に発生する可能性があります。

ダングリング エッジを禁止すると、次のメリットがあります。

  • グラフ構造の完全性を適用します。
  • エンドポイントが存在しないエッジをフィルタする余分な作業を回避することで、クエリのパフォーマンスが向上します。

参照制約を使用してダングリング エッジを禁止する

ダングリング エッジを禁止するには、両方のエンドポイントに制約を指定します。

  • エッジ入力テーブルをソースノード入力テーブルにインターリーブします。このアプローチにより、エッジのソースノードが常に存在します。
  • エッジに外部キー制約を作成して、エッジの宛先ノードが常に存在するようにします。

次の例では、インターリーブと外部キーを使用して参照整合性を適用します。

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 を使用して、ノードを削除するときにエッジを自動的に削除する

インターリーブまたは外部キーを使用してダングリング エッジを禁止する場合は、ON DELETE 句を使用して、エッジがまだ接続されているノードを削除する場合の動作を制御します。詳細については、インターリーブされたテーブルの削除カスケード外部キーを使用した削除カスケードをご覧ください。

ON DELETE は次のように使用できます。

  • ON DELETE NO ACTION(または ON DELETE 句を省略): エッジのあるノードの削除は失敗します。
  • ON DELETE CASCADE: ノードを削除すると、同じトランザクションで関連するエッジが自動的に削除されます。

異なるタイプのノードを接続するエッジのカスケードを削除する

  • ソースノードが削除されたときにエッジを削除します。たとえば、INTERLEAVE IN PARENT Person ON DELETE CASCADE は、削除される Person ノードから出力 PersonOwnAccount エッジをすべて削除します。詳細については、インターリーブ テーブルを作成するをご覧ください。

  • 宛先ノードが削除されたときにエッジを削除します。たとえば、CONSTRAINT FK_Account FOREIGN KEY(account_id) REFERENCES Account(id) ON DELETE CASCADE は、削除される Account ノードへのすべての受信 PersonOwnAccount エッジを削除します。詳細については、外部キーをご覧ください。

同じタイプのノードを接続するエッジのカスケードを削除する

エッジの送信元ノードと宛先ノードが同じタイプで、エッジが送信元ノードにインターリーブされている場合、ON DELETE CASCADE は送信元ノードまたは宛先ノードのいずれか(両方のノードではない)にのみ定義できます。

どちらの場合もダングリング エッジを自動的に削除するには、エッジ入力テーブルをソースノード入力テーブルにインターリーブするのではなく、エッジ ソースノード参照に外部キーを作成します。

順方向エッジの走査を最適化するには、インターリーブすることをおすすめします。続行する前に、ワークロードへの影響を必ず確認してください。エッジ入力テーブルとして AccountTransferAccount を使用する次の例をご覧ください。

--Define two Foreign Keys, each on one end Node of Transfer Edge, both with ON DELETE CASCADE action:
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);

セカンダリ インデックスを使用してノードまたはエッジのプロパティでフィルタする

セカンダリ インデックスは、クエリ処理を効率化するために不可欠です。グラフ構造全体を走査することなく、特定のプロパティ値に基づいてノードとエッジの迅速な検索をサポートします。これは、すべてのノードとエッジを走査すると非常に非効率になる可能性があるため、大規模なグラフを扱う場合に重要です。

プロパティでノードのフィルタリングを高速化する

ノード プロパティによるフィルタリングを高速化するには、プロパティにセカンダリ インデックスを作成します。たとえば、次のクエリは、特定のニックネームのアカウントを見つけます。セカンダリ インデックスがない場合、フィルタ条件に一致するようにすべての 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 AccountByEmail
ON Account (nick_name);

ヒント: スパース プロパティには NULL 値でインデックスをフィルタリングします。詳細については、NULL 値のインデックス登録を無効にするをご覧ください。

エッジ プロパティのフィルタリングでフォワード エッジ トラバーサルを高速化する

エッジのプロパティでフィルタリングしながらエッジを走査する場合は、エッジ プロパティにセカンダリ インデックスを作成し、インデックスをソースノードにインターリーブすることで、クエリを高速化できます。

たとえば、次のクエリは、特定のユーザーが所有するアカウントを特定の時間後に検索します。

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;

デフォルトでは、このクエリは指定された人物のすべてのエッジを読み取り、create_time の条件を満たすエッジをフィルタします。

次の例は、エッジ送信元ノード参照(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 PersonOwnAccountByCreateTime
    ON PersonOwnAccount (account_id, create_time);
    

    このアプローチでは、リバースエッジが account_idcreate_time で並べ替えられるため、クエリエンジンは create_time の条件を満たす account_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 PersonOwnAccountByCreateTime
    ON PersonOwnAccount (account_id) STORING (create_time);
    

    このアプローチでは複数のプロパティを保存できますが、クエリで宛先ノードのすべてのエッジを読み取り、エッジ プロパティでフィルタリングする必要があります。

これらのアプローチを組み合わせるには、次のガイドラインに沿って操作します。

  • パフォーマンスに影響するクエリで使用する場合、インデックス列でエッジ プロパティを使用します。
  • パフォーマンスに影響の少ないクエリで使用されるプロパティは、保存列に追加します。

ラベルとプロパティを使用してモデルのノードタイプとエッジタイプをモデル化する

ノードタイプとエッジタイプは通常、ラベルでモデル化されます。ただし、プロパティを使用して型をモデル化することもできます。BankAccountInvestmentAccountRetirementAccount など、さまざまな種類のアカウントがある例について考えてみましょう。アカウントを個別の入力テーブルに保存し、個別のラベルとしてモデル化することも、アカウントを 1 つの入力テーブルに保存し、プロパティを使用してタイプを区別することもできます。

ラベル付きのタイプをモデル化して、モデル化プロセスを開始します。次のシナリオでは、プロパティの使用を検討してください。

スキーマ管理を改善する

グラフにさまざまなノードタイプとエッジタイプが多数ある場合、それぞれに個別の入力テーブルを管理するのは困難です。スキーマ管理を容易にするため、タイプをプロパティとしてモデル化します。

頻繁に変更されるタイプを管理するためのプロパティ内のモデルタイプ

タイプをラベルとしてモデル化する場合は、タイプを追加または削除するためにスキーマを変更する必要があります。短時間に多数のスキーマ更新を実行した場合、Spanner はキューに格納されたスキーマ更新の処理をスロットリングする可能性があります。詳細については、スキーマ更新の頻度を制限するをご覧ください。

スキーマを頻繁に変更する必要がある場合は、スキーマの更新頻度に関する制限を回避するために、プロパティで型をモデル化することをおすすめします。

クエリを高速化する

プロパティを使用してタイプをモデリングすると、ノードまたはエッジ パターンが複数のラベルを参照する場合にクエリの速度が向上する可能性があります。次のサンプルクエリは、アカウントタイプがラベルでモデル化されていることを前提として、Person が所有する SavingsAccountInvestmentAccount のすべてのインスタンスを検索します。

GRAPH FinGraph
MATCH (:Person {id: 1})-[:Owns]->(acct:SavingsAccount|InvestmentAccount)
RETURN acct.id;

acct ノードパターンは 2 つのラベルを参照します。パフォーマンスが重要なクエリの場合は、プロパティを使用して Account をモデル化することを検討してください。次のクエリの例に示すように、このアプローチではクエリのパフォーマンスが向上する可能性があります。両方のクエリをベンチマークすることをおすすめします。

GRAPH FinGraph
MATCH (:Person {id: 1})-[:Owns]->(acct:Account)
WHERE acct.type IN ("Savings", "Investment")
RETURN acct.id;

クエリを高速化するために、ノード要素キーにタイプを保存する

ノードタイプがプロパティでモデル化され、ノード ライフタイム全体でタイプが変更されない場合に、ノードタイプでフィルタリングされたクエリを高速化するには、次の操作を行います。

  1. プロパティをノード要素キーの一部として含めます。
  2. エッジ入力テーブルにノードタイプを追加します。
  3. エッジ参照キーにノードタイプを含めます。

次の例では、この最適化を Account ノードと AccountTransferAccount エッジに適用します。

CREATE TABLE Account (
  type             STRING(MAX) NOT NULL,
  id               INT64 NOT NULL,
  create_time      TIMESTAMP,
) PRIMARY KEY (type, id);

CREATE TABLE AccountTransferAccount (
  type             STRING(MAX) NOT NULL,
  id               INT64 NOT NULL,
  to_type          STRING(MAX) NOT NULL,
  to_id            INT64 NOT NULL,
  amount           FLOAT64,
  create_time      TIMESTAMP NOT NULL,
  order_number     STRING(MAX),
) PRIMARY KEY (type, id, to_type, to_id),
  INTERLEAVE IN PARENT Account ON DELETE CASCADE;

CREATE PROPERTY GRAPH FinGraph
  NODE TABLES (
    Account
  )
  EDGE TABLES (
    AccountTransferAccount
      SOURCE KEY (type, id) REFERENCES Account
      DESTINATION KEY (to_type, to_id) REFERENCES Account
  );

ノードとエッジで TTL を構成する

Spanner の有効期間(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 があり、外部キーを介してエッジテーブルによって参照されている場合、外部キーは ON DELETE CASCADE で定義する必要があります。

次の例では、アカウントが有効な間は AccountTransferAccount が最長で 10 年間保存されます。アカウントを削除すると、移行履歴も削除されます。

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) があります。暗黙的なエッジ定義があります。idowner_id の場合、キー(id)を持つ Person ノードが、複合キー (owner_id, account_id) を持つ Account ノードを所有します。

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 ノードと PersonOwnAccount エッジを定義できます。すべての要素テーブル名が一意になるように、この例ではエッジテーブル定義にエイリアス 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
  );

次のステップ