Bonnes pratiques pour concevoir un schéma de graphique Spanner

Ce document explique comment créer des requêtes efficaces en utilisant les bonnes pratiques pour concevoir des schémas Spanner Graph. Vous pouvez itérer sur la conception de votre schéma. Nous vous recommandons donc d'identifier d'abord les modèles de requête critiques pour guider la conception de votre schéma.

Pour obtenir des informations générales sur les bonnes pratiques de conception de schémas Spanner, consultez Bonnes pratiques de conception de schémas.

Optimiser la traversée des arêtes

La traversée d'arêtes est le processus de navigation dans un graphique en suivant ses arêtes, en commençant par un nœud particulier et en se déplaçant le long des arêtes connectées pour atteindre d'autres nœuds. Le sens de l'arête est défini par le schéma. Le parcours des arêtes est une opération fondamentale dans Spanner Graph. Il est donc essentiel d'améliorer l'efficacité du parcours des arêtes pour les performances de votre application.

Vous pouvez parcourir une arête dans deux directions :

  • Parcours des arêtes sortantes : suit les arêtes sortantes du nœud source.

  • reverse edge traversal : suit les arêtes entrantes du nœud de destination.

Étant donné une personne, l'exemple de requête suivant effectue une traversée d'arêtes vers l'avant des arêtes Owns :

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

Pour un compte donné, l'exemple de requête suivant effectue une traversée d'arêtes inversée des arêtes Owns :

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

Optimiser la traversée de bord avant à l'aide de l'entrelacement

Pour améliorer les performances de traversée des arêtes en avant, entrelacez la table d'entrée des arêtes dans la table d'entrée des nœuds sources afin de colocaliser les arêtes avec les nœuds sources. L'entrelacement est une technique d'optimisation du stockage dans Spanner qui place physiquement les lignes de la table enfant avec leurs lignes parentes correspondantes dans le stockage. Pour en savoir plus sur l'entrelacement, consultez Présentation des schémas.

L'exemple suivant illustre ces bonnes pratiques :

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;

Optimiser le parcours des arêtes inversées à l'aide d'une clé étrangère

Pour parcourir efficacement les arêtes inversées, créez une contrainte de clé étrangère appliquée entre l'arête et le nœud de destination. Cette clé étrangère appliquée crée un index secondaire sur l'arête, indexé par les clés du nœud de destination. L'index secondaire est utilisé automatiquement lors de l'exécution de la requête.

L'exemple suivant illustre ces bonnes pratiques :

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;

Optimiser le parcours des arêtes inversées à l'aide d'un index secondaire

Si vous ne souhaitez pas créer de clé étrangère appliquée en périphérie, par exemple en raison de l'intégrité stricte des données qu'elle applique, vous pouvez créer directement un index secondaire sur la table d'entrée en périphérie, comme indiqué dans l'exemple suivant :

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 déclare une relation de localité des données entre l'index secondaire et la table dans laquelle il est entrelacé (Account dans l'exemple). Avec l'entrelacement, les lignes de l'index secondaire AccountOwnedByPerson sont colocalisées avec les lignes correspondantes de la table Account. Pour en savoir plus sur l'entrelacement, consultez Relations entre tables parent et enfant. Pour en savoir plus sur les index entrelacés, consultez Index et entrelacement.

Optimiser la traversée des arêtes à l'aide de clés étrangères informationnelles

Si votre scénario présente des goulots d'étranglement des performances d'écriture causés par des clés étrangères forcées, par exemple lorsque vous mettez fréquemment à jour des nœuds de hub comportant de nombreuses arêtes connectées, envisagez d'utiliser des clés étrangères informatives. L'utilisation de clés étrangères informatives sur les colonnes de référence d'une table Edge aide l'optimiseur de requêtes à supprimer les analyses de tables de nœuds redondantes. Toutefois, comme les clés étrangères informationnelles ne nécessitent pas d'index secondaires sur la table d'arêtes, elles n'améliorent pas la vitesse des recherches lorsqu'une requête tente de trouver des arêtes à l'aide de nœuds finaux. Pour en savoir plus, consultez Comparaison des types de clés étrangères.

