Indici Datastore

App Engine predefinisce un indice semplice su ciascuna proprietà di un'entità. Un'applicazione App Engine può definire ulteriori indici personalizzati in un file di configurazione degli indici denominato datastore-indexes.xml, che viene generato nella directory /war/WEB-INF/appengine-generated dell'applicazione. Il server di sviluppo aggiunge automaticamente suggerimenti a questo file quando rileva query che non possono essere eseguite con gli indici esistenti. Puoi ottimizzare gli indici manualmente modificando il file prima di caricare l'applicazione.

Nota: il meccanismo di query basato su indice supporta un'ampia gamma di query ed è adatto alla maggior parte delle applicazioni. Tuttavia, non supporta alcuni tipi di query comuni in altre tecnologie di database: in particolare, i join e le query aggregate non sono supportati all'interno di Datastore Query Engine. Consulta la pagina Query Datastore per conoscere le limitazioni relative alle query di Datastore.

Definizione e struttura dell'indice

Un indice viene definito su un elenco di proprietà di un determinato tipo di entità, con un ordine corrispondente (ordine crescente o decrescente) per ogni proprietà. Per l'utilizzo con le query dei predecessori, l'indice può anche includere facoltativamente i predecessori di un'entità.

Una tabella di indice contiene una colonna per ogni proprietà denominata nella definizione dell'indice. Ogni riga della tabella rappresenta un'entità nel datastore che è un potenziale risultato per le query basate sull'indice. Un'entità è inclusa nell'indice solo se ha un valore indicizzato impostato per ogni proprietà utilizzata nell'indice; se la definizione dell'indice fa riferimento a una proprietà per la quale l'entità non ha valore, l'entità non verrà visualizzata nell'indice e quindi non verrà mai restituita come risultato per nessuna query basata sull'indice.

Nota: Datastore distingue tra un'entità che non possiede una proprietà e un'entità che possiede la proprietà con un valore null (null). Se assegni esplicitamente un valore null alla proprietà di un'entità, tale entità potrebbe essere inclusa nei risultati di una query che fa riferimento a quella proprietà.

Nota: gli indici composti da più proprietà richiedono che ogni singola proprietà non debba essere impostata su non indicizzata.

Le righe di una tabella di indice vengono ordinate prima per predecessore e poi per valori delle proprietà, nell'ordine specificato nella definizione dell'indice. L'indice perfetto per una query, che consente di eseguirla nel modo più efficiente, viene definito nelle seguenti proprietà, nell'ordine:

  1. Proprietà utilizzate nei filtri di uguaglianza
  2. Proprietà utilizzata in un filtro di disuguaglianza (non più di uno)
  3. Proprietà utilizzate negli ordini di ordinamento

In questo modo, tutti i risultati di ogni possibile esecuzione della query verranno visualizzati in righe consecutive della tabella. Datastore esegue una query utilizzando un indice perfetto:

  1. Identifica l'indice corrispondente al tipo, alle proprietà di filtro, agli operatori di filtro e agli ordini di ordinamento della query.
  2. Esegue la scansione dall'inizio dell'indice alla prima entità che soddisfa tutte le condizioni di filtro della query.
  3. Continua la scansione dell'indice, restituendo ogni entità alla volta finché non viene
    • rileva un'entità che non soddisfa le condizioni di filtro oppure
    • raggiunge la fine dell'indice o
    • ha raccolto il numero massimo di risultati richiesti dalla query.

Ad esempio, considera la query seguente:

Query q1 =
    new Query("Person")
        .setFilter(
            CompositeFilterOperator.and(
                new FilterPredicate("lastName", FilterOperator.EQUAL, "Smith"),
                new FilterPredicate("height", FilterOperator.EQUAL, 72)))
        .addSort("height", Query.SortDirection.DESCENDING);

L'indice perfetto per questa query è una tabella di chiavi per le entità di tipo Person, con colonne per i valori delle proprietà lastName e height. L'indice viene ordinato prima in ordine crescente per lastName e poi in ordine decrescente in base a height.

Per generare questi indici, configurali nel seguente modo:

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes autoGenerate="false">
  <datastore-index kind="Person" ancestor="false" source="manual">
    <property name="lastName" direction="asc"/>
    <property name="height" direction="desc"/>
  </datastore-index>
</datastore-indexes>

