Best practice per la progettazione di uno schema di grafo Spanner

Questo documento descrive come creare query efficienti utilizzando le best practice per la progettazione di schemi Spanner Graph. Puoi iterare la progettazione dello schema, quindi ti consigliamo di identificare prima i pattern di query critici per guidare la progettazione dello schema.

Per informazioni generali sulle best practice di progettazione dello schema di Spanner, consulta Best practice per la progettazione degli schemi.

Ottimizza l'attraversamento degli archi

L'attraversamento degli archi è il processo di navigazione in un grafico seguendo i suoi archi, partendo da un nodo specifico e spostandosi lungo gli archi collegati per raggiungere altri nodi. La direzione del bordo è definita dallo schema. L'attraversamento degli archi è un'operazione fondamentale in Spanner Graph, quindi migliorare l'efficienza dell'attraversamento degli archi è fondamentale per le prestazioni della tua applicazione.

Puoi attraversare un arco in due direzioni:

  • attraversamento degli archi in avanti: segue gli archi in uscita del nodo di origine.

  • reverse edge traversal: segue gli archi in entrata del nodo di destinazione.

Dato un utente, la seguente query di esempio esegue l'attraversamento degli archi in avanti degli archi Owns:

GRAPH FinGraph
MATCH (person:Person {id: 1})-[owns:Owns]->(accnt:Account)
RETURN accnt.id;

Dato un account, la seguente query di esempio esegue l'attraversamento degli archi inverso degli archi Owns:

GRAPH FinGraph
MATCH (accnt:Account {id: 1})<-[owns:Owns]-(person:Person)
RETURN person.name;

Ottimizzare l'attraversamento del bordo anteriore utilizzando l'interleaving

Per migliorare le prestazioni di attraversamento degli archi in avanti, interleava la tabella di input degli archi nella tabella di input dei nodi di origine per collocare gli archi con i nodi di origine. L'interleaving è una tecnica di ottimizzazione dell'archiviazione in Spanner che colloca fisicamente le righe della tabella secondaria con le righe principali corrispondenti nell'archiviazione. Per saperne di più sull'interleaving, consulta la panoramica degli schemi.

L'esempio seguente mostra queste best practice:

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;

Ottimizza l'attraversamento degli archi inversi utilizzando la chiave esterna

Per attraversare in modo efficiente gli archi inversi, crea un vincolo di chiave esterna forzata tra l'arco e il nodo di destinazione. Questa chiave esterna forzata crea un indice secondario sul lato con chiave in base alle chiavi del nodo di destinazione. L'indice secondario viene utilizzato automaticamente durante l'esecuzione della query.

L'esempio seguente mostra queste best practice:

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;

Ottimizza l'attraversamento degli archi inversi utilizzando l'indice secondario

Se non vuoi creare una chiave esterna forzata sul bordo, ad esempio a causa della rigida integrità dei dati che impone, puoi creare direttamente un indice secondario nella tabella di input del bordo, come mostrato nell'esempio seguente:

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 dichiara una relazione di località dei dati tra l'indice secondario e la tabella in cui è intercalato (Account nell'esempio). Con l'interleaving, le righe dell'indice secondario AccountOwnedByPerson si trovano insieme alle righe corrispondenti della tabella Account. Per saperne di più sull'interleaving, consulta Relazioni tra tabelle padre-figlio. Per ulteriori informazioni sugli indici intercalati, consulta la sezione Indici e intercalazione.

Ottimizzare l'attraversamento dei bordi utilizzando le chiavi esterne informative

Se il tuo scenario presenta colli di bottiglia delle prestazioni di scrittura causati da chiavi esterne forzate, ad esempio quando aggiorni frequentemente i nodi hub con molti archi connessi, valuta la possibilità di utilizzare chiavi esterne informative. L'utilizzo di chiavi esterne informative nelle colonne di riferimento di una tabella edge consente all'ottimizzatore delle query di eliminare le scansioni ridondanti delle tabelle dei nodi. Tuttavia, poiché le chiavi esterne informative non richiedono indici secondari nella tabella edge, non migliorano la velocità delle ricerche quando una query tenta di trovare gli edge utilizzando i nodi finali. Per saperne di più, consulta Confronto tra chiave esterna esterne.

