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

本文說明設計 Spanner 圖表結構定義的最佳做法,重點在於有效率的查詢、最佳化的邊緣遍歷,以及有效率的資料管理技術。

如要瞭解 Spanner 結構定義 (而非 Spanner Graph 結構定義) 的設計,請參閱結構定義設計最佳做法

選擇結構定義設計

結構定義設計會影響圖表效能。下列主題有助於您選擇有效的策略。

已設定結構定義的設計與未設定結構定義的設計

  • 結構化設計會將圖形定義儲存在 Spanner 圖形結構定義中,適用於定義不常變動的穩定圖形。結構定義會強制執行圖表定義,而屬性支援所有 Spanner 資料類型。

  • 無結構定義設計會從資料推斷圖表定義,提供更多彈性,且不需要變更結構定義。系統預設不會強制執行動態標籤和屬性。 屬性必須是有效的 JSON 值。

下表摘要列出結構定義和無結構定義資料管理的主要差異。此外,請考量圖形查詢,決定要使用哪種結構定義。

功能 結構化資料管理 無結構定義資料管理
儲存圖表定義 圖表定義會儲存在 Spanner Graph 結構定義中。 圖表定義可從資料中看出。不過,Spanner Graph 不會檢查資料來推斷定義。
更新圖表定義 需要變更 Spanner 圖表結構定義。適合用於定義明確且不常變更的情況。 不需要變更 Spanner 圖表結構定義。
強制執行圖表定義 屬性圖表結構定義會強制執行邊緣的允許節點類型。 此外,也會強制執行圖形節點或邊緣類型允許的屬性和屬性類型。 預設不會強制執行。您可以使用 檢查限制 來強制執行標籤和屬性資料完整性。
資源資料類型 支援任何 Spanner 資料類型,例如 timestamp 動態屬性 必須是有效的 JSON 值。

根據圖表查詢選擇結構定義設計

有結構和無結構的設計通常效能相當。但如果查詢使用跨越多個節點或邊緣類型的量化路徑模式,無結構設計就能提供更出色的效能。這是因為無結構定義的設計會將所有資料儲存在單一節點和邊緣資料表中,盡量減少資料表掃描。相較之下,結構化設計會為每個節點和邊緣類型使用不同的資料表,因此跨越多個類型的查詢必須掃描並合併所有對應資料表的資料。

以下是適合無結構定義設計的查詢範例,以及適合兩種設計的查詢範例:

無結構定義設計

下列查詢使用量化路徑模式,可比對多種節點和邊緣,因此採用無結構定義設計時效能較佳:

  • 這項查詢的量化路徑模式使用多種邊緣類型 (TransferWithdraw),且未指定路徑長度超過一躍點的中間節點類型。

    GRAPH FinGraph
    MATCH p = (:Account {id:1})-[:Transfer|Withdraw]->{1,3}(:Account)
    RETURN TO_JSON(p) AS p;
    
  • 這項查詢的量化路徑模式會找出 PersonAccount 節點之間的一到三跳路徑,使用多個邊緣類型 (OwnsTransfers),且不指定較長路徑的中間節點類型。這樣一來,路徑就能遍歷各種型別的中間節點。例如:(:Person)-[:Owns]->(:Account)-[:Transfers]->(:Account)

    GRAPH FinGraph
    MATCH p = (:Person {id:1})-[:Owns|Transfers]->{1,3}(:Account)
    RETURN TO_JSON(p) AS p;
    
  • 這項查詢會使用任何方向 (-[:Owns]-) 的 Owns 類型邊緣,在 Account 節點之間尋找一到三跳的路徑。由於路徑可以雙向遍歷邊緣,且未指定中繼節點,因此兩跳路徑可能會經過不同類型的節點。例如:(: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) 包含一個節點類型 Account 和一個邊緣類型 Transfer。由於路徑只涉及一種節點類型和一種邊緣類型,因此兩種設計的效能相當。即使中繼節點未明確標示,模式仍會將其限制為 Account 節點。Person 節點會顯示在這個量化路徑之外。

GRAPH FinGraph
MATCH p = (:Person {id:1})-[:Owns]->(:Account)-[:Transfer]->{1,3}(:Account)
RETURN TO_JSON(p) AS p;

盡可能提高 Spanner 圖表結構定義成效

選擇使用結構化或無結構的 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;

最佳化前向邊緣遍歷

如要提升轉送邊緣遍歷效能,請從來源到邊緣,以及從邊緣到目的地的遍歷作業最佳化。

  • 如要最佳化來源到邊緣的遍歷,請使用 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;

避免懸空邊緣

連接零或一個節點的邊緣 (即懸空邊緣) 可能會影響 Spanner Graph 查詢效率和圖形結構完整性。如果刪除節點時未刪除相關聯的邊緣,可能會發生懸空邊緣。如果您建立邊緣,但來源或目的地節點不存在,也可能會發生懸空邊緣。如要避免懸空邊緣,請在 Spanner 圖表結構定義中加入下列項目:

使用參照完整性限制

您可以在兩個端點上使用交錯和強制執行的外部鍵,防止邊緣懸空,方法如下:

  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 圖表結構定義中使用 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,但不能同時為兩者定義。

為避免在這些情況下出現懸空邊緣,請勿交錯插入來源節點輸入資料表。請改為在來源和目的地節點參照上建立兩個強制執行的外鍵。

以下範例使用 AccountTransferAccount 做為邊緣輸入資料表。它會定義兩個外部索引鍵,分別位於轉移邊緣的兩個端點節點上,且都具有 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 刪除節點時,相關聯的交錯邊緣也會一併刪除。

  • 針對含有外鍵的邊緣資料表:如果邊緣資料表以外鍵參照節點資料表,您可以選擇以下兩種做法:

    • 如要讓系統在 TTL 刪除參照節點時自動刪除邊緣,請在外部鍵上使用 ON DELETE CASCADE。這樣就能維持參照完整性。
    • 如要允許在刪除參照節點後保留邊緣 (建立懸空邊緣),請將外鍵定義為資訊外鍵

在下列範例中,AccountTransferAccount 邊緣資料表適用於兩項資料刪除政策:

  • 存留時間政策會刪除超過十年的轉移記錄。
  • 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
    );

後續步驟