Due query dello stesso formato, ma con valori di filtro diversi, utilizzano lo stesso indice. Ad esempio, la query seguente utilizza lo stesso indice di quello riportato sopra:

Query q2 =
    new Query("Person")
        .setFilter(
            CompositeFilterOperator.and(
                new FilterPredicate("lastName", FilterOperator.EQUAL, "Jones"),
                new FilterPredicate("height", FilterOperator.EQUAL, 63)))
        .addSort("height", Query.SortDirection.DESCENDING);

Anche le seguenti due query utilizzano lo stesso indice, nonostante le diverse forme:

Query q3 =
    new Query("Person")
        .setFilter(
            CompositeFilterOperator.and(
                new FilterPredicate("lastName", FilterOperator.EQUAL, "Friedkin"),
                new FilterPredicate("firstName", FilterOperator.EQUAL, "Damian")))
        .addSort("height", Query.SortDirection.ASCENDING);

e

Query q4 =
    new Query("Person")
        .setFilter(new FilterPredicate("lastName", FilterOperator.EQUAL, "Blair"))
        .addSort("firstName", Query.SortDirection.ASCENDING)
        .addSort("height", Query.SortDirection.ASCENDING);

Configurazione degli indici

Per impostazione predefinita, Datastore predefinisce automaticamente un indice per ogni proprietà di ogni tipo di entità. Questi indici predefiniti sono sufficienti per eseguire molte query semplici, ad esempio query di sola uguaglianza e semplici query di disuguaglianza. Per tutte le altre query, l'applicazione deve definire gli indici di cui ha bisogno in un file di configurazione indice denominato datastore-indexes.xml. Se l'applicazione tenta di eseguire una query che non può essere eseguita con gli indici disponibili (predefiniti o specificati nel file di configurazione degli indici), la query avrà esito negativo con DatastoreNeedIndexException.

Datastore crea indici automatici per query nei seguenti formati:

  • Query non ordinate utilizzando solo filtri dei predecessori e chiave
  • Query che utilizzano solo filtri di predecessore e di uguaglianza
  • Query che utilizzano solo filtri di disuguaglianza (limitati a una singola proprietà)
  • Query che usano solo filtri dei predecessori, filtri di uguaglianza sulle proprietà e filtri di disuguaglianza sulle chiavi
  • Query senza filtri e con un solo ordinamento per una proprietà, ordine crescente o decrescente

Altre forme di query richiedono la specifica dei relativi indici nel file di configurazione degli indici, tra cui:

  • Query con filtri di predecessore e di disuguaglianza
  • Query con uno o più filtri di disuguaglianza su una proprietà e uno o più filtri di uguaglianza su altre proprietà
  • Query con ordinamento dei tasti in ordine decrescente
  • Query con più ordini

Indici e proprietà

Di seguito sono riportate alcune considerazioni speciali da tenere presenti sugli indici e sulla loro relazione con le proprietà delle entità in Datastore:

Proprietà con tipi di valori misti

Quando due entità hanno proprietà con lo stesso nome ma tipi di valore diversi, un indice della proprietà ordina le entità prima in base al tipo di valore e poi in base a un ordinamento secondario appropriato per ogni tipo. Ad esempio, se due entità hanno ciascuna una proprietà denominata age, una con un valore intero e una con un valore stringa, l'entità con valore intero precede sempre quella con il valore stringa se ordinata in base alla proprietà age, indipendentemente dai valori stessi della proprietà.

Ciò vale in particolare nel caso di numeri interi e in virgola mobile, che vengono trattati come tipi separati da Datastore. Poiché tutti i numeri interi vengono ordinati prima di tutti i valori in virgola mobile, una proprietà con valore intero 38 viene ordinata prima di quella con valore in virgola mobile 37.5.

Proprietà non indicizzate

Se sai che non dovrai mai filtrare o ordinare i dati su una determinata proprietà, puoi indicare a Datastore di non mantenere le voci di indice per quella proprietà dichiarando la proprietà non indicizzata. In questo modo si riducono i costi di esecuzione dell'applicazione diminuendo il numero di scritture Datastore che deve eseguire. Un'entità con una proprietà non indicizzata si comporta come se la proprietà non fosse impostata: le query con un filtro o un ordinamento nella proprietà non indicizzata non corrisponderanno mai a questa entità.

Nota: se una proprietà viene visualizzata in un indice composto da più proprietà e la sua impostazione su Non indicizzato ne impedirà l'indicizzazione nell'indice composto.