È importante capire che se la tua applicazione non può garantire l'integrità referenziale, l'utilizzo di chiavi esterne informative per l'ottimizzazione delle query potrebbe portare a risultati errati.

L'esempio seguente crea una tabella con una chiave esterna informativa nella colonna 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;

Se l'interleaving non è un'opzione, puoi contrassegnare entrambi i riferimenti perimetrali con chiavi esterne informative, come nel seguente esempio:

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);

Non consentire bordi sospesi

Un arco pendente è un arco che collega meno di due nodi. Un arco pendente può verificarsi quando un nodo viene eliminato senza rimuovere gli archi associati oppure quando un arco viene creato senza collegarlo correttamente ai relativi nodi.

Il divieto di utilizzare bordi sospesi offre i seguenti vantaggi:

  • Applica l'integrità della struttura del grafico.
  • Migliora le prestazioni delle query evitando il lavoro aggiuntivo di filtrare i bordi in cui non esistono endpoint.

Non consentire i bordi sospesi utilizzando i vincoli referenziali

Per non consentire i bordi sospesi, specifica i vincoli su entrambi gli endpoint:

  • Interlaccia la tabella di input degli archi nella tabella di input dei nodi di origine. Questo approccio garantisce che il nodo di origine di un arco esista sempre.
  • Crea un vincolo di chiave esterna forzato sugli archi per assicurarti che il nodo di destinazione di un arco esista sempre.

L'esempio seguente utilizza l'interleaving e una chiave esterna forzata per applicare l'integrità referenziale:

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;

Utilizza ON DELETE CASCADE per rimuovere automaticamente gli archi quando elimini un nodo

Quando utilizzi l'interleaving o una chiave esterna forzata per impedire i bordi sospesi, utilizza la clausola ON DELETE per controllare il comportamento quando vuoi eliminare un nodo con bordi ancora collegati. Per ulteriori informazioni, consulta Eliminazione a cascata per le tabelle con interfoliazione e Azioni delle chiavi esterne.

Puoi utilizzare ON DELETE nei seguenti modi:

  • ON DELETE NO ACTION (o omettendo la clausola ON DELETE): l'eliminazione di un nodo con bordi non riuscirà.
  • ON DELETE CASCADE: L'eliminazione di un nodo rimuove automaticamente gli archi associati nella stessa transazione.

Eliminazione a cascata per i bordi che collegano diversi tipi di nodi

  • Elimina i bordi quando viene eliminato il nodo di origine. Ad esempio,INTERLEAVE IN PARENT Person ON DELETE CASCADE elimina tutti gli archi PersonOwnAccount in uscita dal nodo Person in fase di eliminazione. Per ulteriori informazioni, vedi Creare tabelle con interfoliazione.

  • Elimina i bordi quando viene eliminato il nodo di destinazione. Ad esempio, CONSTRAINT FK_Account FOREIGN KEY(account_id) REFERENCES Account(id) ON DELETE CASCADE elimina tutti gli archi PersonOwnAccount in entrata nel nodo Account in fase di eliminazione.

Eliminazione a cascata per i bordi che collegano lo stesso tipo di nodi

Quando i nodi di origine e di destinazione di un arco hanno lo stesso tipo e l'arco è intercalato nel nodo di origine, puoi definire ON DELETE CASCADE solo per il nodo di origine o di destinazione (ma non per entrambi).

Per rimuovere gli archi sospesi in entrambi i casi, crea una chiave esterna forzata sul riferimento al nodo di origine dell'arco anziché intercalare la tabella di input dell'arco nella tabella di input del nodo di origine.

Consigliamo l'interleaving per ottimizzare l'attraversamento del bordo in avanti. Assicurati di verificare l'impatto sui tuoi workload prima di procedere. Vedi l'esempio seguente, che utilizza AccountTransferAccount come tabella di input dei bordi:

--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);

Filtra per proprietà di nodi o archi con indici secondari

Gli indici secondari sono essenziali per l'elaborazione efficiente delle query. Supportano ricerche rapide di nodi e archi in base a valori di proprietà specifici, senza dover attraversare l'intera struttura del grafico. Questo è importante quando lavori con grafici di grandi dimensioni, perché attraversare tutti i nodi e gli archi può essere molto inefficiente.

