設計 Spanner 圖表結構定義的最佳做法

本文件說明如何運用設計 Spanner Graph 結構定義的最佳做法,建立有效率的查詢。您可以重複執行結構定義設計,因此建議您先找出重要的查詢模式,以便為結構定義設計提供指引。

如要進一步瞭解 Spanner 結構定義設計的最佳做法,請參閱「結構定義設計最佳做法」。

最佳化邊緣橫跨

邊緣遍歷是指透過沿著邊緣,從特定節點開始,沿著已連結的邊緣移動,以便前往其他節點的過程。邊緣的方向由結構定義決定。邊緣穿越是 Spanner 圖表中的基本作業,因此改善邊緣穿越效率是提升應用程式效能的重要關鍵。

您可以從兩個方向穿越邊緣:

  • 前向邊緣穿越:沿著來源節點的傳出邊緣。

  • 反向邊緣遍歷:沿著目的地節點的傳入邊緣。

以下列查詢為例,針對某個人執行 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;

使用交錯技術最佳化前向邊緣檢查

為了提升前向邊緣檢索效能,請將邊緣輸入資料表交錯插入來源節點輸入資料表,以便將邊緣與來源節點並置。交錯是 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 AccountOwnedByPerson
ON PersonOwnAccount (account_id), INTERLEAVE IN Account;

INTERLEAVE IN 會在次要索引和交錯的資料表 (在本例中為 Account) 之間宣告資料位置關係。在交錯模式下,AccountOwnedByPerson 次要索引的資料列會與 Account 資料表的對應資料列共置。如要進一步瞭解交錯,請參閱「父項子項資料表關係」。如要進一步瞭解交錯式索引,請參閱「索引和交錯」。

使用資訊外鍵最佳化邊緣橫跨

如果您的情境有因強制外鍵而導致的寫入效能瓶頸 (例如經常更新有許多連結邊的樞紐節點),建議您使用資訊外鍵。在邊緣資料表的參照欄上使用資訊外部關鍵字,有助於查詢最佳化工具捨棄多餘的節點資料表掃描。不過,由於資訊外鍵不需要邊緣資料表的次要索引,因此當查詢嘗試使用端點找出邊緣時,資訊外鍵不會提升查詢速度。詳情請參閱「外鍵類型比較」。

請務必瞭解,如果應用程式無法保證參照完整性,那麼使用資訊外鍵進行查詢最佳化可能會導致不正確的查詢結果。

以下範例會建立資料表,並在 account_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;

如果交錯不是選項,您可以使用資訊外鍵標示兩個邊緣參照,例如下例:

CREATE TABLE PersonOwnAccount (
  id               INT64 NOT NULL,
  account_id       INT64 NOT NULL,
  create_time      TIMESTAMP,
  CONSTRAINT FK_Person FOREIGN KEY (id)
    REFERENCES Person (id) NOT ENFORCED,
  CONSTRAINT FK_Account FOREIGN KEY (account_id)
    REFERENCES Account (id) NOT ENFORCED
) PRIMARY KEY (id, account_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) 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 會刪除所有傳入 PersonOwnAccount 邊緣至要刪除的 Account 節點。

刪除連結相同類型節點的邊緣連鎖

如果邊緣的來源節點和目的地節點具有相同類型,且邊緣與來源節點交錯,您只能為來源節點或目的地節點定義 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 AccountByNickName
ON Account (nick_name);

提示:請為稀疏屬性使用已篩除空值的索引。如需詳細資訊,請參閱「停用空值的索引」。

透過邊緣屬性篩選功能加快前向邊緣穿越速度

在沿著邊緣進行搜尋時,如果同時對其屬性進行篩選,您可以建立邊緣屬性的次要索引,並將索引交錯插入來源節點,藉此加快查詢速度。

舉例來說,以下查詢會在特定時間後找出特定使用者擁有的帳戶:

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 排序,讓查詢引擎能有效找出 account_id 的邊,符合 create_time 的條件。不過,如果不同的查詢模式篩選不同的屬性,則每個屬性都可能需要個別的索引,這可能會增加額外負擔。

  • 在邊緣目的地節點參照 (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。您可以將帳戶儲存在個別輸入表格中,並將其模擬為個別標籤,也可以將帳戶儲存在單一輸入表格中,並使用屬性來區分類型。

請先為標籤類型建立模型,然後開始建模程序。請考慮在下列情況下使用資源。

改善結構定義管理

如果圖表包含許多不同類型的節點和邊,管理每個類型的輸入表格可能會變得困難。為簡化結構定義管理作業,請將類型模擬為屬性。

在屬性中設定模型類型,以便管理經常變更的類型

將類型模擬為標籤時,新增或移除類型需要變更結構定義。如果您在短時間內執行太多結構定義更新,Spanner 可能會節流處理排隊的結構定義更新。詳情請參閱「限制結構定義更新頻率」。

如果需要經常變更結構定義,建議您在屬性中建立型別的模型,以便因應結構定義更新頻率的限制。

加快查詢速度

當節點或邊緣模式參照多個標籤時,使用屬性的建模類型可能會加快查詢速度。以下查詢範例會假設帳戶類型是以標籤建模,找出 Person 擁有的所有 SavingsAccountInvestmentAccount 例項:

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

acct 節點模式會參照兩個標籤。如果這是效能至關重要的查詢,請考慮使用屬性模擬 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
  );

在節點和邊上設定存留時間

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 會在帳戶處於有效狀態時儲存最多十年。刪除帳戶後,轉移記錄也會一併刪除。

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)。有一個隱含的邊緣定義,當 id 等於 owner_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
  );

後續步驟