Il est important de comprendre que si votre application ne peut pas garantir l'intégrité référentielle, l'utilisation de clés étrangères informationnelles pour l'optimisation des requêtes peut entraîner des résultats de requête incorrects.

L'exemple suivant crée une table avec une clé étrangère informationnelle sur la colonne 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;

Si l'entrelacement n'est pas une option, vous pouvez marquer les deux références de périphérie avec des clés étrangères informatives, comme dans l'exemple suivant :

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

Interdire les arêtes isolées

Une arête isolée est une arête qui relie moins de deux nœuds. Une arête orpheline peut se produire lorsqu'un nœud est supprimé sans que ses arêtes associées le soient également, ou lorsqu'une arête est créée sans être correctement associée à ses nœuds.

Le fait de ne pas autoriser les arêtes isolées présente les avantages suivants :

  • Applique l'intégrité de la structure du graphique.
  • Améliore les performances des requêtes en évitant le travail supplémentaire nécessaire pour filtrer les arêtes dont les points de terminaison n'existent pas.

Interdire les arêtes isolées à l'aide de contraintes référentielles

Pour interdire les arêtes isolées, spécifiez des contraintes sur les deux points de terminaison :

  • Entrelacez la table d'entrée des arêtes dans la table d'entrée des nœuds sources. Cette approche garantit que le nœud source d'un bord existe toujours.
  • Créez une contrainte de clé étrangère appliquée sur les arêtes pour vous assurer que le nœud de destination d'une arête existe toujours.

L'exemple suivant utilise l'entrelacement et une clé étrangère forcée pour appliquer l'intégrité référentielle :

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;

Utiliser ON DELETE CASCADE pour supprimer automatiquement les arêtes lors de la suppression d'un nœud

Lorsque vous utilisez l'entrelacement ou une clé étrangère forcée pour interdire les arêtes orphelines, utilisez la clause ON DELETE pour contrôler le comportement lorsque vous souhaitez supprimer un nœud avec des arêtes encore attachées. Pour en savoir plus, consultez Suppression en cascade pour les tables entrelacées et Actions de clé étrangère.

Vous pouvez utiliser ON DELETE de différentes manières :

  • ON DELETE NO ACTION (ou en omettant la clause ON DELETE) : la suppression d'un nœud avec des arêtes échouera.
  • ON DELETE CASCADE : la suppression d'un nœud supprime automatiquement les arêtes associées dans la même transaction.

Suppression en cascade pour les arêtes reliant différents types de nœuds

  • Supprimez les arêtes lorsque le nœud source est supprimé. Par exemple,INTERLEAVE IN PARENT Person ON DELETE CASCADE supprime tous les bords PersonOwnAccount sortants du nœud Person en cours de suppression. Pour en savoir plus, consultez Créer des tables entrelacées.

  • Supprimez les arêtes lorsque le nœud de destination est supprimé. Par exemple, CONSTRAINT FK_Account FOREIGN KEY(account_id) REFERENCES Account(id) ON DELETE CASCADE supprime tous les bords PersonOwnAccount entrants dans le nœud Account en cours de suppression.

Suppression en cascade pour les arêtes connectant le même type de nœuds

Lorsque les nœuds source et de destination d'un bord ont le même type et que le bord est imbriqué dans le nœud source, vous ne pouvez définir ON DELETE CASCADE que pour le nœud source ou le nœud de destination (mais pas les deux).

Pour supprimer les arêtes orphelines dans les deux cas, créez une clé étrangère appliquée sur la référence du nœud source de l'arête au lieu d'entrelacer la table d'entrée de l'arête dans la table d'entrée du nœud source.

Nous recommandons l'entrelacement pour optimiser la traversée des arêtes sortantes. Assurez-vous de vérifier l'impact sur vos charges de travail avant de continuer. Consultez l'exemple suivant, qui utilise AccountTransferAccount comme tableau d'entrée des arêtes :

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

Filtrer par propriétés de nœud ou d'arête avec des index secondaires

Les index secondaires sont essentiels pour un traitement efficace des requêtes. Ils permettent de rechercher rapidement des nœuds et des arêtes en fonction de valeurs de propriétés spécifiques, sans avoir à parcourir l'intégralité de la structure du graphique. C'est important lorsque vous travaillez avec de grands graphiques, car parcourir tous les nœuds et toutes les arêtes peut être très inefficace.