Accelerare il filtraggio dei nodi per proprietà

Per velocizzare il filtraggio in base alle proprietà dei nodi, crea indici secondari sulle proprietà. Ad esempio, la seguente query trova gli account per un determinato nickname. Senza un indice secondario, tutti i nodi Account vengono analizzati per corrispondere ai criteri di filtro.

GRAPH FinGraph
MATCH (acct:Account)
WHERE acct.nick_name = "abcd"
RETURN acct.id;

Per velocizzare la query, crea un indice secondario sulla proprietà filtrata, come mostrato nell'esempio seguente:

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);

Suggerimento:utilizza gli indici filtrati NULL per le proprietà sparse. Per maggiori informazioni, vedi Disattivare l'indicizzazione dei valori NULL.

Accelerare l'attraversamento del bordo in avanti con il filtro delle proprietà del bordo

Quando attraversi un arco filtrando in base alle sue proprietà, puoi velocizzare la query creando un indice secondario sulle proprietà dell'arco e intercalando l'indice nel nodo di origine.

Ad esempio, la seguente query trova gli account di proprietà di una determinata persona dopo un determinato periodo di tempo:

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;

Per impostazione predefinita, questa query legge tutti i bordi della persona specificata e poi filtra i bordi che soddisfano la condizione su create_time.

L'esempio seguente mostra come migliorare l'efficienza delle query creando un indice secondario sul riferimento al nodo di origine del bordo (id) e sulla proprietà del bordo (create_time). Interleave l'indice nella tabella di input del nodo di origine per collocarlo insieme al nodo di origine.

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;

Utilizzando questo approccio, la query può trovare in modo efficiente tutti gli archi che soddisfano la condizione su create_time.

Accelerare l'attraversamento degli archi inversi con il filtro delle proprietà degli archi

Quando attraversi un arco inverso durante il filtraggio in base alle relative proprietà, puoi velocizzare la query creando un indice secondario utilizzando il nodo di destinazione e le proprietà dell'arco per il filtraggio.

La seguente query di esempio esegue l'attraversamento inverso degli archi con il filtro delle proprietà degli archi:

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;

Per velocizzare questa query utilizzando un indice secondario, utilizza una delle seguenti opzioni:

  • Crea un indice secondario sul riferimento al nodo di destinazione del bordo (account_id) e sulla proprietà del bordo (create_time), come mostrato nell'esempio seguente:

    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);
    

    Questo approccio offre prestazioni migliori perché gli archi inversi sono ordinati per account_id e create_time, il che consente al motore di query di trovare in modo efficiente gli archi per account_id che soddisfano la condizione su create_time. Tuttavia, se diversi pattern di query filtrano proprietà diverse, ogni proprietà potrebbe richiedere un indice separato, il che può aumentare il sovraccarico.

  • Crea un indice secondario sul riferimento al nodo di destinazione edge (account_id) e memorizza la proprietà edge (create_time) in una colonna di archiviazione, come mostrato nel seguente esempio:

    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);
    

    Questo approccio può memorizzare più proprietà; tuttavia, la query deve leggere tutti i bordi del nodo di destinazione e poi filtrare in base alle proprietà dei bordi.

Puoi combinare questi approcci seguendo queste linee guida:

  • Utilizza le proprietà edge nelle colonne dell'indice se vengono utilizzate in query fondamentali per il rendimento.
  • Per le proprietà utilizzate in query meno sensibili al rendimento, aggiungile alle colonne di archiviazione.

Modella i tipi di nodi e archi con etichette e proprietà

I tipi di nodi e archi vengono spesso modellati con etichette. Tuttavia, puoi anche utilizzare le proprietà per modellare i tipi. Considera un esempio in cui sono presenti molti tipi diversi di account, come BankAccount, InvestmentAccount e RetirementAccount. Puoi memorizzare gli account in tabelle di input separate e modellarli come etichette separate oppure puoi memorizzare gli account in un'unica tabella di input e utilizzare una proprietà per distinguere i tipi.