Ad esempio, supponiamo che un'entità abbia le proprietà a e b e che voglia creare un indice in grado di soddisfare query come WHERE a ="bike" and b="red". Supponiamo inoltre che le query WHERE a="bike" e WHERE b="red" non ti interessino. Se imposti a su Non indicizzato e crei un indice per a e b, il datastore non creerà voci di indice per gli indici a e b e, di conseguenza, la query WHERE a="bike" and b="red" non funzionerà. Affinché Datastore possa creare voci per gli indici a e b, sia a che b devono essere indicizzati.

Nell'API Java Datastore di basso livello, le proprietà vengono definite come indicizzate o non indicizzate a seconda dell'entità, a seconda del metodo utilizzato per impostarle (setProperty() o setUnindexedProperty()):

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

Key acmeKey = KeyFactory.createKey("Company", "Acme");

Entity tom = new Entity("Person", "Tom", acmeKey);
tom.setProperty("name", "Tom");
tom.setProperty("age", 32);
datastore.put(tom);

Entity lucy = new Entity("Person", "Lucy", acmeKey);
lucy.setProperty("name", "Lucy");
lucy.setUnindexedProperty("age", 29);
datastore.put(lucy);

Filter ageFilter = new FilterPredicate("age", FilterOperator.GREATER_THAN, 25);

Query q = new Query("Person").setAncestor(acmeKey).setFilter(ageFilter);

// Returns tom but not lucy, because her age is unindexed
List<Entity> results = datastore.prepare(q).asList(FetchOptions.Builder.withDefaults());

Puoi modificare una proprietà indicizzata in non indicizzata reimpostando il suo valore con setUnindexedProperty() oppure da non indicizzata a indicizzata reimpostandola con setProperty().

Tuttavia, tieni presente che la modifica di una proprietà da non indicizzata a indicizzata non influisce sulle entità esistenti che potrebbero essere state create prima della modifica. Le query che filtrano nella proprietà non restituiranno queste entità esistenti, perché queste entità non sono state scritte nell'indice della query quando sono state create. Per rendere le entità accessibili dalle query future, devi riscriverle in Datastore in modo che vengano inserite negli indici appropriati. Ciò significa che devi eseguire le seguenti operazioni per ciascuna entità esistente:

  1. Recupera (get) l'entità da Datastore.
  2. Scrivi (put) di nuovo l'entità in Datastore.

Analogamente, la modifica di una proprietà da indicizzata a non indicizzata interessa solo le entità scritte successivamente in Datastore. Le voci di indice per le entità esistenti con quella proprietà continueranno a esistere fino a quando le entità non verranno aggiornate o eliminate. Per evitare risultati indesiderati, devi eliminare definitivamente il codice di tutte le query che filtrano o ordinano in base alla proprietà (ora non indicizzata).

Limiti dell'indice

Datastore impone dei limiti al numero e alla dimensione complessiva delle voci di indice che possono essere associate a una singola entità. Questi limiti sono grandi e la maggior parte delle applicazioni non sono interessate. Tuttavia, in alcune circostanze potresti imbatterti in questi limiti.

Come descritto sopra, Datastore crea una voce in un indice predefinito per ogni proprietà di ogni entità, tranne le stringhe di testo lunghe (Text), le stringhe di byte lunghi (Blob) e le entità incorporate (EmbeddedEntity) e quelle che hai dichiarato esplicitamente come non indicizzato. La proprietà può anche essere inclusa in indici aggiuntivi personalizzati dichiarati nel file di configurazione di datastore-indexes.xml. Purché un'entità non abbia proprietà elenco, avrà al massimo una voce in ogni indice personalizzato (per gli indici non predecessori) o una per ciascuno dei predecessori dell'entità (per gli indici predecessori). Ognuna di queste voci di indice deve essere aggiornata ogni volta che il valore della proprietà cambia.

Per una proprietà con un singolo valore per ogni entità, ogni possibile valore deve essere archiviato una sola volta per entità nell'indice predefinito della proprietà. Anche in questo caso, è possibile che un'entità con un numero elevato di proprietà a valore singolo superi il limite di voci o dimensioni dell'indice. Analogamente, un'entità che può avere più valori per la stessa proprietà richiede una voce di indice distinta per ogni valore. Anche in questo caso, se il numero di valori possibili è elevato, tale entità può superare il limite di voci.

