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

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

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

最佳化邊緣遍歷

邊緣遍歷是指透過圖形的邊緣進行導覽的程序,從特定節點開始,沿著相連的邊緣移動,抵達其他節點。邊緣的方向是由結構定義決定。邊緣遍歷是 Spanner Graph 的基本作業,因此提升邊緣遍歷效率是應用程式效能的關鍵。

您可以沿著邊緣在兩個方向上移動:

  • 正向邊緣遍歷:追蹤來源節點的外向邊緣。

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

以下列查詢為例,針對特定人員執行 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 會刪除所有連入要刪除的 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 AccountByNickName
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。您可以將帳戶儲存在不同的輸入資料表中,並將其建模為不同的標籤,也可以將帳戶儲存在單一輸入資料表中,並使用屬性來區分類型。

請先為附有標籤的型別建立模型,然後開始建模程序。建議您在下列情況下使用屬性。

改善結構定義管理

如果圖表有多種不同的節點和邊緣類型,管理每個節點和邊緣類型的個別輸入資料表可能會很困難。為簡化結構定義管理作業,請將型別設為屬性。

物業中的模型類型,可管理經常變更的類型

將型別當做標籤建立模型時,新增或移除型別需要變更結構定義。如果在短時間內執行過多結構定義更新,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
  );

後續步驟