Inizia il processo di modellazione modellando i tipi con le etichette. Prendi in considerazione l'utilizzo delle proprietà nei seguenti scenari.

Migliorare la gestione degli schemi

Se il grafico ha molti tipi diversi di nodi e archi, la gestione di una tabella di input separata per ciascuno può diventare difficile. Per semplificare la gestione dello schema, modella il tipo come proprietà.

Tipi di modelli in una proprietà per gestire i tipi che cambiano di frequente

Quando modelli i tipi come etichette, l'aggiunta o la rimozione di tipi richiede modifiche allo schema. Se esegui troppi aggiornamenti dello schema in un breve periodo di tempo, Spanner potrebbe limitare l'elaborazione degli aggiornamenti dello schema in coda. Per ulteriori informazioni, vedi Limitare la frequenza degli aggiornamenti dello schema.

Se devi modificare spesso lo schema, ti consigliamo di modellare il tipo in una proprietà per aggirare i limiti alla frequenza degli aggiornamenti dello schema.

Velocizzare le query

La modellazione di tipi con proprietà può velocizzare le query quando il pattern di nodi o archi fa riferimento a più etichette. La seguente query di esempio trova tutte le istanze di SavingsAccount e InvestmentAccount di proprietà di un Person, supponendo che i tipi di account siano modellati con etichette:

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

Il pattern del nodo acct fa riferimento a due etichette. Se si tratta di una query critica per il rendimento, valuta la possibilità di modellare Account utilizzando una proprietà. Questo approccio potrebbe fornire prestazioni delle query migliori, come mostrato nel seguente esempio di query. Ti consigliamo di eseguire il benchmark di entrambe le query.

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

Memorizza il tipo di negozio nella chiave dell'elemento nodo per velocizzare le query

Per velocizzare le query con il filtro in base al tipo di nodo quando un tipo di nodo viene modellato con una proprietà e il tipo non cambia durante il ciclo di vita del nodo, segui questi passaggi:

  1. Includi la proprietà come parte della chiave dell'elemento nodo.
  2. Aggiungi il tipo di nodo nella tabella di input degli archi.
  3. Includi il tipo di nodo nelle chiavi di riferimento degli archi.

Il seguente esempio applica questa ottimizzazione al nodo Account e al nodo 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
  );

Configurare il TTL su nodi e bordi

La durata (TTL) di Spanner è un meccanismo che supporta la scadenza e la rimozione automatiche dei dati dopo un periodo di tempo specificato. Questo viene spesso utilizzato per i dati con una durata o pertinenza limitata, come informazioni sulla sessione, cache temporanee o log degli eventi. In questi casi, il TTL aiuta a mantenere le dimensioni e le prestazioni del database.

L'esempio seguente utilizza il TTL per eliminare gli account 90 giorni dopo la loro chiusura:

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));

Se la tabella dei nodi ha un TTL e una tabella degli archi intercalata, l'interleave deve essere definito con ON DELETE CASCADE. Allo stesso modo, se la tabella dei nodi ha un TTL e viene referenziata da una tabella degli archi tramite una chiave esterna, la chiave esterna deve essere definita con ON DELETE CASCADE per mantenere l'integrità referenziale oppure come chiave esterna informativa per consentire l'esistenza di un arco sospeso.

Nell'esempio seguente, AccountTransferAccount viene memorizzato per un massimo di dieci anni mentre un account rimane attivo. Quando un account viene eliminato, viene eliminata anche la cronologia dei trasferimenti.

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));

Unire le tabelle di input di nodi e archi

Puoi utilizzare la stessa tabella di input per definire più di un nodo e un arco nello schema.

Nelle tabelle di esempio seguenti, i nodi Account hanno una chiave composta (owner_id, account_id). Esiste una definizione di edge implicita: il nodo Person con chiave (id) è proprietario del nodo Account con chiave composita (owner_id, account_id) quando id è uguale a owner_id.

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);

In questo caso, puoi utilizzare la tabella di input Account per definire il nodo Account e il bordo PersonOwnAccount, come mostrato nell'esempio di schema seguente. Per garantire che tutti i nomi delle tabelle degli elementi siano univoci, l'esempio assegna alla definizione della tabella edge l'alias 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
  );

Passaggi successivi