Index Datastore

App Engine prédéfinit un index simple sur chaque propriété d'une entité. Une application App Engine peut définir d'autres index personnalisés dans un fichier de configuration d'index appelé index.yaml. Le serveur de développement ajoute automatiquement des suggestions à ce fichier lorsqu'il est confronté à des requêtes qui ne peuvent pas être exécutées avec les index existants. Vous pouvez définir des index manuellement en modifiant le fichier avant de transférer l'application.

Remarque : Le mécanisme de requête basé sur les index permet l'exécution d'un large éventail de requêtes et convient à la plupart des applications. Toutefois, il n'est pas compatible avec certains genres de requêtes couramment rencontrés dans d'autres technologies de base de données. Plus précisément, les requêtes de jointure et d'agrégation ne sont pas acceptées dans le moteur de requêtes en mode Datastore. Pour connaître les limites associées aux requêtes Datastore, consultez la page Requêtes Datastore.

Définition et structure des index

Un index est défini sur une liste de propriétés d'un genre d'entité donné, avec un ordre correspondant (croissant ou décroissant) pour chaque propriété. Pour une utilisation avec des requêtes ascendantes, l'index peut également inclure les ancêtres d'une entité.

Une table d'index contient une colonne pour chaque propriété nommée dans la définition de l'index. Chaque ligne de la table représente une entité dans Datastore qui constitue un résultat potentiel pour les requêtes basées sur l'index. Une entité n'est incluse dans l'index que si un ensemble de valeurs indexées est défini pour chaque propriété utilisée dans l'index. Si la définition de l'index fait référence à une propriété pour laquelle l'entité n'a pas de valeur, cette entité n'apparaîtra pas dans l'index et ne sera donc jamais renvoyée comme résultat pour une requête basée sur l'index.

Les lignes d'une table d'index sont d'abord triées par ancêtre, puis par valeur de propriété, dans l'ordre spécifié dans la définition d'index. L'index parfait pour une requête, permettant de l'exécuter le plus efficacement, est défini dans les propriétés ci-dessous. Dans l'ordre :

  1. Propriétés utilisées dans les filtres d'égalité
  2. Propriété utilisée dans un filtre d'inégalité (il ne peut y en avoir qu'une seul)
  3. Propriétés utilisées dans les ordres de tri

Cela garantit que tous les résultats pour chaque exécution possible de la requête apparaissent dans des lignes consécutives de la table. Datastore exécute une requête avec un index parfait en procédant comme suit :

  1. Il identifie l'index correspondant au genre de la requête, aux propriétés du filtre, aux opérateurs de filtre et aux ordres de tri.
  2. Il effectue une analyse en partant du début de l'index jusqu'à la première entité répondant à toutes les conditions de filtrage de la requête.
  3. Il continue l'analyse de l'index, en renvoyant tour à tour chaque entité, jusqu'à ce qu'il :
    • rencontre une entité ne répondant pas aux conditions de filtre ;
    • atteigne la fin de l'index ;
    • ait collecté le nombre maximal de résultats demandés par la requête.

Par exemple, examinez la requête suivante :

SELECT * FROM Person WHERE LastName = "Smith"
                       AND Height < 72
                  ORDER BY Height DESC

L'index parfait pour cette requête est une table de clés pour les entités de genre Person, avec des colonnes pour les valeurs des propriétés LastName et Height. L'index est d'abord trié par ordre croissant de LastName, puis par ordre décroissant de Height.

Pour générer ces index, configurez vos index comme suit :

indexes:
- kind: Person
  properties:
  - name: LastName
    direction: asc
  - name: Height
    direction: desc

Deux requêtes ayant la même forme mais des valeurs de filtrage différentes utilisent le même index. Par exemple, la requête suivante utilise le même index que ci-dessus :

SELECT * FROM Person WHERE LastName = "Jones"
                       AND Height < 63
                     ORDER BY Height DESC