Accélérer le filtrage des nœuds par propriété

Pour accélérer le filtrage par propriétés de nœud, créez des index secondaires sur les propriétés. Par exemple, la requête suivante recherche des comptes pour un pseudo donné. Sans index secondaire, tous les nœuds Account sont analysés pour correspondre aux critères de filtrage.

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

Pour accélérer la requête, créez un index secondaire sur la propriété filtrée, comme indiqué dans l'exemple suivant :

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

Conseil : Utilisez des index filtrés sur NULL pour les propriétés creuses. Pour en savoir plus, consultez Désactiver l'indexation des valeurs NULL.

Accélérer la traversée de périphérie avant avec le filtrage sur les propriétés de périphérie

Lorsque vous parcourez un bord tout en filtrant ses propriétés, vous pouvez accélérer la requête en créant un index secondaire sur les propriétés du bord et en entrelant l'index dans le nœud source.

Par exemple, la requête suivante recherche les comptes appartenant à une personne donnée après une certaine heure :

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;

Par défaut, cette requête lit tous les nœuds de la personne spécifiée, puis filtre ceux qui répondent à la condition sur create_time.

L'exemple suivant vous montre comment améliorer l'efficacité des requêtes en créant un index secondaire sur la référence du nœud source d'arête (id) et la propriété d'arête (create_time). Entrelacez l'index sous la table d'entrée du nœud source pour colocaliser l'index avec le nœud source.

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;

Grâce à cette approche, la requête peut trouver efficacement tous les nœuds satisfaisant la condition sur create_time.

Accélérer la traversée des arêtes inversées en filtrant les propriétés des arêtes

Lorsque vous parcourez un bord inversé tout en filtrant ses propriétés, vous pouvez accélérer la requête en créant un index secondaire à l'aide du nœud de destination et des propriétés de bord pour le filtrage.

L'exemple de requête suivant effectue un parcours d'arêtes inversé avec filtrage sur les propriétés des arêtes :

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;

Pour accélérer cette requête à l'aide d'un index secondaire, utilisez l'une des options suivantes :

  • Créez un index secondaire sur la référence du nœud de destination Edge (account_id) et la propriété Edge (create_time), comme indiqué dans l'exemple suivant :

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

    Cette approche offre de meilleures performances, car les arêtes inversées sont triées par account_id et create_time, ce qui permet au moteur de requête de trouver efficacement les arêtes pour account_id qui répondent à la condition sur create_time. Toutefois, si différents modèles de requête filtrent différentes propriétés, chaque propriété peut nécessiter un index distinct, ce qui peut entraîner une surcharge.

  • Créez un index secondaire sur la référence du nœud de destination de périphérie (account_id) et stockez la propriété de périphérie (create_time) dans une colonne de stockage, comme indiqué dans l'exemple suivant :

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

    Cette approche permet de stocker plusieurs propriétés. Toutefois, la requête doit lire tous les nœuds de destination, puis filtrer les propriétés des arêtes.

Vous pouvez combiner ces approches en suivant ces consignes :

  • Utilisez les propriétés d'arête dans les colonnes d'index si elles sont utilisées dans des requêtes critiques pour les performances.
  • Pour les propriétés utilisées dans des requêtes moins sensibles aux performances, ajoutez-les dans les colonnes de stockage.

Modéliser les types de nœuds et d'arêtes avec des libellés et des propriétés

Les types de nœuds et d'arêtes sont généralement modélisés avec des libellés. Toutefois, vous pouvez également utiliser des propriétés pour modéliser des types. Prenons l'exemple de nombreux types de comptes différents, comme BankAccount, InvestmentAccount et RetirementAccount. Vous pouvez stocker les comptes dans des tables d'entrée distinctes et les modéliser en tant que libellés distincts, ou vous pouvez les stocker dans une seule table d'entrée et utiliser une propriété pour différencier les types.

Commencez le processus de modélisation en modélisant les types avec des libellés. Envisagez d'utiliser des propriétés dans les scénarios suivants.