La situazione peggiora nel caso di entità con più proprietà, ognuna delle quali può assumere più valori. Per ospitare un'entità di questo tipo, l'indice deve includere una voce per ogni possibile combinazione di valori della proprietà. Gli indici personalizzati che fanno riferimento a più proprietà, ciascuna con più valori, possono "esplodere" in modo combinato, richiedendo un numero elevato di voci per un'entità con un numero relativamente ridotto di valori di proprietà possibili. Una tale esplosione di indici può aumentare notevolmente il costo della scrittura di un'entità in Datastore, a causa dell'elevato numero di voci di indice che devono essere aggiornate e può anche causare facilmente il superamento del limite di voci di indice o dimensioni da parte dell'entità.

Considera la query

Query q =
    new Query("Widget")
        .setFilter(
            CompositeFilterOperator.and(
                new FilterPredicate("x", FilterOperator.EQUAL, 1),
                new FilterPredicate("y", FilterOperator.EQUAL, 2)))
        .addSort("date", Query.SortDirection.ASCENDING);

per cui l'SDK suggerirà il seguente indice:

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes autoGenerate="false">
  <datastore-index kind="Widget" ancestor="false" source="manual">
    <property name="x" direction="asc"/>
    <property name="y" direction="asc"/>
    <property name="date" direction="asc"/>
  </datastore-index>
</datastore-indexes>
Questo indice richiederà un totale di |x| * |y| * voci |date| per ogni entità (dove |x| indica il numero di valori associati all'entità per la proprietà x). Ad esempio, il seguente codice
Entity widget = new Entity("Widget");
widget.setProperty("x", Arrays.asList(1, 2, 3, 4));
widget.setProperty("y", Arrays.asList("red", "green", "blue"));
widget.setProperty("date", new Date());
datastore.put(widget);

crea un'entità con quattro valori per la proprietà x, tre valori per la proprietà y e date impostati sulla data corrente. Ciò richiederà 12 voci di indice, una per ogni possibile combinazione di valori di proprietà:

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

Quando la stessa proprietà viene ripetuta più volte, Datastore può rilevare indici esplosivi e suggerire un indice alternativo. Tuttavia, in tutte le altre circostanze (come la query definita in questo esempio), il datastore genererà un indice enorme. In questo caso, puoi eludere lindice enorme configurando manualmente un indice nel file di configurazione degli indici:

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes autoGenerate="false">
  <datastore-index kind="Widget">
    <property name="x" direction="asc" />
    <property name="date" direction="asc" />
  </datastore-index>
  <datastore-index kind="Widget">
    <property name="y" direction="asc" />
    <property name="date" direction="asc" />
  </datastore-index>
</datastore-indexes>
In questo modo il numero di voci necessario sarà ridotto a (|x| * |date| + |y| * |date|) o a 7 voci anziché 12:

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

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

Qualsiasi operazione put che comporterebbe il superamento della voce di indice o del limite di dimensione da parte di un indice non riuscirà con un elemento IllegalArgumentException. Il testo dell'eccezione descrive quale limite è stato superato ("Too many indexed properties" o "Index entries too large") e quale indice personalizzato ne è stata la causa. Se crei un nuovo indice che supererebbe i limiti per qualsiasi entità al momento della creazione, le query sull'indice non andranno a buon fine e l'indice verrà visualizzato nello stato Error nella console Google Cloud. Per risolvere gli indici nello stato Error:

  1. Rimuovi l'indice con stato Error dal file datastore-indexes.xml.

  2. Esegui questo comando dalla directory in cui si trova datastore-indexes.xml per rimuovere l'indice da Datastore:

    gcloud datastore indexes cleanup datastore-indexes.xml
    
  3. Risolvi la causa dell'errore. Ad esempio:

    • Riformula la definizione dell'indice e le query corrispondenti.
    • Rimuovi le entità che causano l'esplosione dell'indice.
  4. Aggiungi di nuovo l'indice al file datastore-indexes.xml.

  5. Esegui questo comando dalla directory in cui si trova datastore-indexes.xml per creare l'indice in Datastore:

    gcloud datastore indexes create datastore-indexes.xml
    

Puoi evitare di esplodere gli indici evitando di eseguire query che richiederebbero un indice personalizzato, utilizzando una proprietà elenco. Come descritto sopra, sono incluse le query con più ordini di ordinamento o query con una combinazione di filtri di uguaglianza e disuguaglianza.