Les deux requêtes suivantes utilisent également le même index, bien que leurs formes soient différentes :

SELECT * FROM Person WHERE LastName = "Friedkin"
                       AND FirstName = "Damian"
                     ORDER BY Height ASC

et

SELECT * FROM Person WHERE LastName = "Blair"
                  ORDER BY FirstName, Height ASC

Configuration des index

Par défaut, Datastore prédéfinit automatiquement un index pour chaque propriété de chaque genre d'entité. Les index intégrés suffisent à exécuter de nombreuses requêtes simples, telles que les requêtes comportant uniquement des égalités ou les requêtes à inégalité simples. Pour toutes les autres requêtes, l'application doit définir les index dont elle a besoin dans un fichier de configuration d'index nommé index.yaml. Si l'application essaie d'exécuter une requête ne pouvant pas être exécutée avec les index disponibles (prédéfinis ou spécifiés dans le fichier de configuration d'index), la requête échouera.

Datastore crée des index automatiques pour les requêtes qui se présentent sous les formes suivantes :

  • Requêtes sans genre n'utilisant que des filtres d'ancêtre et de clé
  • Requêtes n'utilisant que des filtres d'ancêtre et d'égalité
  • Requêtes n'utilisant que des filtres d'inégalité (limités à une seule propriété)
  • Requêtes n'utilisant que des filtres d'ancêtre, des filtres d'égalité sur les propriétés et des filtres d'inégalité sur les clés
  • Requêtes n'utilisant pas de filtre et un seul ordre de tri sur une propriété, croissant ou décroissant

Les autres formes de requêtes nécessitent que leurs index soient spécifiés dans le fichier de configuration d'index, en particulier :

  • Requêtes avec filtres d'ancêtre et d'inégalité
  • Requêtes avec un ou plusieurs filtres d'inégalité sur une propriété, et un ou plusieurs filtres d'égalité sur d'autres propriétés
  • Requêtes avec un ordre de tri décroissant sur les clés
  • Requêtes avec plusieurs ordres de tri

Index et propriétés

Voici quelques considérations spécifiques à prendre en compte au sujet des index et de leur relation avec les propriétés des entités dans Datastore :

Propriétés ayant des types de valeurs mixtes

Lorsque deux entités possèdent des propriétés portant le même nom, mais dont les types de valeurs sont différents, un index de la propriété les trie d'abord par type de valeur, puis dans un ordre secondaire approprié à chaque type. Par exemple, si deux entités possèdent chacune une propriété nommée age, l'une avec une valeur entière et l'autre avec une valeur de chaîne, l'entité avec la valeur entière précède toujours celle avec la valeur de chaîne lorsqu'elle est triée selon la propriété age, indépendamment des valeurs de propriété.

Cela vaut particulièrement pour les entiers et les nombres à virgule flottante, qui sont considérés comme des types distincts par Datastore. Comme les entiers sont triés avant les nombres à virgule flottante, cela signifie qu'une propriété avec la valeur entière 38 précède une propriété avec la valeur à virgule flottante 37.5.

Propriétés non indexées

Si vous savez que vous ne devrez jamais filtrer ou trier selon une propriété spécifique, vous pouvez indiquer à Datastore de ne pas conserver les entrées d'index de cette propriété en déclarant la propriété non indexée. Cette opération permet de réduire le coût d'exécution de votre application en diminuant le nombre d'écritures Datastore nécessaires. Une entité avec une propriété non indexée se comporte comme si la propriété n'était pas définie : les requêtes avec un filtre ou un ordre de tri sur la propriété non indexée ne correspondront jamais à cette entité.

Remarque : Si une propriété apparaît dans un index comportant plusieurs propriétés, le fait de la définir en tant que propriété non indexée l'empêchera d'être indexée dans l'index composite.