Améliorer la gestion des schémas

Si votre graphique comporte de nombreux types de nœuds et d'arêtes différents, il peut devenir difficile de gérer une table d'entrée distincte pour chacun d'eux. Pour faciliter la gestion du schéma, modélisez le type en tant que propriété.

Modéliser les types dans une propriété pour gérer les types qui changent fréquemment

Lorsque vous modélisez des types en tant que libellés, l'ajout ou la suppression de types nécessite des modifications du schéma. Si vous effectuez trop de mises à jour de schéma sur une courte période, Spanner peut limiter le traitement des mises à jour de schéma en file d'attente. Pour en savoir plus, consultez Limiter la fréquence des mises à jour du schéma.

Si vous devez modifier fréquemment le schéma, nous vous recommandons de modéliser le type dans une propriété pour contourner les limites de fréquence des mises à jour du schéma.

Accélérer les requêtes

La modélisation de types avec des propriétés peut accélérer les requêtes lorsque le modèle de nœud ou d'arête fait référence à plusieurs libellés. L'exemple de requête suivant recherche toutes les instances de SavingsAccount et InvestmentAccount appartenant à un Person, en supposant que les types de compte sont modélisés avec des libellés :

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

Le modèle de nœud acct fait référence à deux libellés. Si cette requête est essentielle pour les performances, envisagez de modéliser Account à l'aide d'une propriété. Cette approche peut améliorer les performances des requêtes, comme le montre l'exemple de requête suivant. Nous vous recommandons de comparer les deux requêtes.

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

Stocker le type dans la clé de l'élément de nœud pour accélérer les requêtes

Pour accélérer les requêtes avec filtrage sur le type de nœud lorsqu'un type de nœud est modélisé avec une propriété et que le type ne change pas pendant la durée de vie du nœud, procédez comme suit :

  1. Incluez la propriété dans la clé de l'élément de nœud.
  2. Ajoutez le type de nœud dans le tableau d'entrée des arêtes.
  3. Incluez le type de nœud dans les clés de référence d'arête.

L'exemple suivant applique cette optimisation au nœud Account et à l'arête 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
  );

Configurer le TTL sur les nœuds et les périphéries

Dans Spanner, la valeur TTL (Time To Live) est un mécanisme qui permet l'expiration et la suppression automatiques des données après une période spécifiée. Cette option est souvent utilisée pour les données dont la durée de vie ou la pertinence sont limitées, comme les informations de session, les caches temporaires ou les journaux d'événements. Dans ce cas, la valeur TTL permet de maintenir la taille et les performances de la base de données.

L'exemple suivant utilise le TTL pour supprimer les comptes 90 jours après leur clôture :

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

Si la table de nœuds comporte une valeur TTL et une table d'arêtes entrelacée, l'entrelacement doit être défini avec ON DELETE CASCADE. De même, si la table de nœuds a une valeur TTL et est référencée par une table d'arêtes via une clé étrangère, la clé étrangère doit être définie avec ON DELETE CASCADE pour maintenir l'intégrité référentielle, ou définie comme clé étrangère informationnelle pour permettre l'existence d'arêtes orphelines.

Dans l'exemple suivant, AccountTransferAccount est stocké pendant 10 ans maximum tant qu'un compte reste actif. Lorsqu'un compte est supprimé, l'historique des transferts l'est également.

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

Fusionner les tables d'entrée de nœuds et d'arêtes

Vous pouvez utiliser la même table d'entrée pour définir plusieurs nœuds et arêtes dans votre schéma.

Dans les exemples de tableaux suivants, les nœuds Account ont une clé composite (owner_id, account_id). Il existe une définition de bord implicite. Le nœud Person avec la clé (id) possède le nœud Account avec la clé composite (owner_id, account_id) lorsque id est égal à 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);

Dans ce cas, vous pouvez utiliser la table d'entrée Account pour définir le nœud Account et l'arête PersonOwnAccount, comme illustré dans l'exemple de schéma suivant. Pour s'assurer que tous les noms de tables d'éléments sont uniques, l'exemple attribue l'alias Owns à la définition de la table d'arêtes.

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

Étapes suivantes