Par exemple, supposons qu'une entité possède des propriétés a et b et que vous souhaitez créer un index capable de satisfaire les requêtes telles que WHERE a ="bike" and b="red". Supposons également que vous ne vous souciez pas des requêtes WHERE a="bike" et WHERE b="red". Si vous définissez a comme propriété non indexée et créez un index pour a et b, Datastore ne crée pas d'entrées d'index pour les index a et b, et la requête WHERE a="bike" and b="red" ne fonctionnera pas. Pour que Datastore crée des entrées pour les index a et b, a et b doivent être indexés.

Vous déclarez une propriété non indexée en définissant noindex dans le tag du champ "struct" :

type Person struct {
	Name string
	Age  int `datastore:",noindex"`
}

Toutefois, notez que le fait d'indexer une propriété initialement non indexée n'a aucune incidence sur les entités éventuellement créées avant la modification. Les requêtes filtrant cette propriété ne renverront pas ces entités existantes, car elles n'ont pas été écrites dans l'index de la requête lors de leur création. Pour rendre ces entités accessibles aux requêtes ultérieures, vous devez les réécrire dans Datastore afin de les inclure dans les index appropriés. En d'autres termes, vous devez effectuer les opérations suivantes pour chacune de ces entités existantes :

  1. Récupérer (commande "get") l'entité dans Cloud Datastore.
  2. Écrire (commande "put") de nouveau l'entité dans Cloud Datastore.

De même, le fait de modifier une propriété initialement indexée en tant que propriété non indexée n'a une incidence que sur les entités écrites ultérieurement dans Cloud Datastore. Les entrées d'index pour toute entité existante avec cette propriété continueront d'exister jusqu'à ce que les entités soient mises à jour ou supprimées. Pour éviter des résultats indésirables, vous devez purger votre code de toutes les requêtes de filtrage ou de tri basées sur cette propriété (désormais non indexée).

Limites relatives aux index

Datastore impose des limites sur le nombre et la taille globale des entrées d'index pouvant être associées à une même entité. Ces limites sont élevées et la plupart des applications ne sont pas affectées. Toutefois, il existe des circonstances dans lesquelles vous pourriez y être confronté.

Comme indiqué ci-dessus, Cloud Datastore crée une entrée dans un index prédéfini pour chaque propriété de chaque entité, à l'exception des champs []byte et des propriétés que vous avez explicitement déclarées comme non indexées. La propriété peut également être incluse dans des index personnalisés supplémentaires déclarés dans votre fichier de configuration index.yaml. Si une entité ne possède pas de propriétés de liste, elle affichera au maximum une entrée dans chacun de ces index personnalisés (pour les index non ascendants) ou une entrée pour chacun des ancêtres de l'entité (pour les index ascendants). Chacune de ces entrées d'index doit être mise à jour chaque fois que la valeur de la propriété change.

Pour une propriété ayant une valeur unique pour chaque entité, chaque valeur possible doit être stockée une seule fois par entité dans l'index prédéfini de la propriété. Même dans ce cas, une entité ayant un grand nombre de propriétés dotées d'une seule valeur est susceptible de dépasser la limite d'entrées ou de taille de l'index. De même, une entité pouvant avoir plusieurs valeurs pour la même propriété nécessite une entrée d'index distincte pour chaque valeur. Là encore, si le nombre de valeurs possibles est important, une entité de ce type peut dépasser la limite d'entrées.

La situation s'aggrave dans le cas d'entités ayant plusieurs propriétés, chacune pouvant utiliser plusieurs valeurs. Pour accepter les entités de ce type, l'index doit inclure une entrée pour chaque combinaison possible de valeurs de propriété. Les index personnalisés faisant référence à plusieurs propriétés, chacune ayant plusieurs valeurs, peuvent générer une "explosion" combinatoire, car ils nécessitent un grand nombre d'entrées pour une entité n'ayant, au final, qu'un nombre relativement réduit de valeurs de propriétés possibles. Ces index exponentiels peuvent considérablement augmenter le coût lié à l'écriture d'une entité dans Datastore en raison du grand nombre d'entrées d'index à mettre à jour, et entraîner le dépassement de la limite d'entrées ou de taille de l'index de l'entité.

Considérons la requête suivante :

SELECT * FROM Widget WHERE X=1 AND Y=2 ORDER BY Date

Elle amène le SDK à suggérer l'index suivant :

indexes:
- kind: Widget
  properties:
  - name: X
  - name: Y
  - name: Date
Cet index nécessite un total de |X| * |Y| * |Date| entrées pour chaque entité (où |X| désigne le nombre de valeurs associées à l'entité pour la propriété X). Par exemple, le code suivant :
type Widget struct {
	X    []int
	Y    []string
	Date time.Time
}

func f(ctx context.Context) {
	e2 := &Widget{
		X:    []int{1, 2, 3, 4},
		Y:    []string{"red", "green", "blue"},
		Date: time.Now(),
	}

	k := datastore.NewIncompleteKey(ctx, "Widget", nil)
	if _, err := datastore.Put(ctx, k, e2); err != nil {
		// Handle error.
	}
}

crée une entité avec quatre valeurs pour la propriété x, trois valeurs pour la propriété y et date définie à la date du jour. Douze entrées d'index sont requises, une pour chaque combinaison possible des valeurs des propriétés :

(1, "red", <now>) (1, "green", <now>) (1, "blue", <now>)

(2, "red", <now>) (2, "green", <now>) (2, "blue", <now>)

(3, "red", <now>) (3, "green", <now>) (3, "blue", <now>)

(4, "red", <now>) (4, "green", <now>) (4, "blue", <now>)

Lorsque la même propriété est répétée plusieurs fois, Datastore peut détecter les index exponentiels et suggérer un autre index. Toutefois, dans tous les autres cas (comme la requête définie dans cet exemple), Datastore génère un index exponentiel. Dans ce cas, vous pouvez contourner l'index exponentiel en configurant manuellement un index dans le fichier de configuration d'index :

indexes:
- kind: Widget
  properties:
  - name: X
  - name: Date
- kind: Widget
  properties:
  - name: Y
  - name: Date
Cela permet de réduire le nombre d'entrées nécessaires à seulement (|X| * |Date| + |Y| * |Date|), soit 7 entrées au lieu de 12 :

(1, <now>) (2, <now>) (3, <now>) (4, <now>)

("red", <now>) ("green", <now>) ("blue", <now>)

Toute opération de type "put" susceptible de faire dépasser la limite d'entrées ou de taille de l'index échoue et génère une erreur. Le texte de l'erreur indique quelle limite a été dépassée ("Too many indexed properties" ou "Index entries too large") et quel index personnalisé est à l'origine de l'incident. Si vous créez un index dépassant les limites de n'importe quelle entité lors de sa création, les requêtes sur l'index échouent et l'index présente un état Error dans la console Google Cloud. Pour résoudre le problème d'un index affichant l'état Error, procédez comme suit :

  1. Supprimez l'index affichant l'état Error de votre fichier index.yaml.

  2. Exécutez la commande suivante à partir du répertoire contenant votre fichier index.yaml pour supprimer cet index de Datastore :

    gcloud datastore indexes cleanup index.yaml
    
  3. Résolvez la cause de l'erreur. Exemple :

    • Reformulez la définition de l'index et les requêtes correspondantes.
    • Supprimez les entités à l'origine de l'index exponentiel.
  4. Ajoutez à nouveau l'index à votre fichier index.yaml.

  5. Exécutez la commande suivante à partir du répertoire contenant votre fichier index.yaml pour créer l'index dans Datastore :

    gcloud datastore indexes create index.yaml
    

En utilisant une propriété de liste, vous pouvez éviter les requêtes qui nécessitent un index personnalisé, et ainsi éviter les index exponentiels. Comme décrit ci-dessus, cette opération inclut les requêtes avec plusieurs ordres de tri ou les requêtes combinant des filtres d'égalité et d'inégalité.